From ddb7fb89c0964991a7a7a371056e65617528db57 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 2 Jan 2024 17:15:57 -0800 Subject: [PATCH 001/159] Add missing Scheduler Relationships --- .../tables/public_scheduling_condition.yaml | 11 ++++++ .../tables/public_scheduling_goal.yaml | 11 ++++++ .../public_scheduling_goal_analysis.yaml | 3 ++ ...ling_goal_analysis_created_activities.yaml | 4 ++ ...g_goal_analysis_satisfying_activities.yaml | 4 ++ .../tables/public_scheduling_request.yaml | 37 +++++++++++++++++++ .../public_scheduling_specification.yaml | 33 +++++++++++++++++ 7 files changed, 103 insertions(+) diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition.yaml index 313c126afa..fdb37c7dce 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition.yaml @@ -9,6 +9,17 @@ array_relationships: table: name: scheduling_specification_conditions schema: public +remote_relationships: +- name: model + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: public + name: mission_model + field_mapping: + model_id: id select_permissions: - role: aerie_admin permission: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml index 4e425d7ab3..cf7720e5a0 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml @@ -26,6 +26,17 @@ array_relationships: insertion_order: null column_mapping: id: goal_id +remote_relationships: +- name: model + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: public + name: mission_model + field_mapping: + model_id: id select_permissions: - role: aerie_admin permission: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml index d389ed442b..590ff89be1 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml @@ -11,6 +11,9 @@ object_relationships: insertion_order: null column_mapping: analysis_id: analysis_id +- name: goal + using: + foreign_key_constraint_on: goal_id array_relationships: - name: satisfying_activities using: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_created_activities.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_created_activities.yaml index 784635e9b2..b947f998b3 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_created_activities.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_created_activities.yaml @@ -1,6 +1,10 @@ table: name: scheduling_goal_analysis_created_activities schema: public +object_relationships: +- name: analysis + using: + foreign_key_constraint_on: analysis_id select_permissions: - role: aerie_admin permission: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_satisfying_activities.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_satisfying_activities.yaml index d82de4ca83..2880149ef3 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_satisfying_activities.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis_satisfying_activities.yaml @@ -1,6 +1,10 @@ table: name: scheduling_goal_analysis_satisfying_activities schema: public +object_relationships: +- name: analysis + using: + foreign_key_constraint_on: analysis_id select_permissions: - role: aerie_admin permission: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_request.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_request.yaml index 4ef0283cf2..d613f1c75d 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_request.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_request.yaml @@ -1,6 +1,43 @@ table: name: scheduling_request schema: public +object_relationships: +- name: scheduling_specification + using: + foreign_key_constraint_on: specification_id +array_relationships: +- name: goal_analysis + using: + foreign_key_constraint_on: + column: analysis_id + table: + name: scheduling_goal_analysis + schema: public +- name: satisfying_activities + using: + foreign_key_constraint_on: + column: analysis_id + table: + name: scheduling_goal_analysis_satisfying_activities + schema: public +- name: created_activities + using: + foreign_key_constraint_on: + column: analysis_id + table: + name: scheduling_goal_analysis_created_activities + schema: public +remote_relationships: +- name: simulation_dataset + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: public + name: simulation_dataset + field_mapping: + dataset_id: dataset_id select_permissions: - role: aerie_admin permission: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification.yaml index 8dd63f050c..5d52fc8fce 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification.yaml @@ -1,6 +1,39 @@ table: name: scheduling_specification schema: public +array_relationships: +- name: goals + using: + foreign_key_constraint_on: + column: specification_id + table: + name: scheduling_specification_goals + schema: public +- name: conditions + using: + foreign_key_constraint_on: + column: specification_id + table: + name: scheduling_specification_conditions + schema: public +- name: requests + using: + foreign_key_constraint_on: + column: specification_id + table: + name: scheduling_request + schema: public +remote_relationships: +- name: plan + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: public + name: plan + field_mapping: + plan_id: id select_permissions: - role: aerie_admin permission: From 4153f0ea7afa483474e55276c2a8e554ff979c80 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 21 Dec 2023 16:13:18 -0800 Subject: [PATCH 002/159] Replace temp table in validate_anchors with stable function Postgres will take out table-level locks even when working with temp tables, and because `validate_anchors` is called whenever an activity is added or has its anchor definition altered, transactions that created or altered activities had their number of in-use locks be unexpectedly high. --- .../AerieMerlin/35_remove_temp_table/down.sql | 165 +++++++++++++++++ .../AerieMerlin/35_remove_temp_table/up.sql | 174 ++++++++++++++++++ .../sql/merlin/applied_migrations.sql | 1 + .../tables/anchor_validation_status.sql | 51 +++-- 4 files changed, 371 insertions(+), 20 deletions(-) create mode 100644 deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/down.sql create mode 100644 deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/up.sql diff --git a/deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/down.sql b/deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/down.sql new file mode 100644 index 0000000000..66f4eafe87 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/down.sql @@ -0,0 +1,165 @@ +create or replace function validate_anchors() + returns trigger + security definer + language plpgsql as $$ +declare + end_anchor_id integer; + invalid_descendant_act_ids integer[]; + offset_from_end_anchor interval; + offset_from_plan_start interval; +begin + -- Clear the reason invalid field (if an exception is thrown, this will be rolled back) + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + values (new.id, new.plan_id, '') + on conflict (activity_id, plan_id) do update + set reason_invalid = ''; + + -- An activity cannot anchor to itself + if(new.anchor_id = new.id) then + raise exception 'Cannot anchor activity % to itself.', new.anchor_id; + end if; + + -- Validate that no cycles were added + if exists( + with recursive history(activity_id, anchor_id, is_cycle, path) as ( + select new.id, new.anchor_id, false, array[new.id] + union all + select ad.id, ad.anchor_id, + ad.id = any(path), + path || ad.id + from activity_directive ad, history h + where (ad.id, ad.plan_id) = (h.anchor_id, new.plan_id) + and not is_cycle + ) select * from history + where is_cycle + limit 1 + ) then + raise exception 'Cycle detected. Cannot apply changes.'; + end if; + + /* + An activity directive may have a negative offset from its anchor's start time. + If its anchor is anchored to the end time of another activity (or so on up the chain), the activity with a + negative offset must come out to have a positive offset relative to that end time anchor. + */ + call validate_nonnegative_net_end_offset(new.id, new.plan_id); + call validate_nonegative_net_plan_start(new.id, new.plan_id); + + /* + Everything below validates that the activities anchored to this one did not become invalid as a result of these changes. + + This only checks descendent start-time anchors, as we know that the state after an end-time anchor is valid + (As if it no longer is, it will be caught when that activity's row is processed by this trigger) + */ + -- Get collection of dependent activities, with offset relative to this activity + create temp table dependent_activities as + with recursive d_activities(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, ad.start_offset + from activity_directive ad + where (ad.anchor_id, ad.plan_id) = (new.id, new.plan_id) -- select all activities anchored to this one + union + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, da.total_offset + ad.start_offset + from activity_directive ad, d_activities da + where (ad.anchor_id, ad.plan_id) = (da.activity_id, new.plan_id) -- select all activities anchored to those in the selection + and ad.anchored_to_start -- stop at next end-time anchor + ) select activity_id, total_offset + from d_activities da; + + -- Get the total offset from the most recent end-time anchor earlier in this activity's chain (or null if there is none) + with recursive end_time_anchor(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( + select new.id, new.anchor_id, new.anchored_to_start, new.start_offset, new.start_offset + union + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, eta.total_offset + ad.start_offset + from activity_directive ad, end_time_anchor eta + where (ad.id, ad.plan_id) = (eta.anchor_id, new.plan_id) + and eta.anchor_id is not null -- stop at plan + and eta.anchored_to_start -- or stop at end time anchor + ) select into end_anchor_id, offset_from_end_anchor + anchor_id, total_offset from end_time_anchor eta -- get the id of the activity that the selected activity is anchored to + where not eta.anchored_to_start and eta.anchor_id is not null + limit 1; + + -- Not null iff the activity being looked at has some end anchor to another activity in its chain + if offset_from_end_anchor is not null then + select array_agg(activity_id) from dependent_activities + where total_offset + offset_from_end_anchor < '0' + into invalid_descendant_act_ids; + + if invalid_descendant_act_ids is not null then + raise info 'The following Activity Directives now have a net negative offset relative to an end-time anchor on Activity Directive %: % \n' + 'There may be additional activities that are invalid relative to this activity.', + end_anchor_id, array_to_string(invalid_descendant_act_ids, ','); + + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select id, new.plan_id, 'Activity Directive ' || id || ' has a net negative offset relative to an end-time' || + ' anchor on Activity Directive ' || end_anchor_id ||'.' + from unnest(invalid_descendant_act_ids) as id + on conflict (activity_id, plan_id) do update + set reason_invalid = 'Activity Directive ' || excluded.activity_id || ' has a net negative offset relative to an end-time' || + ' anchor on Activity Directive ' || end_anchor_id ||'.'; + end if; + end if; + + -- Gets the total offset from plan start (or null if there's an end-time anchor in the way) + with recursive anchors(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( + select new.id, new.anchor_id, new.anchored_to_start, new.start_offset, new.start_offset + union + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, anchors.total_offset + ad.start_offset + from activity_directive ad, anchors + where anchors.anchor_id is not null -- stop at plan + and (ad.id, ad.plan_id) = (anchors.anchor_id, new.plan_id) + and anchors.anchored_to_start -- or, stop at end-time offset + ) + select total_offset -- get the id of the activity that the selected activity is anchored to + from anchors a + where a.anchor_id is null + and a.anchored_to_start + limit 1 + into offset_from_plan_start; + + -- Not null iff the activity being looked at is connected to plan start via a chain of start anchors + if offset_from_plan_start is not null then + -- Validate descendents + invalid_descendant_act_ids := null; + select array_agg(activity_id) from dependent_activities + where total_offset + offset_from_plan_start < '0' into invalid_descendant_act_ids; -- grab all and split + + if invalid_descendant_act_ids is not null then + raise info 'The following Activity Directives now have a net negative offset relative to Plan Start: % \n' + 'There may be additional activities that are invalid relative to this activity.', + array_to_string(invalid_descendant_act_ids, ','); + + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select id, new.plan_id, 'Activity Directive ' || id || ' has a net negative offset relative to Plan Start.' + from unnest(invalid_descendant_act_ids) as id + on conflict (activity_id, plan_id) do update + set reason_invalid = 'Activity Directive ' || excluded.activity_id || ' has a net negative offset relative to Plan Start.'; + end if; + end if; + + -- These are both null iff the activity is anchored to plan end + if(offset_from_plan_start is null and offset_from_end_anchor is null) then + -- All dependent activities should have no errors, as Plan End can have an offset of any value. + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select da.activity_id, new.plan_id, '' + from dependent_activities as da + on conflict (activity_id, plan_id) do update + set reason_invalid = ''; + end if; + + -- Remove the error from the dependent activities that wouldn't have been flagged by the earlier checks. + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select da.activity_id, new.plan_id, '' + from dependent_activities as da + where total_offset + offset_from_plan_start >= '0' + or total_offset + offset_from_end_anchor >= '0' -- only one of these checks will run depending on which one has `null` behind the offset + on conflict (activity_id, plan_id) do update + set reason_invalid = ''; + + drop table dependent_activities; + return new; +end $$; + +drop function get_dependent_activities(_activity_id int, _plan_id int); + +call migrations.mark_migration_rolled_back('35'); diff --git a/deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/up.sql b/deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/up.sql new file mode 100644 index 0000000000..c3499f5935 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/35_remove_temp_table/up.sql @@ -0,0 +1,174 @@ +create function get_dependent_activities(_activity_id int, _plan_id int) + returns table(activity_id int, total_offset interval) + stable + language plpgsql as $$ +begin + return query + with recursive d_activities(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, ad.start_offset + from activity_directive ad + where (ad.anchor_id, ad.plan_id) = (_activity_id, _plan_id) -- select all activities anchored to this one + union + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, da.total_offset + ad.start_offset + from activity_directive ad, d_activities da + where (ad.anchor_id, ad.plan_id) = (da.activity_id, _plan_id) -- select all activities anchored to those in the selection + and ad.anchored_to_start -- stop at next end-time anchor + ) select da.activity_id, da.total_offset + from d_activities da; +end; +$$; + +comment on function get_dependent_activities(_activity_id int, _plan_id int) is e'' +'Get the collection of activities that depend on the given activity, with offset relative to the specified activity'; + +create or replace function validate_anchors() + returns trigger + security definer + language plpgsql as $$ +declare + end_anchor_id integer; + invalid_descendant_act_ids integer[]; + offset_from_end_anchor interval; + offset_from_plan_start interval; +begin + -- Clear the reason invalid field (if an exception is thrown, this will be rolled back) + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + values (new.id, new.plan_id, '') + on conflict (activity_id, plan_id) do update + set reason_invalid = ''; + + -- An activity cannot anchor to itself + if(new.anchor_id = new.id) then + raise exception 'Cannot anchor activity % to itself.', new.anchor_id; + end if; + + -- Validate that no cycles were added + if exists( + with recursive history(activity_id, anchor_id, is_cycle, path) as ( + select new.id, new.anchor_id, false, array[new.id] + union all + select ad.id, ad.anchor_id, + ad.id = any(path), + path || ad.id + from activity_directive ad, history h + where (ad.id, ad.plan_id) = (h.anchor_id, new.plan_id) + and not is_cycle + ) select * from history + where is_cycle + limit 1 + ) then + raise exception 'Cycle detected. Cannot apply changes.'; + end if; + + /* + An activity directive may have a negative offset from its anchor's start time. + If its anchor is anchored to the end time of another activity (or so on up the chain), the activity with a + negative offset must come out to have a positive offset relative to that end time anchor. + */ + call validate_nonnegative_net_end_offset(new.id, new.plan_id); + call validate_nonegative_net_plan_start(new.id, new.plan_id); + + /* + Everything below validates that the activities anchored to this one did not become invalid as a result of these changes. + + This only checks descendent start-time anchors, as we know that the state after an end-time anchor is valid + (As if it no longer is, it will be caught when that activity's row is processed by this trigger) + */ + -- Get the total offset from the most recent end-time anchor earlier in this activity's chain (or null if there is none) + with recursive end_time_anchor(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( + select new.id, new.anchor_id, new.anchored_to_start, new.start_offset, new.start_offset + union + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, eta.total_offset + ad.start_offset + from activity_directive ad, end_time_anchor eta + where (ad.id, ad.plan_id) = (eta.anchor_id, new.plan_id) + and eta.anchor_id is not null -- stop at plan + and eta.anchored_to_start -- or stop at end time anchor + ) select into end_anchor_id, offset_from_end_anchor + anchor_id, total_offset from end_time_anchor eta -- get the id of the activity that the selected activity is anchored to + where not eta.anchored_to_start and eta.anchor_id is not null + limit 1; + + -- Not null iff the activity being looked at has some end anchor to another activity in its chain + if offset_from_end_anchor is not null then + select array_agg(activity_id) + from get_dependent_activities(new.id, new.plan_id) + where total_offset + offset_from_end_anchor < '0' + into invalid_descendant_act_ids; + + if invalid_descendant_act_ids is not null then + raise info 'The following Activity Directives now have a net negative offset relative to an end-time anchor on Activity Directive %: % \n' + 'There may be additional activities that are invalid relative to this activity.', + end_anchor_id, array_to_string(invalid_descendant_act_ids, ','); + + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select id, new.plan_id, 'Activity Directive ' || id || ' has a net negative offset relative to an end-time' || + ' anchor on Activity Directive ' || end_anchor_id ||'.' + from unnest(invalid_descendant_act_ids) as id + on conflict (activity_id, plan_id) do update + set reason_invalid = 'Activity Directive ' || excluded.activity_id || ' has a net negative offset relative to an end-time' || + ' anchor on Activity Directive ' || end_anchor_id ||'.'; + end if; + end if; + + -- Gets the total offset from plan start (or null if there's an end-time anchor in the way) + with recursive anchors(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( + select new.id, new.anchor_id, new.anchored_to_start, new.start_offset, new.start_offset + union + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, anchors.total_offset + ad.start_offset + from activity_directive ad, anchors + where anchors.anchor_id is not null -- stop at plan + and (ad.id, ad.plan_id) = (anchors.anchor_id, new.plan_id) + and anchors.anchored_to_start -- or, stop at end-time offset + ) + select total_offset -- get the id of the activity that the selected activity is anchored to + from anchors a + where a.anchor_id is null + and a.anchored_to_start + limit 1 + into offset_from_plan_start; + + -- Not null iff the activity being looked at is connected to plan start via a chain of start anchors + if offset_from_plan_start is not null then + -- Validate descendents + invalid_descendant_act_ids := null; + select array_agg(activity_id) + from get_dependent_activities(new.id, new.plan_id) + where total_offset + offset_from_plan_start < '0' + into invalid_descendant_act_ids; -- grab all and split + + if invalid_descendant_act_ids is not null then + raise info 'The following Activity Directives now have a net negative offset relative to Plan Start: % \n' + 'There may be additional activities that are invalid relative to this activity.', + array_to_string(invalid_descendant_act_ids, ','); + + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select id, new.plan_id, 'Activity Directive ' || id || ' has a net negative offset relative to Plan Start.' + from unnest(invalid_descendant_act_ids) as id + on conflict (activity_id, plan_id) do update + set reason_invalid = 'Activity Directive ' || excluded.activity_id || ' has a net negative offset relative to Plan Start.'; + end if; + end if; + + -- These are both null iff the activity is anchored to plan end + if(offset_from_plan_start is null and offset_from_end_anchor is null) then + -- All dependent activities should have no errors, as Plan End can have an offset of any value. + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select da.activity_id, new.plan_id, '' + from get_dependent_activities(new.id, new.plan_id) as da + on conflict (activity_id, plan_id) do update + set reason_invalid = ''; + end if; + + -- Remove the error from the dependent activities that wouldn't have been flagged by the earlier checks. + insert into anchor_validation_status (activity_id, plan_id, reason_invalid) + select da.activity_id, new.plan_id, '' + from get_dependent_activities(new.id, new.plan_id) as da + where total_offset + offset_from_plan_start >= '0' + or total_offset + offset_from_end_anchor >= '0' -- only one of these checks will run depending on which one has `null` behind the offset + on conflict (activity_id, plan_id) do update + set reason_invalid = ''; + + return new; +end $$; + +call migrations.mark_migration_applied('35'); diff --git a/merlin-server/sql/merlin/applied_migrations.sql b/merlin-server/sql/merlin/applied_migrations.sql index d8f06465f9..0d4d1d7177 100644 --- a/merlin-server/sql/merlin/applied_migrations.sql +++ b/merlin-server/sql/merlin/applied_migrations.sql @@ -37,3 +37,4 @@ call migrations.mark_migration_applied('31'); call migrations.mark_migration_applied('32'); call migrations.mark_migration_applied('33'); call migrations.mark_migration_applied('34'); +call migrations.mark_migration_applied('35'); diff --git a/merlin-server/sql/merlin/tables/anchor_validation_status.sql b/merlin-server/sql/merlin/tables/anchor_validation_status.sql index bb4530c0d7..f5e1a0ba0c 100644 --- a/merlin-server/sql/merlin/tables/anchor_validation_status.sql +++ b/merlin-server/sql/merlin/tables/anchor_validation_status.sql @@ -9,6 +9,29 @@ create table anchor_validation_status( on delete cascade ); +create function get_dependent_activities(_activity_id int, _plan_id int) + returns table(activity_id int, total_offset interval) + stable + language plpgsql as $$ +begin + return query + with recursive d_activities(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, ad.start_offset + from activity_directive ad + where (ad.anchor_id, ad.plan_id) = (_activity_id, _plan_id) -- select all activities anchored to this one + union + select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, da.total_offset + ad.start_offset + from activity_directive ad, d_activities da + where (ad.anchor_id, ad.plan_id) = (da.activity_id, _plan_id) -- select all activities anchored to those in the selection + and ad.anchored_to_start -- stop at next end-time anchor + ) select da.activity_id, da.total_offset + from d_activities da; +end; +$$; + +comment on function get_dependent_activities(_activity_id int, _plan_id int) is e'' +'Get the collection of activities that depend on the given activity, with offset relative to the specified activity'; + create index anchor_validation_plan_id_index on anchor_validation_status (plan_id); comment on index anchor_validation_plan_id_index is e'' @@ -191,20 +214,6 @@ begin This only checks descendent start-time anchors, as we know that the state after an end-time anchor is valid (As if it no longer is, it will be caught when that activity's row is processed by this trigger) */ - -- Get collection of dependent activities, with offset relative to this activity - create temp table dependent_activities as - with recursive d_activities(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( - select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, ad.start_offset - from activity_directive ad - where (ad.anchor_id, ad.plan_id) = (new.id, new.plan_id) -- select all activities anchored to this one - union - select ad.id, ad.anchor_id, ad.anchored_to_start, ad.start_offset, da.total_offset + ad.start_offset - from activity_directive ad, d_activities da - where (ad.anchor_id, ad.plan_id) = (da.activity_id, new.plan_id) -- select all activities anchored to those in the selection - and ad.anchored_to_start -- stop at next end-time anchor - ) select activity_id, total_offset - from d_activities da; - -- Get the total offset from the most recent end-time anchor earlier in this activity's chain (or null if there is none) with recursive end_time_anchor(activity_id, anchor_id, anchored_to_start, start_offset, total_offset) as ( select new.id, new.anchor_id, new.anchored_to_start, new.start_offset, new.start_offset @@ -221,7 +230,8 @@ begin -- Not null iff the activity being looked at has some end anchor to another activity in its chain if offset_from_end_anchor is not null then - select array_agg(activity_id) from dependent_activities + select array_agg(activity_id) + from get_dependent_activities(new.id, new.plan_id) where total_offset + offset_from_end_anchor < '0' into invalid_descendant_act_ids; @@ -261,8 +271,10 @@ begin if offset_from_plan_start is not null then -- Validate descendents invalid_descendant_act_ids := null; - select array_agg(activity_id) from dependent_activities - where total_offset + offset_from_plan_start < '0' into invalid_descendant_act_ids; -- grab all and split + select array_agg(activity_id) + from get_dependent_activities(new.id, new.plan_id) + where total_offset + offset_from_plan_start < '0' + into invalid_descendant_act_ids; -- grab all and split if invalid_descendant_act_ids is not null then raise info 'The following Activity Directives now have a net negative offset relative to Plan Start: % \n' @@ -282,7 +294,7 @@ begin -- All dependent activities should have no errors, as Plan End can have an offset of any value. insert into anchor_validation_status (activity_id, plan_id, reason_invalid) select da.activity_id, new.plan_id, '' - from dependent_activities as da + from get_dependent_activities(new.id, new.plan_id) as da on conflict (activity_id, plan_id) do update set reason_invalid = ''; end if; @@ -290,13 +302,12 @@ begin -- Remove the error from the dependent activities that wouldn't have been flagged by the earlier checks. insert into anchor_validation_status (activity_id, plan_id, reason_invalid) select da.activity_id, new.plan_id, '' - from dependent_activities as da + from get_dependent_activities(new.id, new.plan_id) as da where total_offset + offset_from_plan_start >= '0' or total_offset + offset_from_end_anchor >= '0' -- only one of these checks will run depending on which one has `null` behind the offset on conflict (activity_id, plan_id) do update set reason_invalid = ''; - drop table dependent_activities; return new; end $$; From a64792e35df198a37dd139671c94da69acdde6d6 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Fri, 22 Dec 2023 09:20:14 -0800 Subject: [PATCH 003/159] Update test setup - Run both test files - Remove tsconfig checking `e2eTests` for `.ts` files --- load-tests/load-test.sh | 4 ++++ load-tests/tsconfig.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/load-tests/load-test.sh b/load-tests/load-test.sh index f9fea7adcb..f0ace33962 100755 --- a/load-tests/load-test.sh +++ b/load-tests/load-test.sh @@ -46,3 +46,7 @@ curl -L "https://github.com/grafana/xk6-dashboard/releases/download/${version}/x --out "json=load-report.json" \ --out "dashboard=period=1s&report=load-report.html" \ ./dist/load-test.js +./k6 run \ + --out "json=load-report.json" \ + --out "dashboard=period=1s&report=load-report.html" \ + ./dist/db-lockup-test.js diff --git a/load-tests/tsconfig.json b/load-tests/tsconfig.json index 0097ea7663..6fbcd6bcb3 100644 --- a/load-tests/tsconfig.json +++ b/load-tests/tsconfig.json @@ -24,5 +24,5 @@ "skipLibCheck": true }, - "include": ["../e2e-tests/src/**/*.d.ts", "src/**/*.ts"] + "include": ["src/**/*.ts"] } From cc2f53ae7b1aca7454fdf95484455f38f414673a Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 2 Jan 2024 14:03:03 -0800 Subject: [PATCH 004/159] Update existing load tests - Add types - extract duplicated request code in load-test.ts into requests.ts - simplify user-related fields into User type - add a "targeted_plan_id" so that load tests will still work even if there is no plan with id 1 - Remove leakiness in existing load tests --- load-tests/src/load-test.ts | 149 +++++++------------- load-tests/src/requests.ts | 170 +++++++++++++++++------ load-tests/src/types/activity.d.ts | 12 ++ load-tests/src/types/auth.d.ts | 10 ++ load-tests/src/types/effective_args.d.ts | 10 ++ load-tests/src/types/model.d.ts | 16 +++ load-tests/src/types/plan.d.ts | 23 +++ load-tests/src/types/simulation.d.ts | 6 + 8 files changed, 258 insertions(+), 138 deletions(-) create mode 100644 load-tests/src/types/activity.d.ts create mode 100644 load-tests/src/types/auth.d.ts create mode 100644 load-tests/src/types/effective_args.d.ts create mode 100644 load-tests/src/types/model.d.ts create mode 100644 load-tests/src/types/plan.d.ts create mode 100644 load-tests/src/types/simulation.d.ts diff --git a/load-tests/src/load-test.ts b/load-tests/src/load-test.ts index 80552a4349..0e27305b6a 100644 --- a/load-tests/src/load-test.ts +++ b/load-tests/src/load-test.ts @@ -1,16 +1,20 @@ -import http from 'k6/http'; import { sleep, check } from 'k6'; import { Trend } from 'k6/metrics'; -import { Options } from 'k6/options'; +import type {Options} from 'k6/options'; import { req } from './requests'; import gql from '../assets/gql'; -import * as urls from '../assets/urls'; +import type {User} from "./types/auth"; +import {CreatePlanInput} from "./types/plan"; +import {ActivityInsertInput} from "./types/activity"; +import {EffectiveArgsData, EffectiveArgumentItem} from "./types/effective_args"; // Besides the default metrics k6 records, we can define our own statistics and manually add data points to them const effective_args_duration = new Trend('effective_arg_duration', true); type VUSharedData = { - token: string + user: User + targeted_plan_id: number + model_id: number }; // The options object configures our overall load test @@ -105,92 +109,69 @@ export const options: Options = { }; const plan_start_timestamp = '2021-001T00:00:00.000'; -const jar = open('./banananation.jar', 'b'); -const username = "load-tester"; -// this randomness is run once per VU -const rd = Math.random() * 10000; +const jar = open('./banananation.jar', 'b') // `setup()` runs once per k6 instance, and sets up a shared mission model for all VUs // VUSharedData is a custom type we return from the `setup()` function // This data is then accessible as an argument for other load testing functions export function setup(): VUSharedData { - + const username = "load-tester"; const user = req.login(username); - const token = user.token; - check(user, { - 'user has correct name': (user) => user.id === username + 'user has correct name': (user) => user.username === username }); - // upload banananation jar - const file = { - type: "application/java-archive", - file: http.file(jar) - }; - - const res = http.post(`${urls.GATEWAY_URL}/file`, file); - - check(res, { - 'is status 200': (r) => r.status === 200, - }); - - const jar_data = res.json(); - const jar_id = jar_data.id; + const modelId = req.uploadMissionModel(jar, user); + sleep(2); - check(jar_id, { - "valid id": (id) => Number.isInteger(id) - }); + const planInput: CreatePlanInput = { + model_id: modelId, + name: "load test targeted plan", + start_time: plan_start_timestamp, + duration: "1d" + } + const planId = req.createPlan(planInput, user); - // create mission model - const modelInput: MissionModelInsertInput = { - jar_id, - mission: "aerie-load-test" + rd, - name: "Banananation (load-test)"+rd, - version: "0.0.0"+ rd, + return { + user, + targeted_plan_id: planId, + model_id: modelId, }; +} - const model_resp = req.hasura(gql.CREATE_MISSION_MODEL, {model: modelInput}, token, username); - const model_data = model_resp.json("data"); - check(model_data, { - "model id is valid": (data) => Number.isInteger(data.insert_mission_model_one.id) - }); - - sleep(2); +export function teardown(data: VUSharedData) { + // 3. teardown code + // Runs once after all tests are finished - return { - token - }; + // Remove all plans using the uploaded model + req.removePlansForModel(data.model_id, data.user); + // Remove mission model + req.removeModel(data.model_id, data.user); } export function insert_plans(data: VUSharedData) { - const { token } = data; + const { user, model_id } = data; // generate new randomness per iteration const rd = Math.random() * 10000; - const random_mission_id = req.get_random_mission_model_id(token, username); - const input: CreatePlanInput = { - model_id: random_mission_id, - name: `test_plan_${rd}_${random_mission_id}`, + model_id: model_id, + name: `test_plan_${rd}`, start_time: plan_start_timestamp, duration: "1d" }; - const resp = req.hasura(gql.CREATE_PLAN, {plan: input}, token, username); - const plan_data = resp.json("data"); - const { insert_plan_one } = plan_data; - const plan_id = insert_plan_one.id; - + const plan_id = req.createPlan(input, user); check(plan_id, { "plan_id is a number": (plan_id) => Number.isInteger(plan_id) }); } export function insert_random_activities(data: VUSharedData) { - const { token } = data; + const { user, model_id } = data; const random_hour_offset = Math.round(Math.random() * 24); - const random_id = req.get_random_plan_id(token, username); + const random_id = req.get_random_plan_id(model_id, user); const input: ActivityInsertInput = { arguments: { @@ -201,82 +182,52 @@ export function insert_random_activities(data: VUSharedData) { start_offset: random_hour_offset + 'h', }; - const resp = req.hasura(gql.CREATE_ACTIVITY_DIRECTIVE, {activityDirectiveInsertInput: input}, token, username); - const act_data = resp.json("data"); - const { createActivityDirective } = act_data; - const act_id = createActivityDirective.id; - - check(act_id, { - "act dir id is a number": (act_id) => Number.isInteger(act_id) - }); + req.createActivityDirective(input, user); } // "Targeted" means insert lots of activities to plan_id 1 export function insert_targeted_activities(data: VUSharedData) { - const { token } = data; + const { user, targeted_plan_id: planId } = data; const random_seconds_offset = Math.round(Math.random() * 24 * 60 * 60); const input: ActivityInsertInput = { arguments: { biteSize: 1, }, - plan_id: 1, + plan_id: planId, type: 'BiteBanana', start_offset: random_seconds_offset + 's', }; - const resp = req.hasura(gql.CREATE_ACTIVITY_DIRECTIVE, {activityDirectiveInsertInput: input}, token, username); - const act_data = resp.json("data"); - const { createActivityDirective } = act_data; - const act_id = createActivityDirective.id; - - check(act_id, { - "act dir id is a number": (act_id) => Number.isInteger(act_id) - }); + req.createActivityDirective(input, user); } // Targeted simulations only request simulations on plan_id 1, which is packed full of activities export function run_targeted_simulations(data: VUSharedData) { - const { token } = data; - - const resp = req.hasura(gql.SIMULATE, {plan_id: 1}, token, username); - - const sim_data = resp.json("data"); - - check(sim_data, { - "got sim dataset id": (sim_data) => Number.isInteger(sim_data.simulate.simulationDatasetId) - }); + const { user, targeted_plan_id: planId } = data; + req.simulate(planId, user); } export function run_random_simulations(data: VUSharedData) { - const { token } = data; - const random_id = req.get_random_plan_id(token, username); - - const resp = req.hasura(gql.SIMULATE, {plan_id: random_id}, token, username); - - const sim_data = resp.json("data"); - - check(sim_data, { - "got sim dataset id": (sim_data) => Number.isInteger(sim_data.simulate.simulationDatasetId) - }); + const { user, model_id } = data; + const random_id = req.get_random_plan_id(model_id, user); + req.simulate(random_id, user); } export function get_effective_args(data: VUSharedData) { - const { token } = data; + const { user, model_id } = data; const input: EffectiveArgumentItem = { activityTypeName: "BiteBanana", activityArguments: {} }; - const random_id = req.get_random_mission_model_id(token, username); - - const resp = req.hasura(gql.GET_EFFECTIVE_ACTIVITY_ARGUMENTS_BULK, { modelId: random_id, activities: input }, token, username); + const resp = req.hasura(gql.GET_EFFECTIVE_ACTIVITY_ARGUMENTS_BULK, { modelId: model_id, activities: input }, user); // we are able to manually add data points to our custom metrics as follows effective_args_duration.add(resp.timings.duration); - const effective_args_data = resp.json("data"); + const effective_args_data = resp.json("data") as EffectiveArgsData; const effective_args = effective_args_data.getActivityEffectiveArgumentsBulk[0]; check(effective_args, { diff --git a/load-tests/src/requests.ts b/load-tests/src/requests.ts index 2bed21e1ff..2e7dfb2308 100644 --- a/load-tests/src/requests.ts +++ b/load-tests/src/requests.ts @@ -1,27 +1,36 @@ import http, { RefinedResponse, ResponseType } from "k6/http"; import * as urls from "../assets/urls"; import { JSONObject, check } from "k6"; +import type {LoginResponse, User} from "./types/auth"; +import type { + UploadJarResponse, + MissionModelInsertInput, + MissionModelInsertResponse, +} from "./types/model"; +import gql from "../assets/gql"; +import {CreatePlanInput, CreatePlanResponse, DuplicatePlanResponse, PlanIdList} from "./types/plan"; +import {ActivityInsertInput, CreateActivityResponse} from "./types/activity"; +import {SimulateResponse} from "./types/simulation"; export const req = { hasura( query: string, variables: JSONObject, - token: string, - username: string + user: User ): RefinedResponse { const headers = { - 'Authorization': `Bearer ${token}`, + 'Authorization': `Bearer ${user.token}`, 'Content-Type': 'application/json', 'x-hasura-role': 'aerie_admin', - 'x-hasura-user-id': `${username}` + 'x-hasura-user-id': `${user.username}` }; const body = JSON.stringify({ query, variables - }); + }); const response = http.post(`${urls.HASURA_URL}/v1/graphql`, body, { headers }); @@ -32,17 +41,17 @@ export const req = { return response; }, - get_random_plan_id(token: string, username: string) { + get_random_plan_id(model_id: number, user: User) { + const variables = {model_id: model_id} const query = `#graphql - query { - plan { + query GetPlansForModel($model_id: Int!) { + plan(where: {model_id: {_eq: $model_id}}) { id } } `; - const resp = req.hasura(query, {}, token, username); - const data = resp.json("data"); + const data = req.hasura(query, variables, user).json("data") as PlanIdList; const plan_ids = data.plan; check(plan_ids, { @@ -57,33 +66,8 @@ export const req = { return random_plan.id; }, - get_random_mission_model_id(token: string, username: string) { - const query = `#graphql - query { - mission_model { - id - } - } - `; - - const resp = req.hasura(query, {}, token, username); - const data = resp.json("data"); - - const mission_models = data.mission_model; - check(mission_models, { - "mission model exists": (mission_models) => mission_models.length > 0 - }); - - const random_mission = mission_models[Math.floor(Math.random() * mission_models.length)]; - check(random_mission, { - "random mission exists": (random_mission) => random_mission !== undefined - }); - - return random_mission.id; - }, - - login(username: string) { - const response = http.post(`${urls.UI_URL}/auth/login`, + login(username: string): User { + const response = http.post(`${urls.GATEWAY_URL}/auth/login`, `{ "username": "${username}", "password": "${username}" @@ -94,7 +78,115 @@ export const req = { }, }); - const login_resp = response.json(); - return login_resp.user; + const login_resp = response.json() as LoginResponse; + return {token: login_resp.token, username: username} as User; + }, + + uploadMissionModel(jar: ArrayBuffer, user: User): number { + const rd = Math.random() * 10000; + + // upload jar + const file = { + type: "application/java-archive", + file: http.file(jar) + }; + + const res = http.post(`${urls.GATEWAY_URL}/file`, file); + + check(res, { + 'is status 200': (r) => r.status === 200, + }); + + const jar_data = res.json() as UploadJarResponse; + const jar_id = jar_data.id; + + check(jar_id, { + "valid id": (id) => Number.isInteger(id) + }); + + // add jar to aerie + // create mission model + const modelInput: MissionModelInsertInput = { + jar_id, + mission: "aerie-load-test" + rd, + name: "Banananation (load-test)" + rd, + version: "0.0.0" + rd, + }; + + const model_resp = req.hasura(gql.CREATE_MISSION_MODEL, {model: modelInput}, user); + const model_data = model_resp.json("data") as MissionModelInsertResponse; + check(model_data, { + "model id is valid": (data) => Number.isInteger(data.insert_mission_model_one.id) + }); + return model_data.insert_mission_model_one.id; + }, + + createPlan(plan: CreatePlanInput, user: User): number { + const variables = {plan: plan}; + const data = req.hasura(gql.CREATE_PLAN, variables, user).json("data") as CreatePlanResponse; + const { insert_plan_one: { id: id } } = data ; + check(id, { + "plan id is valid": (id) => Number.isInteger(id) + }); + return id; + }, + + createActivityDirective(activity: ActivityInsertInput, user: User): number { + const variables = {activityDirectiveInsertInput: activity}; + const data = req.hasura(gql.CREATE_ACTIVITY_DIRECTIVE, variables, user).json("data") as CreateActivityResponse; + const { createActivityDirective: { id: id } } = data ; + check(id, { + "act dir id is a number": (act_id) => Number.isInteger(act_id) + }); + return id; + }, + + duplicatePlan(planId: number, newName: string, user: User) { + const mutation = ` + mutation DuplicatePlan($new_plan_name: String!, $plan_id: Int!) { + duplicate_plan(args: {new_plan_name: $new_plan_name, plan_id: $plan_id}) { + new_plan_id + } + }`; + + const vars = { + new_plan_name: newName, + plan_id: planId + } + const data = req.hasura(mutation, vars, user).json("data"); + const { duplicate_plan: { new_plan_id: id } } = data as DuplicatePlanResponse; + check(id, { + "new plan id is valid": (id) => Number.isInteger(id) + }); + return id; + }, + + removePlansForModel(modelId: number, user: User) { + const query = ` + mutation RemovePlansForModel($model_id: Int!) { + delete_plan(where: {model_id: {_eq: $model_id}}) { + affected_rows + } + }`; // Doesn't handle scheduling specs + + const vars = { model_id: modelId} + req.hasura(query, vars, user); + }, + + removeModel(modelId: number, user: User) { + req.hasura(gql.DELETE_MISSION_MODEL, {id: modelId}, user); + }, + + removePlan(planId: number, user: User) { + req.hasura(gql.DELETE_PLAN, {id: planId}, user); + }, + + simulate(planId: number, user: User): SimulateResponse { + const sim_data = req.hasura(gql.SIMULATE, {plan_id: planId}, user).json("data") as SimulateResponse; + + check(sim_data, { + "sim dataset id is a number": (sim_data) => Number.isInteger(sim_data.simulate.simulationDatasetId) + }); + return sim_data; } }; diff --git a/load-tests/src/types/activity.d.ts b/load-tests/src/types/activity.d.ts new file mode 100644 index 0000000000..ba006df41f --- /dev/null +++ b/load-tests/src/types/activity.d.ts @@ -0,0 +1,12 @@ +export type ActivityInsertInput = { + arguments: any, + plan_id: number, + type: string, + start_offset: string +} + +export type CreateActivityResponse = { + createActivityDirective: { + id: number + } +} diff --git a/load-tests/src/types/auth.d.ts b/load-tests/src/types/auth.d.ts new file mode 100644 index 0000000000..6868a2936d --- /dev/null +++ b/load-tests/src/types/auth.d.ts @@ -0,0 +1,10 @@ +export type LoginResponse = { + message: string | null; + success: true; + token: string; +} + +export type User = { + username: string; + token: string; +}; diff --git a/load-tests/src/types/effective_args.d.ts b/load-tests/src/types/effective_args.d.ts new file mode 100644 index 0000000000..50dadd8365 --- /dev/null +++ b/load-tests/src/types/effective_args.d.ts @@ -0,0 +1,10 @@ +export type EffectiveArgumentItem = { + activityTypeName: string, + activityArguments: any +} + +export type EffectiveArgsData = { + getActivityEffectiveArgumentsBulk: [ + { success: boolean } + ] +} diff --git a/load-tests/src/types/model.d.ts b/load-tests/src/types/model.d.ts new file mode 100644 index 0000000000..4f4459e3d2 --- /dev/null +++ b/load-tests/src/types/model.d.ts @@ -0,0 +1,16 @@ +export type UploadJarResponse = { + id: number +} + +export type MissionModelInsertInput = { + jar_id: number, + mission: string, + name: string, + version: string +} + +export type MissionModelInsertResponse = { + insert_mission_model_one: { + id: number + } +} diff --git a/load-tests/src/types/plan.d.ts b/load-tests/src/types/plan.d.ts new file mode 100644 index 0000000000..7ad1f9d59d --- /dev/null +++ b/load-tests/src/types/plan.d.ts @@ -0,0 +1,23 @@ +export type PlanIdList = { + plan: [{ id: number } ] +} + +export type CreatePlanInput = { + model_id: number, + name: string, + start_time: string, + duration: string +} + +export type CreatePlanResponse = { + insert_plan_one: { + id: number, + revision: number + } +} + +export type DuplicatePlanResponse = { + duplicate_plan: { + new_plan_id: number + } +} diff --git a/load-tests/src/types/simulation.d.ts b/load-tests/src/types/simulation.d.ts new file mode 100644 index 0000000000..5b882cbedc --- /dev/null +++ b/load-tests/src/types/simulation.d.ts @@ -0,0 +1,6 @@ +export type SimulateResponse = { + simulate: { + status: string + simulationDatasetId: number + } +} From 45c32cdac06a0ceaed0151718137d0ff1c28c9ca Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 2 Jan 2024 14:03:35 -0800 Subject: [PATCH 005/159] Add new Load Test file for DB lockups --- load-tests/src/db-lockup-test.ts | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 load-tests/src/db-lockup-test.ts diff --git a/load-tests/src/db-lockup-test.ts b/load-tests/src/db-lockup-test.ts new file mode 100644 index 0000000000..9e05197040 --- /dev/null +++ b/load-tests/src/db-lockup-test.ts @@ -0,0 +1,82 @@ +import { sleep } from "k6"; +import { req } from './requests'; +import type { User } from "./types/auth"; +import {CreatePlanInput} from "./types/plan"; +import {ActivityInsertInput} from "./types/activity"; + +export const options = { + // A number specifying the number of VUs to run concurrently. + vus: 50, + // A string specifying the total duration of the test run. + duration: '10s', + + // If the statistics of the load test fall below the following threshold, it with exit with a nonzero exit code + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + http_req_duration: ['p(50)<300', 'p(95)<1000'], // 50% below 300ms, 95% of requests should be below 1000ms + }, +}; + +type VUSharedData = { + user: User, + planId: number, + modelId: number +} + +// 'open' is only permitted in the 'init' stage in k6 (the scope that sets up globals) +// http requests are NOT permitted in the 'init' stage in k6 +// therefore, we must grab the jars during the init stage and upload them during the setup stage +const jar = open('./banananation.jar', 'b') + +export function setup() : VUSharedData { + // 1. setup code + // Runs once + // Login + const user = req.login('lockup_tester'); + + // Upload a model + const modelId = req.uploadMissionModel(jar, user); + + // Upload initial plan + const planInput: CreatePlanInput = { + model_id: modelId, + name: "lockup test plan", + start_time: "2024-01-01T00:00:00+00:00", + duration: "400:00:00" + }; + const planId = req.createPlan(planInput, user); + + // Upload one activity per hour + for (let hour = 0; hour < 400; hour++) { + const activityInput: ActivityInsertInput = { + plan_id: planId, + type: "BiteBanana", + arguments: {}, + start_offset: hour+":00:00" + } + req.createActivityDirective(activityInput, user); + } + + return { user: user, planId: planId, modelId: modelId }; +} + +export default function (data: VUSharedData) { + // 2. VU code + // Runs once per iteration + req.duplicatePlan(data.planId, "duplicate_loadtest_branch_" +Math.trunc(Math.random() * 10000000).toString(), data.user); + sleep(1); +} + + +export function teardown(data: VUSharedData) { + // 3. teardown code + // Runs once + + // Remove parent plan + req.removePlan(data.planId, data.user); + // Remove all plans using the uploaded model + req.removePlansForModel(data.modelId, data.user); + // Remove mission model + req.removeModel(data.modelId, data.user); +} + From 24e345931cd3c3bf9944efc50bacd915ea3960b2 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Mon, 18 Dec 2023 12:04:08 -1000 Subject: [PATCH 006/159] Include JPL Specific Reference Support Due to limitations in the eDSL, reference variables utilized by JPLs coreFSW weren't directly supported. Existing tools like SeqGen and SeqAdaption addressed this gap with a workaround. I've implemented functionality within the eDSL to enable creation of reference variables that is very specific to JPLs downstream tools. --- .../src/lib/codegen/CommandEDSLPreface.ts | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/sequencing-server/src/lib/codegen/CommandEDSLPreface.ts b/sequencing-server/src/lib/codegen/CommandEDSLPreface.ts index 1999902a2b..40b670bbd0 100644 --- a/sequencing-server/src/lib/codegen/CommandEDSLPreface.ts +++ b/sequencing-server/src/lib/codegen/CommandEDSLPreface.ts @@ -465,6 +465,10 @@ declare global { optionals?: { allowable_ranges?: VariableRange[]; allowable_values?: unknown[]; sc_name?: string }, ): ENUM; + function REF( + type: T, + ): T; + // @ts-ignore : 'GroundEpoch' and 'Step' found in JSON Spec function REQUEST(name: string, epoch: GroundEpoch, ...steps: [Step, ...Step[]]): RequestEpoch; @@ -966,6 +970,7 @@ export class Variable implements VariableDeclaration { [k: string]: unknown; private kind: 'locals' | 'parameters' | 'unknown' = 'locals'; + public reference: boolean = false; private readonly _enum_name?: string | undefined; // @ts-ignore : 'VariableRange: Request' found in JSON Spec private readonly _allowable_ranges?: VariableRange[] | undefined; @@ -1041,8 +1046,13 @@ export class Variable implements VariableDeclaration { return variable; } + public setAsVariableReference() { + this.reference = true; + } + public toReferenceString(): string { - return `${this.kind}.${this.name}`; + const _var = `${this.kind}.${this.name}`; + return this.reference ? `\nREF(${_var}) --> "VERIFY: '${_var}' is a Variable References"\n` : _var; } public toEDSLString(): string { @@ -1214,6 +1224,24 @@ export function ENUM( return { name, enum_name, type: VariableType.ENUM as unknown as VARIABLE_ENUM, allowable_ranges, allowable_values, sc_name }; } +export function REF( + value: T, +): T { + if ( + Variable.isVariable(value) && + (value.type === 'FLOAT' || + value.type === 'INT' || + value.type === 'STRING' || + value.type === 'UINT' || + value.type === 'ENUM') + ) { + const var_ref = new Variable({ name: value.name as unknown as string, type: value.type as T }); + var_ref.setAsVariableReference(); + return var_ref as unknown as T; + } + throw new Error('Invalid variable, make sure you use a defined local or parameter variable'); +} + /** * --------------------------------- * STEPS eDSL @@ -3593,6 +3621,24 @@ function convertInterfacesToArgs(interfaces: Args, localNames?: String[], parame return { hex_error: 'Remote property injection detected...' }; } else { if (validate(argName)) { + // This is JPL mission specific for Variable References + if (arg.type === 'string') { + let variable = Variable.new({ name: arg.value, type: VariableType.INT }); + if (localNames && localNames.length > 0) { + if (localNames.includes(arg.value)) { + variable.setKind('locals'); + variable.setAsVariableReference(); + return { [argName]: variable }; + } + } + if (parameterNames && parameterNames.length > 0) { + if (parameterNames.includes(arg.value)) { + variable.setKind('parameters'); + variable.setAsVariableReference(); + return { [argName]: variable }; + } + } + } return { [argName]: arg.value }; } return { error: 'Remote property injection detected...' }; @@ -3648,17 +3694,14 @@ function convertValueToObject(value: any, key: string): any { case 'boolean': return { type: 'boolean', value: value, name: key }; default: - if ( - value instanceof Object && - 'name' in value && - 'type' in value && + if (Variable.isVariable(value) && (value.type === 'FLOAT' || value.type === 'INT' || value.type === 'STRING' || value.type === 'UINT' || - value.type === 'ENUM') - ) { - return { type: 'symbol', value: value.name, name: key }; + value.type === 'ENUM')) { + // jpl specific support for Variable Reference + return { type: value.reference ? 'string' : 'symbol', value: value.name, name: key }; } else if ( value instanceof Object && value.hex && From a26fa9fc0427ff7e9f8a81ab1624344dff7e0774 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Mon, 18 Dec 2023 12:04:33 -1000 Subject: [PATCH 007/159] Expose the REF function to the user. --- sequencing-server/src/lib/codegen/CommandTypeCodegen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequencing-server/src/lib/codegen/CommandTypeCodegen.ts b/sequencing-server/src/lib/codegen/CommandTypeCodegen.ts index 907febd592..5bf826d6a0 100644 --- a/sequencing-server/src/lib/codegen/CommandTypeCodegen.ts +++ b/sequencing-server/src/lib/codegen/CommandTypeCodegen.ts @@ -57,7 +57,7 @@ export const Hardwares = {\n${dictionary.hwCommands .map(hwCommands => `\t\t${hwCommands.stem}: ${hwCommands.stem},\n`) .join('')}}; -Object.assign(globalThis, { A:A, R:R, E:E, C:Object.assign(Commands, STEPS, REQUESTS), Sequence, FLOAT, UINT,INT, STRING, ENUM, REQUEST}, Hardwares, Immediates); +Object.assign(globalThis, { A:A, R:R, E:E, C:Object.assign(Commands, STEPS, REQUESTS), Sequence, FLOAT, UINT,INT, STRING, ENUM, REQUEST, REF}, Hardwares, Immediates); `; return { From 47d7b08643e9ffbaf5e025c3cc6a33ae5cf64ef0 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Tue, 19 Dec 2023 10:25:57 -1000 Subject: [PATCH 008/159] update the e2e test --- .../lib/codegen/CommandEDSLPreface.spec.ts | 20 +++++ .../test/seqjson-to-edsl.spec.ts | 85 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/sequencing-server/src/lib/codegen/CommandEDSLPreface.spec.ts b/sequencing-server/src/lib/codegen/CommandEDSLPreface.spec.ts index 4ef75fdac5..a230aced5a 100644 --- a/sequencing-server/src/lib/codegen/CommandEDSLPreface.spec.ts +++ b/sequencing-server/src/lib/codegen/CommandEDSLPreface.spec.ts @@ -345,6 +345,9 @@ describe('Sequence', () => { local.setKind('locals') const parameter = Variable.new(ENUM('duration', 'POSSIBLE_DURATION')); parameter.setKind('parameters') + const reference = Variable.new(FLOAT('LO1FLOAT' )); + reference.setKind('parameters') + reference.setAsVariableReference() const sequence = Sequence.new({ seqId: 'test', metadata: {}, @@ -378,6 +381,17 @@ describe('Sequence', () => { }).METADATA({ author: 'ZZZZ', }), + CommandStem.new({ + stem: 'TEST', + arguments: { + temperature: reference, + duration: 10, + }, + + absoluteTime: doyToInstant('2022-001T00:00:00.000' as DOY_STRING), + }).METADATA({ + author: 'bbbb', + }), ], }); @@ -401,6 +415,12 @@ describe('Sequence', () => { .METADATA({ author: 'ZZZZ', }), + A\`2022-001T00:00:00.000\`.TEST( + REF(parameters.LO1FLOAT) --> "VERIFY: 'parameters.LO1FLOAT' is a Variable References" + ,10) + .METADATA({ + author: 'bbbb', + }), ]), });`); }); diff --git a/sequencing-server/test/seqjson-to-edsl.spec.ts b/sequencing-server/test/seqjson-to-edsl.spec.ts index 8e4a12c626..f96bc924b7 100644 --- a/sequencing-server/test/seqjson-to-edsl.spec.ts +++ b/sequencing-server/test/seqjson-to-edsl.spec.ts @@ -606,6 +606,91 @@ describe('getEdslForSeqJson', () => { ]), });`); }); + + it('should create variable reference in edsl', async () => { + const res = await graphqlClient.request<{ + getEdslForSeqJson: string; + }>( + gql` + query GetEdslForSeqJson($seqJson: SequenceSeqJson!) { + getEdslForSeqJson(seqJson: $seqJson) + } + `, + { + seqJson: { + id: '', + locals: [ + { + name: 'LOOFLOAT', + type: 'FLOAT', + }, + ], + metadata: {}, + parameters: [ + { + name: 'LOOINT', + type: 'INT', + }, + ], + steps: [ + { + args: [ + { + name: 'temperature', + type: 'string', + value: 'LOOINT', + }, + ], + stem: 'PREHEAT_OVEN', + time: { + type: 'COMMAND_COMPLETE', + }, + type: 'command', + }, + { + args: [ + { + name: 'tb_sugar', + type: 'string', + value: 'LOOFLOAT', + }, + { + name: 'gluten_free', + type: 'string', + value: 'FALSE', + }, + ], + stem: 'PREPARE_LOAF', + time: { + type: 'COMMAND_COMPLETE', + }, + type: 'command', + }, + ], + }, + }, + ); + + expect(res.getEdslForSeqJson).toEqual(`export default () => + Sequence.new({ + seqId: '', + metadata: {}, + locals: [ + FLOAT('LOOFLOAT') + ], + parameters: [ + INT('LOOINT') + ], + steps: ({ locals, parameters }) => ([ + C.PREHEAT_OVEN( + REF(parameters.LOOINT) --> "VERIFY: 'parameters.LOOINT' is a Variable References" + ), + C.PREPARE_LOAF( + REF(locals.LOOFLOAT) --> "VERIFY: 'locals.LOOFLOAT' is a Variable References" + ,'FALSE'), + ]), + });`); + }); }); describe('getEdslForSeqJsonBulk', () => { From cdfeecb4fe10444a588d59af101795110eb76a49 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Tue, 19 Dec 2023 10:26:08 -1000 Subject: [PATCH 009/159] Update the jest snapshot --- .../__snapshots__/command-types.spec.ts.snap | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/sequencing-server/test/__snapshots__/command-types.spec.ts.snap b/sequencing-server/test/__snapshots__/command-types.spec.ts.snap index cea9e21917..ab5448338b 100644 --- a/sequencing-server/test/__snapshots__/command-types.spec.ts.snap +++ b/sequencing-server/test/__snapshots__/command-types.spec.ts.snap @@ -468,6 +468,10 @@ declare global { optionals?: { allowable_ranges?: VariableRange[]; allowable_values?: unknown[]; sc_name?: string }, ): ENUM; + function REF( + type: T, + ): T; + // @ts-ignore : 'GroundEpoch' and 'Step' found in JSON Spec function REQUEST(name: string, epoch: GroundEpoch, ...steps: [Step, ...Step[]]): RequestEpoch; @@ -969,6 +973,7 @@ export class Variable implements VariableDeclaration { [k: string]: unknown; private kind: 'locals' | 'parameters' | 'unknown' = 'locals'; + public reference: boolean = false; private readonly _enum_name?: string | undefined; // @ts-ignore : 'VariableRange: Request' found in JSON Spec private readonly _allowable_ranges?: VariableRange[] | undefined; @@ -1044,8 +1049,13 @@ export class Variable implements VariableDeclaration { return variable; } + public setAsVariableReference() { + this.reference = true; + } + public toReferenceString(): string { - return \`\${this.kind}.\${this.name}\`; + const _var = \`\${this.kind}.\${this.name}\`; + return this.reference ? \`\\nREF(\${_var}) --> "VERIFY: '\${_var}' is a Variable References"\\n\` : _var; } public toEDSLString(): string { @@ -1217,6 +1227,24 @@ export function ENUM( return { name, enum_name, type: VariableType.ENUM as unknown as VARIABLE_ENUM, allowable_ranges, allowable_values, sc_name }; } +export function REF( + value: T, +): T { + if ( + Variable.isVariable(value) && + (value.type === 'FLOAT' || + value.type === 'INT' || + value.type === 'STRING' || + value.type === 'UINT' || + value.type === 'ENUM') + ) { + const var_ref = new Variable({ name: value.name as unknown as string, type: value.type as T }); + var_ref.setAsVariableReference(); + return var_ref as unknown as T; + } + throw new Error('Invalid variable, make sure you use a defined local or parameter variable'); +} + /** * --------------------------------- * STEPS eDSL @@ -3596,6 +3624,24 @@ function convertInterfacesToArgs(interfaces: Args, localNames?: String[], parame return { hex_error: 'Remote property injection detected...' }; } else { if (validate(argName)) { + // This is JPL mission specific for Variable References + if (arg.type === 'string') { + let variable = Variable.new({ name: arg.value, type: VariableType.INT }); + if (localNames && localNames.length > 0) { + if (localNames.includes(arg.value)) { + variable.setKind('locals'); + variable.setAsVariableReference(); + return { [argName]: variable }; + } + } + if (parameterNames && parameterNames.length > 0) { + if (parameterNames.includes(arg.value)) { + variable.setKind('parameters'); + variable.setAsVariableReference(); + return { [argName]: variable }; + } + } + } return { [argName]: arg.value }; } return { error: 'Remote property injection detected...' }; @@ -3651,17 +3697,14 @@ function convertValueToObject(value: any, key: string): any { case 'boolean': return { type: 'boolean', value: value, name: key }; default: - if ( - value instanceof Object && - 'name' in value && - 'type' in value && + if (Variable.isVariable(value) && (value.type === 'FLOAT' || value.type === 'INT' || value.type === 'STRING' || value.type === 'UINT' || - value.type === 'ENUM') - ) { - return { type: 'symbol', value: value.name, name: key }; + value.type === 'ENUM')) { + // jpl specific support for Variable Reference + return { type: value.reference ? 'string' : 'symbol', value: value.name, name: key }; } else if ( value instanceof Object && value.hex && @@ -4661,6 +4704,6 @@ export const Hardwares = { HDW_BLENDER_DUMP: HDW_BLENDER_DUMP, }; -Object.assign(globalThis, { A:A, R:R, E:E, C:Object.assign(Commands, STEPS, REQUESTS), Sequence, FLOAT, UINT,INT, STRING, ENUM, REQUEST}, Hardwares, Immediates); +Object.assign(globalThis, { A:A, R:R, E:E, C:Object.assign(Commands, STEPS, REQUESTS), Sequence, FLOAT, UINT,INT, STRING, ENUM, REQUEST, REF}, Hardwares, Immediates); " `; From 18f8827093dd64e05d0c9191da23ed6d5fa1dd8a Mon Sep 17 00:00:00 2001 From: Chet Joswig Date: Thu, 18 Jan 2024 15:21:37 -0800 Subject: [PATCH 010/159] run release workflows on dev-a.b.c --- .github/workflows/cloc.yml | 2 ++ .github/workflows/pgcmp.yml | 1 + .github/workflows/publish.yml | 12 +++++++----- .github/workflows/security-scan.yml | 2 ++ .github/workflows/test.yml | 2 ++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml index 323f908cf9..cb2e7e04e2 100644 --- a/.github/workflows/cloc.yml +++ b/.github/workflows/cloc.yml @@ -4,11 +4,13 @@ on: pull_request: branches: - develop + - dev-[0-9]+.[0-9]+.[0-9]+ push: branches: - develop tags: - v* + workflow_dispatch: jobs: cloc: diff --git a/.github/workflows/pgcmp.yml b/.github/workflows/pgcmp.yml index a19af69d42..731b8009b0 100644 --- a/.github/workflows/pgcmp.yml +++ b/.github/workflows/pgcmp.yml @@ -17,6 +17,7 @@ on: - "deployment/postgres-init-db/sql/**" branches: - develop + - dev-[0-9]+.[0-9]+.[0-9]+ tags: - v* workflow_dispatch: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b02d482c8b..1377a3e711 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,8 +4,10 @@ on: push: branches: - develop + - dev-[0-9]+.[0-9]+.[0-9]+ tags: - v* + workflow_dispatch: env: REGISTRY: ghcr.io @@ -137,19 +139,19 @@ jobs: with: image-ref: ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ matrix.image }}:develop ignore-unfixed: true - exit-code: '1' - severity: 'CRITICAL' - format: 'template' + exit-code: "1" + severity: "CRITICAL" + format: "template" template: "@/contrib/html.tpl" scanners: "vuln" - output: '${{ matrix.image }}-results.html' + output: "${{ matrix.image }}-results.html" - name: Upload ${{ matrix.image }} scan results if: always() uses: actions/upload-artifact@v3 with: name: Vuln Scan Results - path: '${{ matrix.image }}-results.html' + path: "${{ matrix.image }}-results.html" publish: name: gradle publish diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index ed2b03dcf5..f9f4f66441 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -4,11 +4,13 @@ on: pull_request: branches: - develop + - dev-[0-9]+.[0-9]+.[0-9]+ push: branches: - develop tags: - v* + workflow_dispatch: jobs: analyze: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44daa49b79..fc0b0e9efd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,11 +4,13 @@ on: pull_request: branches: - develop + - dev-[0-9]+.[0-9]+.[0-9]+ push: branches: - develop tags: - v* + workflow_dispatch: env: AERIE_USERNAME: "${{secrets.AERIE_USERNAME}}" From 012d9bd2f4f5579f4c13a5d409d73d74e3211a85 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 14:20:25 -0800 Subject: [PATCH 011/159] Add core resource interfaces Adds the core resource interfaces. A Resource is defined as a ThinResource (equivalent to a Supplier) returning an ErrorCatching and Expiring Dynamics. This mirrors the information stored in a cell, which has proven useful to carry with every value tracked by the model. --- .../contrib/streamline/core/Dynamics.java | 19 +++++ .../streamline/core/ErrorCatching.java | 46 ++++++++++++ .../contrib/streamline/core/Expiring.java | 22 ++++++ .../aerie/contrib/streamline/core/Expiry.java | 71 +++++++++++++++++++ .../contrib/streamline/core/Resource.java | 8 +++ .../contrib/streamline/core/ThinResource.java | 15 ++++ 6 files changed, 181 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ErrorCatching.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiring.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ThinResource.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java new file mode 100644 index 0000000000..c70509f47b --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java @@ -0,0 +1,19 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +/** + * A single segment of a resource profile; + * a value which evolves as time passes. + */ +public interface Dynamics> { + /** + * Get the current value. + */ + V extract(); + + /** + * Evolve for the given time. + */ + D step(Duration t); +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ErrorCatching.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ErrorCatching.java new file mode 100644 index 0000000000..bb18425e27 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ErrorCatching.java @@ -0,0 +1,46 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ErrorCatchingMonad; + +import java.util.function.Function; + +/** + * Sum type representing a value or a failure to produce a value. + */ +public sealed interface ErrorCatching { + R match(Function onSuccess, Function onError); + + static ErrorCatching success(T result) { + return new Success<>(result); + } + + static ErrorCatching failure(Throwable exception) { + return new Failure<>(exception); + } + + default ErrorCatching map(Function f) { + return ErrorCatchingMonad.map(this, f); + } + + default T getOrThrow() { + return match( + Function.identity(), + e -> { + throw new RuntimeException(e); + }); + } + + record Success(T result) implements ErrorCatching { + @Override + public R match(final Function onSuccess, final Function onError) { + return onSuccess.apply(result); + } + } + + record Failure(Throwable exception) implements ErrorCatching { + @Override + public R match(final Function onSuccess, final Function onError) { + return onError.apply(exception); + } + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiring.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiring.java new file mode 100644 index 0000000000..b048497ebe --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiring.java @@ -0,0 +1,22 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.NEVER; + +/** + * A value which may be valid for a limited time. + */ +public record Expiring(D data, Expiry expiry) { + public static Expiring expiring(D data, Expiry expiry) { + return new Expiring<>(data, expiry); + } + + public static Expiring neverExpiring(D data) { + return expiring(data, NEVER); + } + + public static Expiring expiring(D data, Duration expiry) { + return expiring(data, Expiry.at(expiry)); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java new file mode 100644 index 0000000000..f8242572a9 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java @@ -0,0 +1,71 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * The time at which a value expires. + */ +public record Expiry(Optional value) { + public static Expiry NEVER = expiry(Optional.empty()); + + public static Expiry at(Duration t) { + return expiry(Optional.of(t)); + } + + public static Expiry expiry(Optional value) { + return new Expiry(value); + } + + public Expiry or(Expiry other) { + return expiry( + Stream.concat(value().stream(), other.value().stream()).reduce(Duration::min)); + } + + public Expiry minus(Duration t) { + return expiry(value().map(v -> v.minus(t))); + } + + public boolean isNever() { + return value().isEmpty(); + } + + public int compareTo(Expiry other) { + if (this.isNever()) { + if (other.isNever()) { + return 0; + } else { + return 1; + } + } else { + if (other.isNever()) { + return -1; + } else { + return this.value().get().compareTo(other.value().get()); + } + } + } + + public boolean shorterThan(Expiry other) { + return this.compareTo(other) < 0; + } + + public boolean noShorterThan(Expiry other) { + return this.compareTo(other) >= 0; + } + + public boolean longerThan(Expiry other) { + return this.compareTo(other) > 0; + } + + public boolean noLongerThan(Expiry other) { + return this.compareTo(other) <= 0; + } + + @Override + public String toString() { + return value.map(Duration::toString).orElse("NEVER"); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java new file mode 100644 index 0000000000..2f32a88d07 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +/** + * A function returning a fully-wrapped dynamics, + * and the primary way models track state and report results. + */ +public interface Resource extends ThinResource>> { +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ThinResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ThinResource.java new file mode 100644 index 0000000000..48a963b01d --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/ThinResource.java @@ -0,0 +1,15 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +/** + * Alias for a Supplier. + * + *

+ * While structurally identical to {@link gov.nasa.jpl.aerie.merlin.framework.Resource}, + * the value returned by this interface is meant to be wrapped + * with additional information that should be stripped away + * before giving to {@link gov.nasa.jpl.aerie.merlin.framework.Registrar}. + *

+ */ +public interface ThinResource { + A getDynamics(); +} From d9a4012590f88c58f51808e067bb0d592da0c8c8 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 15:10:07 -0800 Subject: [PATCH 012/159] Add scripts to generate function interfaces and monad methods. To support methods like `map` accepting functions of varying arities, we need to define a function interface for each supported arity above 3. Similarly, we need to define `map` and `bind` methods for every supported arity for each monad. Since Java's type system doesn't support abstraction across type functors, we do this by generating the methods with a python script instead. --- .../streamline/utils/FunctionalUtils.java | 92 ++++++ .../utils/generate_functional_utils.py | 104 +++++++ .../utils/generate_monad_methods.py | 275 ++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/FunctionalUtils.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_functional_utils.py create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_monad_methods.py diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/FunctionalUtils.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/FunctionalUtils.java new file mode 100644 index 0000000000..aadeb97ccf --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/FunctionalUtils.java @@ -0,0 +1,92 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Utility functions for functional style programming. + * + * Generated by generate_functional_utils.py on 2023-11-28 + * to support functions of up to 20 arguments. + */ +public final class FunctionalUtils { + private FunctionalUtils() {} + + public static Function> curry(BiFunction function) { + return a -> b -> function.apply(a, b); + } + + public static Function>> curry(TriFunction function) { + return a -> b -> c -> function.apply(a, b, c); + } + + public static Function>>> curry(Function4 function) { + return a -> b -> c -> d -> function.apply(a, b, c, d); + } + + public static Function>>>> curry(Function5 function) { + return a -> b -> c -> d -> e -> function.apply(a, b, c, d, e); + } + + public static Function>>>>> curry(Function6 function) { + return a -> b -> c -> d -> e -> f -> function.apply(a, b, c, d, e, f); + } + + public static Function>>>>>> curry(Function7 function) { + return a -> b -> c -> d -> e -> f -> g -> function.apply(a, b, c, d, e, f, g); + } + + public static Function>>>>>>> curry(Function8 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> function.apply(a, b, c, d, e, f, g, h); + } + + public static Function>>>>>>>> curry(Function9 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> function.apply(a, b, c, d, e, f, g, h, i); + } + + public static Function>>>>>>>>> curry(Function10 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> function.apply(a, b, c, d, e, f, g, h, i, j); + } + + public static Function>>>>>>>>>> curry(Function11 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> function.apply(a, b, c, d, e, f, g, h, i, j, k); + } + + public static Function>>>>>>>>>>> curry(Function12 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l); + } + + public static Function>>>>>>>>>>>> curry(Function13 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m); + } + + public static Function>>>>>>>>>>>>> curry(Function14 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m, n); + } + + public static Function>>>>>>>>>>>>>> curry(Function15 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> o -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o); + } + + public static Function>>>>>>>>>>>>>>> curry(Function16 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> o -> p -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p); + } + + public static Function>>>>>>>>>>>>>>>> curry(Function17 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> o -> p -> q -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q); + } + + public static Function>>>>>>>>>>>>>>>>> curry(Function18 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> o -> p -> q -> r -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r); + } + + public static Function>>>>>>>>>>>>>>>>>> curry(Function19 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> o -> p -> q -> r -> s -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s); + } + + public static Function>>>>>>>>>>>>>>>>>>> curry(Function20 function) { + return a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> m -> n -> o -> p -> q -> r -> s -> t -> function.apply(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_functional_utils.py b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_functional_utils.py new file mode 100644 index 0000000000..97dbeabe61 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_functional_utils.py @@ -0,0 +1,104 @@ +import datetime +import string +import sys +import os + + +UTILS_PACKAGE = 'gov.nasa.jpl.aerie.contrib.streamline.utils' +UTILS_CLASS = 'FunctionalUtils' + + +def main(n): + utils_class_content = generate_functional_utils(n) + utils_class_file_name = os.path.join( + os.path.dirname(__file__), + UTILS_CLASS + '.java') + with open(utils_class_file_name, 'w') as f: + f.write(utils_class_content) + for i in range(4, n + 1): + function_i_interface_content = generate_function_n_interface(i) + function_i_interface_file_name = os.path.join( + os.path.dirname(__file__), + f'Function{i}.java') + with open(function_i_interface_file_name, 'w') as f: + f.write(function_i_interface_content) + + +def generate_function_n_interface(n): + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + arg_signature = ', '.join( + f'{c} {c.lower()}' + for c in string.ascii_uppercase[:n]) + return f'''package {UTILS_PACKAGE}; + +/** + * {n}-argument function. + * + * Generated by {os.path.basename(__file__)} on {datetime.date.today().isoformat()}. + */ +public interface Function{n}{type_args} {{ + Result apply({arg_signature}); +}}''' + + +def generate_functional_utils(n): + curry_functions = '\n\n'.join(generate_n_arg_curry(i) for i in range(2, n + 1)) + return f'''package {UTILS_PACKAGE}; + +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Utility functions for functional style programming. + * + * Generated by {os.path.basename(__file__)} on {datetime.date.today().isoformat()} + * to support functions of up to {n} arguments. + */ +public final class {UTILS_CLASS} {{ + private {UTILS_CLASS}() {{}} + +{indent(curry_functions, 2)} +}} +''' + + +def generate_n_arg_curry(n): + fn_name = ( + 'BiFunction' if n == 2 else + 'TriFunction' if n == 3 else + f'Function{n}') + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + fn_type_args = type_args + result_type = generate_n_arg_curried_function_type(n) + curried_fn_args = ' -> '.join(string.ascii_lowercase[:n]) + uncurried_fn_args = ', '.join(string.ascii_lowercase[:n]) + return f'''public static {type_args} {result_type} curry({fn_name}{fn_type_args} function) {{ + return {curried_fn_args} -> function.apply({uncurried_fn_args}); +}}''' + + +def generate_n_arg_curried_function_type(n): + result = 'Result' + for c in reversed(string.ascii_uppercase[:n]): + result = f'Function<{c}, {result}>' + return result + + +def indent(s, n): + return ' ' * n + s.replace('\n', '\n' + ' ' * n) + + +if __name__ == '__main__': + if '-h' in sys.argv or '--help' in sys.argv: + print(f''' +Usage: {sys.argv[0]} N + +Generates the FunctionalUtils.java file and supporting function interfaces. + +Args: + N Integer, the maximum arity to generate support for. +''') + exit(0) + main(int(sys.argv[1])) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_monad_methods.py b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_monad_methods.py new file mode 100644 index 0000000000..ebe9928d59 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/generate_monad_methods.py @@ -0,0 +1,275 @@ +import datetime +import os +import string +from collections import namedtuple +from functools import partial + +GENERATED_SECTION_START_COMMENT = '// GENERATED CODE START' +GENERATED_SECTION_END_COMMENT = '// GENERATED CODE END' + + +# Target file names are relative to this file's directory +Target = namedtuple('Target', ['monad_pattern', 'file_name', 'included_methods']) +applicative = partial(Target, included_methods={'apply', 'map'}) +monad = partial(Target, included_methods={'apply', 'map', 'bind'}) +TARGETS = [ + monad('Resource<{}>', '../core/monads/ResourceMonad.java'), + monad('ErrorCatching>', '../core/monads/DynamicsMonad.java'), + monad('Expiring<{}>', '../core/monads/ExpiringMonad.java'), + monad('ErrorCatching<{}>', '../core/monads/ErrorCatchingMonad.java'), + monad('ThinResource<{}>', '../core/monads/ThinResourceMonad.java'), + monad('Discrete<{}>', '../modeling/discrete/monads/DiscreteMonad.java'), + monad('Resource>', '../modeling/discrete/monads/DiscreteResourceMonad.java'), + monad('ErrorCatching>>', '../modeling/discrete/monads/DiscreteDynamicsMonad.java'), + monad('Unstructured<{}>', '../modeling/black_box/monads/UnstructuredMonad.java'), + applicative('Resource>', '../modeling/black_box/monads/UnstructuredResourceApplicative.java'), + applicative('ErrorCatching>>', '../modeling/black_box/monads/UnstructuredDynamicsApplicative.java'), +] + + +def main(n): + for target in TARGETS: + target_file = os.path.join(os.path.dirname(__file__), target.file_name) + print(f'Adding supplemental monad methods to {target_file}...') + supplemental_methods_content = generate_supplemental_methods(target, n) + insert_into_target_file(target_file, supplemental_methods_content) + print('All supplemental monad methods added.') + + +def insert_into_target_file(target_file, content): + with open(target_file) as f: + original_lines = list(f) + + modified_lines = list(original_lines) + stripped_lines = [l.strip() for l in original_lines] + try: + generated_section_start = stripped_lines.index(GENERATED_SECTION_START_COMMENT) + generated_section_end = stripped_lines.index(GENERATED_SECTION_END_COMMENT) + 1 + # Detect indentation that was used before: + indentation = len(original_lines[generated_section_start]) - len( + original_lines[generated_section_start].lstrip()) + except ValueError: + stripped_lines.reverse() + final_brace_line = len(stripped_lines) - stripped_lines.index('}') - 1 + stripped_lines.reverse() + # Detect indentation of the last non-blank line + i = final_brace_line - 1 + while stripped_lines[i] == '': + i -= 1 + indentation = len(original_lines[i]) - len(original_lines[i].lstrip()) + # Insert a blank line before the generated section, if none exists: + if i == final_brace_line - 1: + modified_lines.insert(final_brace_line, '\n') + final_brace_line += 1 + # Set the generated section to be inserted just before the final closing brace + generated_section_start = final_brace_line + generated_section_end = generated_section_start + + # generated_section_start and generated_section_end now indicate where in modified_lines to edit + modified_content = [' ' * indentation + l + '\n' for l in content.split('\n')] + modified_content.insert(0, ' ' * indentation + GENERATED_SECTION_START_COMMENT + '\n') + modified_content.append(' ' * indentation + GENERATED_SECTION_END_COMMENT + '\n') + modified_lines[generated_section_start:generated_section_end] = modified_content + + with open(target_file, 'w') as f: + f.writelines(modified_lines) + + +def generate_supplemental_methods(target, n): + result = f'// Supplemental methods generated by {os.path.basename(__file__)} on {datetime.date.today().isoformat()}.' + if 'apply' in target.included_methods: + result += '\n' + generate_functional_apply(target.monad_pattern) + if 'map' in target.included_methods: + result += '\n' + generate_1_arg_map(target.monad_pattern) + result += '\n' + generate_1_arg_functional_map(target.monad_pattern) + if 'bind' in target.included_methods: + result += '\n' + generate_1_arg_bind(target.monad_pattern) + result += '\n' + generate_1_arg_functional_bind(target.monad_pattern) + for i in range(2, n + 1): + if 'map' in target.included_methods: + result += '\n' + generate_n_arg_map(i, target.monad_pattern) + result += '\n' + generate_n_arg_curried_map(i, target.monad_pattern) + result += '\n' + generate_n_arg_functional_map(i, target.monad_pattern) + if 'bind' in target.included_methods: + result += '\n' + generate_n_arg_bind(i, target.monad_pattern) + result += '\n' + generate_n_arg_curried_bind(i, target.monad_pattern) + result += '\n' + generate_n_arg_functional_bind(i, target.monad_pattern) + return result + + +def generate_functional_apply(monad_pattern): + MA = monad_pattern.format('A') + MB = monad_pattern.format('B') + M_AB = monad_pattern.format('Function') + return f''' +public static Function<{MA}, {MB}> apply({M_AB} f) {{ + return a -> apply(a, f); +}}''' + + +def generate_1_arg_functional_map(monad_pattern): + MA = monad_pattern.format('A') + MB = monad_pattern.format('B') + return f''' +public static Function<{MA}, {MB}> map(Function f) {{ + return apply(pure(f)); +}}''' + + +def generate_1_arg_functional_bind(monad_pattern): + MA = monad_pattern.format('A') + MB = monad_pattern.format('B') + return f''' +public static Function<{MA}, {MB}> bind(Function f) {{ + return a -> bind(a, f); +}}''' + + +def generate_1_arg_map(monad_pattern): + MA = monad_pattern.format('A') + MB = monad_pattern.format('B') + return f''' +public static {MB} map({MA} a, Function f) {{ + return apply(a, pure(f)); +}}''' + + +def generate_1_arg_bind(monad_pattern): + MA = monad_pattern.format('A') + MB = monad_pattern.format('B') + return f''' +public static {MB} bind({MA} a, Function f) {{ + return join(map(a, f)); +}}''' + + +def generate_n_arg_map(n, monad_pattern): + arg_signature = ', '.join( + f'{monad_pattern.format(c)} {c.lower()}' + for c in string.ascii_uppercase[:n]) + fn_name = ( + 'BiFunction' if n == 2 else + 'TriFunction' if n == 3 else + f'Function{n}') + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + fn_type = fn_name + type_args + args = ', '.join(string.ascii_lowercase[:n]) + return f''' +public static {type_args} {monad_pattern.format('Result')} map({arg_signature}, {fn_type} function) {{ + return map({args}, curry(function)); +}}''' + + +def generate_n_arg_functional_map(n, monad_pattern): + fn_name = ( + 'BiFunction' if n == 2 else + 'TriFunction' if n == 3 else + f'Function{n}') + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + fn_type = fn_name + type_args + result_fn_type = f'{fn_name}<{", ".join(monad_pattern.format(c) for c in string.ascii_uppercase[:n])}, {monad_pattern.format("Result")}>' + args = ', '.join(string.ascii_lowercase[:n]) + return f''' +public static {type_args} {result_fn_type} map({fn_type} function) {{ + return ({args}) -> map({args}, function); +}}''' + + +def generate_n_arg_curried_map(n, monad_pattern): + arg_signature = ', '.join( + f'{monad_pattern.format(c)} {c.lower()}' + for c in string.ascii_uppercase[:n]) + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + fn_type = generate_n_arg_curried_function_type(n, 'Result') + last_arg = string.ascii_lowercase[n - 1] + non_last_args = ', '.join(string.ascii_lowercase[:n - 1]) + return f''' +public static {type_args} {monad_pattern.format('Result')} map({arg_signature}, {fn_type} function) {{ + return apply({last_arg}, map({non_last_args}, function)); +}}''' + + +def generate_n_arg_bind(n, monad_pattern): + arg_signature = ', '.join( + f'{monad_pattern.format(c)} {c.lower()}' + for c in string.ascii_uppercase[:n]) + fn_name = ( + 'BiFunction' if n == 2 else + 'TriFunction' if n == 3 else + f'Function{n}') + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + monadic_result = monad_pattern.format('Result') + fn_type = f'{fn_name}<{", ".join(string.ascii_uppercase[:n])}, {monadic_result}>' + args = ', '.join(string.ascii_lowercase[:n]) + return f''' +public static {type_args} {monadic_result} bind({arg_signature}, {fn_type} function) {{ + return join(map({args}, function)); +}}''' + + +def generate_n_arg_functional_bind(n, monad_pattern): + fn_name = ( + 'BiFunction' if n == 2 else + 'TriFunction' if n == 3 else + f'Function{n}') + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + monadic_result = monad_pattern.format('Result') + fn_type = f'{fn_name}<{", ".join(string.ascii_uppercase[:n])}, {monadic_result}>' + result_fn_type = f'{fn_name}<{", ".join(monad_pattern.format(c) for c in string.ascii_uppercase[:n])}, {monadic_result}>' + args = ', '.join(string.ascii_lowercase[:n]) + return f''' +public static {type_args} {result_fn_type} bind({fn_type} function) {{ + return ({args}) -> bind({args}, function); +}}''' + + +def generate_n_arg_curried_bind(n, monad_pattern): + arg_signature = ', '.join( + f'{monad_pattern.format(c)} {c.lower()}' + for c in string.ascii_uppercase[:n]) + type_args = f'<{", ".join(string.ascii_uppercase[:n])}, Result>' + monadic_result = monad_pattern.format('Result') + fn_type = generate_n_arg_curried_function_type(n, monadic_result) + args = ', '.join(string.ascii_lowercase[:n]) + return f''' +public static {type_args} {monad_pattern.format('Result')} bind({arg_signature}, {fn_type} function) {{ + return join(map({args}, function)); +}}''' + + +def generate_n_arg_curried_function_type(n, result): + for c in reversed(string.ascii_uppercase[:n]): + result = f'Function<{c}, {result}>' + return result + + +if __name__ == '__main__': + import sys + + if '-h' in sys.argv or '--help' in sys.argv: + targets_str = ''.join('\n ' + t.file_name for t in TARGETS) + print(f''' +Usage: {sys.argv[0]} N + +Generates supplementary monad methods. + +Assuming a monad M has defined the following methods: + pure :: A -> M A + apply :: M A, M(A -> B) -> M B + join :: M (M A) -> M A +This script will generate the following methods: + apply :: M(A -> B) -> (M A -> M B) + map :: M A, (A -> B) -> M B + map :: (A -> B) -> (M A -> M B) + bind :: M A, (A -> M B) -> M B + bind :: (A -> M B) -> (M A -> M B) + k-argument forms of map and bind, for k up to N, taking regular or curried mapping functions + +These methods will be added to the following classes: +{targets_str} + +Args: + N Integer, the maximum arity to generate support for. +''') + exit(0) + main(int(sys.argv[1])) From dc617932a2cc3fcbb4d390c27507700c787c7ca2 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 15:13:27 -0800 Subject: [PATCH 013/159] Generate function interfaces --- .../jpl/aerie/contrib/streamline/utils/Function10.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function11.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function12.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function13.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function14.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function15.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function16.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function17.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function18.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function19.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function20.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function4.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function5.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function6.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function7.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function8.java | 10 ++++++++++ .../jpl/aerie/contrib/streamline/utils/Function9.java | 10 ++++++++++ 17 files changed, 170 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function10.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function11.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function12.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function13.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function14.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function15.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function16.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function17.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function18.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function19.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function20.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function4.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function5.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function6.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function7.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function8.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function9.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function10.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function10.java new file mode 100644 index 0000000000..71d19fa2a5 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function10.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 10-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function10 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function11.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function11.java new file mode 100644 index 0000000000..ab421a7c34 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function11.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 11-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function11 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function12.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function12.java new file mode 100644 index 0000000000..5efc3d122d --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function12.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 12-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function12 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function13.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function13.java new file mode 100644 index 0000000000..165e1742a1 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function13.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 13-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function13 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function14.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function14.java new file mode 100644 index 0000000000..710c9957c0 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function14.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 14-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function14 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function15.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function15.java new file mode 100644 index 0000000000..5ba91c11e1 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function15.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 15-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function15 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function16.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function16.java new file mode 100644 index 0000000000..da0a424ca5 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function16.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 16-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function16 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o, P p); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function17.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function17.java new file mode 100644 index 0000000000..4824ee9c6a --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function17.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 17-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function17 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o, P p, Q q); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function18.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function18.java new file mode 100644 index 0000000000..2b8b8b2572 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function18.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 18-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function18 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o, P p, Q q, R r); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function19.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function19.java new file mode 100644 index 0000000000..00150187ea --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function19.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 19-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function19 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o, P p, Q q, R r, S s); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function20.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function20.java new file mode 100644 index 0000000000..ed85cf2898 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function20.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 20-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function20 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o, P p, Q q, R r, S s, T t); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function4.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function4.java new file mode 100644 index 0000000000..4863b8ab22 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function4.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 4-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function4 { + Result apply(A a, B b, C c, D d); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function5.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function5.java new file mode 100644 index 0000000000..6c1d6b0bc5 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function5.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 5-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function5 { + Result apply(A a, B b, C c, D d, E e); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function6.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function6.java new file mode 100644 index 0000000000..5c56add9a4 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function6.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 6-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function6 { + Result apply(A a, B b, C c, D d, E e, F f); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function7.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function7.java new file mode 100644 index 0000000000..2915c07bf5 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function7.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 7-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function7 { + Result apply(A a, B b, C c, D d, E e, F f, G g); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function8.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function8.java new file mode 100644 index 0000000000..2c7544000e --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function8.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 8-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function8 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h); +} \ No newline at end of file diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function9.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function9.java new file mode 100644 index 0000000000..1c01b0c930 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/Function9.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +/** + * 9-argument function. + * + * Generated by generate_functional_utils.py on 2023-11-28. + */ +public interface Function9 { + Result apply(A a, B b, C c, D d, E e, F f, G g, H h, I i); +} \ No newline at end of file From 3386e6f0390491916d8e4bed66a4bd100614a79e Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 15:16:42 -0800 Subject: [PATCH 014/159] Add monads for core interfaces Adds monads for all core interfaces: * ExpiringMonad * ErrorCatchingMonad * ThinResourceMonad These monads abstract the handling of expiries, errors, and stitching together multiple dynamics segments into a resource. Also adds two monads that compose the above: * DynamicsMonad = ExpiringMonad + ErrorCatchingMonad * ResourceMonad = ThinResourceMonad + DynamicsMonad Monad users can generally write code that operates on base dynamics objects, and use the monad methods to lift that code to fully-wrapped dynamics or resources. This makes sure the wrapping layers are handled correctly and consistently, and keeps downstream code more focused. --- .../streamline/core/monads/DynamicsMonad.java | 530 +++++++++++++++++ .../core/monads/ErrorCatchingMonad.java | 515 +++++++++++++++++ .../streamline/core/monads/ExpiringMonad.java | 511 +++++++++++++++++ .../contrib/streamline/core/monads/README.md | 75 +++ .../streamline/core/monads/ResourceMonad.java | 537 ++++++++++++++++++ .../core/monads/ThinResourceMonad.java | 506 +++++++++++++++++ 6 files changed, 2674 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/DynamicsMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ErrorCatchingMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ExpiringMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/README.md create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ThinResourceMonad.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/DynamicsMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/DynamicsMonad.java new file mode 100644 index 0000000000..d99706fae6 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/DynamicsMonad.java @@ -0,0 +1,530 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.DynamicsEffect; +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +/** + * Monad A -> ErrorCatching<Expiring<A>>. + * This monad fully wraps a basic {@link Dynamics} object, + * and is mostly used within the library. + * @see ResourceMonad + */ +public final class DynamicsMonad { + private DynamicsMonad() {} + + public static ErrorCatching> pure(A a) { + return ErrorCatchingMonad.pure(ExpiringMonad.pure(a)); + } + + public static ErrorCatching> apply(ErrorCatching> a, ErrorCatching>> f) { + return ErrorCatchingMonad.apply(a, ErrorCatchingMonad.map(f, ExpiringMonad::apply)); + } + + private static ErrorCatching> distribute(Expiring> a) { + return a.data().map(a$ -> expiring(a$, a.expiry())); + } + + public static ErrorCatching> join(ErrorCatching>>> a) { + return ErrorCatchingMonad.map(ErrorCatchingMonad.join(ErrorCatchingMonad.map(a, DynamicsMonad::distribute)), ExpiringMonad::join); + } + + // Not fully monadic since we intentionally ignore expiry information, but useful nonetheless. + + public static > DynamicsEffect effect(Function f) { + return bindEffect(f.andThen(DynamicsMonad::pure)); + } + + public static > DynamicsEffect bindEffect(Function>> f) { + return ea -> ErrorCatchingMonad.bind(ea, a -> f.apply(a.data())); + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function>, ErrorCatching>> apply(ErrorCatching>> f) { + return a -> apply(a, f); + } + + public static ErrorCatching> map(ErrorCatching> a, Function f) { + return apply(a, pure(f)); + } + + public static Function>, ErrorCatching>> map(Function f) { + return apply(pure(f)); + } + + public static ErrorCatching> bind(ErrorCatching> a, Function>> f) { + return join(map(a, f)); + } + + public static Function>, ErrorCatching>> bind(Function>> f) { + return a -> bind(a, f); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction>, ErrorCatching>, ErrorCatching>> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, BiFunction>> function) { + return join(map(a, b, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, Function>>> function) { + return join(map(a, b, function)); + } + + public static BiFunction>, ErrorCatching>, ErrorCatching>> bind(BiFunction>> function) { + return (a, b) -> bind(a, b, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, TriFunction>> function) { + return join(map(a, b, c, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, Function>>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(TriFunction>> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, Function4>> function) { + return join(map(a, b, c, d, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, Function>>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function4>> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, Function5>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, Function>>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function5>> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, Function6>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function6>> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, Function7>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function7>> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, Function8>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function8>> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, Function9>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function9>> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, Function10>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function10>> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, Function11>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function11>> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, Function12>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function12>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, Function13>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function13>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, Function14>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function14>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, Function15>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function15>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, Function16>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function16>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, Function17>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function17>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, Function18>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function18>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, Function19>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function19>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, ErrorCatching> t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static ErrorCatching> map(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, ErrorCatching> t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, ErrorCatching> t, Function20>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static ErrorCatching> bind(ErrorCatching> a, ErrorCatching> b, ErrorCatching> c, ErrorCatching> d, ErrorCatching> e, ErrorCatching> f, ErrorCatching> g, ErrorCatching> h, ErrorCatching> i, ErrorCatching> j, ErrorCatching> k, ErrorCatching> l, ErrorCatching> m, ErrorCatching> n, ErrorCatching> o, ErrorCatching> p, ErrorCatching> q, ErrorCatching> r, ErrorCatching> s, ErrorCatching> t, Function>>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>, ErrorCatching>> bind(Function20>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ErrorCatchingMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ErrorCatchingMonad.java new file mode 100644 index 0000000000..a30499c973 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ErrorCatchingMonad.java @@ -0,0 +1,515 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching.failure; +import static gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching.success; +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; +import static java.util.function.Function.identity; + +public final class ErrorCatchingMonad { + private ErrorCatchingMonad() {} + + public static ErrorCatching pure(A a) { + return success(a); + } + + public static ErrorCatching apply(ErrorCatching a, ErrorCatching> f) { + return f.match(f$ -> a.match(a$ -> { + try { + return success(f$.apply(a$)); + } catch (Throwable e) { + return failure(e); + } + }, ErrorCatching::failure), ErrorCatching::failure); + } + + public static ErrorCatching join(ErrorCatching> a) { + return a.match(identity(), ErrorCatching::failure); + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function, ErrorCatching> apply(ErrorCatching> f) { + return a -> apply(a, f); + } + + public static ErrorCatching map(ErrorCatching a, Function f) { + return apply(a, pure(f)); + } + + public static Function, ErrorCatching> map(Function f) { + return apply(pure(f)); + } + + public static ErrorCatching bind(ErrorCatching a, Function> f) { + return join(map(a, f)); + } + + public static Function, ErrorCatching> bind(Function> f) { + return a -> bind(a, f); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction, ErrorCatching, ErrorCatching> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, BiFunction> function) { + return join(map(a, b, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, Function>> function) { + return join(map(a, b, function)); + } + + public static BiFunction, ErrorCatching, ErrorCatching> bind(BiFunction> function) { + return (a, b) -> bind(a, b, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction, ErrorCatching, ErrorCatching, ErrorCatching> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, TriFunction> function) { + return join(map(a, b, c, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, Function>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction, ErrorCatching, ErrorCatching, ErrorCatching> bind(TriFunction> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, Function4> function) { + return join(map(a, b, c, d, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, Function>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function4> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, Function5> function) { + return join(map(a, b, c, d, e, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, Function>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function5> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, Function6> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, Function>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function6> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, Function7> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function7> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, Function8> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function8> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, Function9> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function9> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, Function10> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function10> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, Function11> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function11> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, Function12> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function12> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, Function13> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function13> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, Function14> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function14> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, Function15> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function15> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, Function16> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching> bind(Function16> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, Function17> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching> bind(Function17> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching, ErrorCatching> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, Function18> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function18> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, Function19> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function19> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, ErrorCatching t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, ErrorCatching t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, ErrorCatching t, Function20> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static ErrorCatching bind(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching

p, ErrorCatching q, ErrorCatching r, ErrorCatching s, ErrorCatching t, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching

, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching, ErrorCatching> bind(Function20> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ExpiringMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ExpiringMonad.java new file mode 100644 index 0000000000..9c618cf26c --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ExpiringMonad.java @@ -0,0 +1,511 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.neverExpiring; +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +/** + * The {@link Expiring} monad, which demands derived values expire no later than their sources. + */ +public final class ExpiringMonad { + private ExpiringMonad() {} + + public static Expiring pure(A a) { + return neverExpiring(a); + } + + public static Expiring apply(Expiring a, Expiring> f) { + return expiring(f.data().apply(a.data()), a.expiry().or(f.expiry())); + } + + public static Expiring join(Expiring> a) { + return expiring(a.data().data(), a.expiry().or(a.data().expiry())); + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function, Expiring> apply(Expiring> f) { + return a -> apply(a, f); + } + + public static Expiring map(Expiring a, Function f) { + return apply(a, pure(f)); + } + + public static Function, Expiring> map(Function f) { + return apply(pure(f)); + } + + public static Expiring bind(Expiring a, Function> f) { + return join(map(a, f)); + } + + public static Function, Expiring> bind(Function> f) { + return a -> bind(a, f); + } + + public static Expiring map(Expiring a, Expiring b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction, Expiring, Expiring> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static Expiring bind(Expiring a, Expiring b, BiFunction> function) { + return join(map(a, b, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Function>> function) { + return join(map(a, b, function)); + } + + public static BiFunction, Expiring, Expiring> bind(BiFunction> function) { + return (a, b) -> bind(a, b, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction, Expiring, Expiring, Expiring> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, TriFunction> function) { + return join(map(a, b, c, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Function>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction, Expiring, Expiring, Expiring> bind(TriFunction> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4, Expiring, Expiring, Expiring, Expiring> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Function4> function) { + return join(map(a, b, c, d, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Function>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4, Expiring, Expiring, Expiring, Expiring> bind(Function4> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Function5> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Function>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function5> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Function6> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Function>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function6> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Function7> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function7> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Function8> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function8> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Function9> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function9> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Function10> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function10> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Function11> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function11> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Function12> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function12> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Function13> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function13> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Function14> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function14> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Function15> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function15> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Function16> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring> bind(Function16> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Function17> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring> bind(Function17> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring, Expiring> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Function18> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring, Expiring> bind(Function18> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring, Expiring, Expiring> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Function19> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring, Expiring, Expiring> bind(Function19> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Expiring t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static Expiring map(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Expiring t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring, Expiring, Expiring, Expiring> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Expiring t, Function20> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Expiring bind(Expiring a, Expiring b, Expiring c, Expiring d, Expiring e, Expiring f, Expiring g, Expiring h, Expiring i, Expiring j, Expiring k, Expiring l, Expiring m, Expiring n, Expiring o, Expiring

p, Expiring q, Expiring r, Expiring s, Expiring t, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring, Expiring

, Expiring, Expiring, Expiring, Expiring, Expiring> bind(Function20> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/README.md b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/README.md new file mode 100644 index 0000000000..8e995fd5c2 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/README.md @@ -0,0 +1,75 @@ +# Monads - Quick Explanation + +We'll briefly motivate and describe monads here. +For a better explanation of monads, see https://bartoszmilewski.com/2016/11/21/monads-programmers-definition/, +on which this is based. + +## Motivation - Wrapper Types + +We often want to "augment" a value in some way. +For example, we augment a type `A` into an `Optional` to add the idea that a computation can fail. +We can think of `Optional` as a function, from the type `A` to the type `Optional`. +Any type with a type parameter is a function like this, called a "type functor". + +We'd like these wrappers to be transparent, in the sense that we write code for `A` and run it on `Optional`. +Equivalently, we want an operator that takes a function `A -> B` +and returns a function `Optional -> Optional`. +More generally, for any type functor `M`, we want an operator from `A -> B` to `M A -> M B`. +This operator is called `map` in general, and is given by `Optional.map`. + +We'd also like to extend this to functions with multiple arguments. +We could define `map` overloads taking multiple arguments directly, but there's a more elegant solution. +First, we "curry" the function: Instead of a multi-argument function like this: `A x B -> C`, +we use a function returning a function, like this: `A -> (B -> C)`. +This lets us "bake in" each argument, one at a time. +Now, imagine we `map` this function, and apply it to an `M A`: We'd get a result of type `M (B -> C)`. +Now, we want an operator that can apply this wrapped function to a wrapped value of type `M B`. + +Indeed, such an operation is called `apply`. +It takes a wrapped function, and turns it into a function on wrapped values: `M (B -> C) -> (M B -> M C)`. +There's no direct equivalent of `apply` for `Optional`. +This looks similar to `map`, though. If we had a way to wrap the function we gave to `map`, we could use `apply` instead. + +The operator which wraps a value is called `pure` (or sometimes `unit`, but we use that word for other purposes.) +It has the signature `A -> M A`, and should be thought of as adding an "emtpy" or "default" wrapper. +For `Optional`, this is `Optional.of`. +Type functors with both `pure` and `apply` are called "applicative functors", or just "applicatives". + +Sometimes this isn't enough. Sometimes we need to add wrapper information as we compute. +For example, consider an implementation of `sqrt : Double -> Optional`, that returns an empty Optional +if the input is negative. +If we `map` a function with signature `A -> M B`, we get a function `M A -> M (M B)`. +Now, we need a way to collapse this double-wrapped result into an `M B`. +This operation is called `join`. +Applicative functors that also have `join` are called "monads". +The `map`-and-then-`join` pattern is so common, it has it's own name: `bind`. +For `Optional`, `bind` is implemented by `Optional.flatMap`. + +There are formal rules for how these operations need to interact, which you can find elsewhere. +As a rule of thumb, if one only uses these operations, and the types of the results agree, +the values of the results generally agree as well. + +If you want to write your own monad, then, simply define `pure`, `apply`, and `join`. +Then, use the monad method script to generate all additional methods and overloads. + +## Composition + +There's no general way to combine two arbitrary monads `M` and `N` to get a monad, +but we can break the problem down. +First, there is a way to combine `M` and `N` as applicatives, which is done in the code. +The `pure` operations compose directly, while the composed `apply` can be derived by "type tetris" - +choosing operations above to make the type signatures "fit". + +To define a composed `join`, we need to define a function `M (N (M (N A))) -> M (N A)`. +If we could "swap" the two middle layers, we'd have an `M (M (N (N A)))`, +to which we could use `map` and `join` to get the desired result. +This "swapping" is called `distribute`, taking inspiration from arithmetic: +A product over a sum "distributes" to a sum of products. +`distribute` cannot be defined for two arbitrary monads, +but it's often not too hard to write `distribute` for two particular monads. +For example, `Expiring` and `ErrorCatching` have a straightforward `distribute` operation. + +If you want to compose two monads, copy the definition of `pure`, `apply`, and `join` from an existing composed monad, +like `DynamicsMonad`, and replace the monads you're composing. +Then, write `distribute` for the two monads you're composing to complete the definition of `join`. +Finally, use the monad method script to generate all additional methods and overloads. diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java new file mode 100644 index 0000000000..04853d3c12 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java @@ -0,0 +1,537 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.ThinResource; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +/** + * Monad A -> Resource<A>. + * This is the primary monad for model authors, + * handling both expiry and "stitching together" of derived resources. + */ +public final class ResourceMonad { + private ResourceMonad() {} + + public static Resource pure(A a) { + return ThinResourceMonad.pure(DynamicsMonad.pure(a))::getDynamics; + } + + public static Resource apply(Resource a, Resource> f) { + Resource result = ThinResourceMonad.apply(a, ThinResourceMonad.map(f, DynamicsMonad::apply))::getDynamics; + addDependency(result, a); + addDependency(result, f); + return result; + } + + private static ThinResource>> distribute(ErrorCatching>> a) { + return () -> DynamicsMonad.map(a, ThinResource::getDynamics); + } + + public static Resource join(Resource> a) { + // Perform a trivial down-conversion to the base ThinResource types, to expose the Dynamics wrappers in the type signature. + ThinResource>>>>> a$ = map(a, $ -> $); + // Then use distributivity and basic joins to collapse the type. + // The ::getDynamics at the end up-converts back to Resource, from ThinResource + Resource result = ThinResourceMonad.map(ThinResourceMonad.join(ThinResourceMonad.map(a$, ResourceMonad::distribute)), DynamicsMonad::join)::getDynamics; + addDependency(result, a); + return result; + } + + // Not strictly part of this monad, but commonly used to "fill the gap" when deriving resources with partial bindings + public static Resource pure(Expiring a) { + return ThinResourceMonad.pure(ErrorCatchingMonad.pure(a))::getDynamics; + } + + public static Resource pure(ErrorCatching> a) { + return ThinResourceMonad.pure(a)::getDynamics; + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function, Resource> apply(Resource> f) { + return a -> apply(a, f); + } + + public static Resource map(Resource a, Function f) { + return apply(a, pure(f)); + } + + public static Function, Resource> map(Function f) { + return apply(pure(f)); + } + + public static Resource bind(Resource a, Function> f) { + return join(map(a, f)); + } + + public static Function, Resource> bind(Function> f) { + return a -> bind(a, f); + } + + public static Resource map(Resource a, Resource b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static Resource map(Resource a, Resource b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction, Resource, Resource> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static Resource bind(Resource a, Resource b, BiFunction> function) { + return join(map(a, b, function)); + } + + public static Resource bind(Resource a, Resource b, Function>> function) { + return join(map(a, b, function)); + } + + public static BiFunction, Resource, Resource> bind(BiFunction> function) { + return (a, b) -> bind(a, b, function); + } + + public static Resource map(Resource a, Resource b, Resource c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction, Resource, Resource, Resource> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, TriFunction> function) { + return join(map(a, b, c, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Function>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction, Resource, Resource, Resource> bind(TriFunction> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4, Resource, Resource, Resource, Resource> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Function4> function) { + return join(map(a, b, c, d, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Function>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4, Resource, Resource, Resource, Resource> bind(Function4> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5, Resource, Resource, Resource, Resource, Resource> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Function5> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Function>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5, Resource, Resource, Resource, Resource, Resource> bind(Function5> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6, Resource, Resource, Resource, Resource, Resource, Resource> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Function6> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Function>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function6> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Function7> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function7> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Function8> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function8> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Function9> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function9> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Function10> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function10> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Function11> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function11> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Function12> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function12> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Function13> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function13> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Function14> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function14> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Function15> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource> bind(Function15> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Function16> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource> bind(Function16> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Function17> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource> bind(Function17> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource, Resource> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Function18> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource, Resource> bind(Function18> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource, Resource, Resource> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Function19> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource, Resource, Resource> bind(Function19> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Resource t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static Resource map(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Resource t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource, Resource, Resource, Resource> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Resource t, Function20> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Resource bind(Resource a, Resource b, Resource c, Resource d, Resource e, Resource f, Resource g, Resource h, Resource i, Resource j, Resource k, Resource l, Resource m, Resource n, Resource o, Resource

p, Resource q, Resource r, Resource s, Resource t, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource, Resource

, Resource, Resource, Resource, Resource, Resource> bind(Function20> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ThinResourceMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ThinResourceMonad.java new file mode 100644 index 0000000000..1cd983cdff --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ThinResourceMonad.java @@ -0,0 +1,506 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.ThinResource; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +public final class ThinResourceMonad { + private ThinResourceMonad() {} + + public static ThinResource pure(A a) { + return () -> a; + } + + public static ThinResource apply(ThinResource a, ThinResource> f) { + return () -> f.getDynamics().apply(a.getDynamics()); + } + + public static ThinResource join(ThinResource> a) { + return () -> a.getDynamics().getDynamics(); + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function, ThinResource> apply(ThinResource> f) { + return a -> apply(a, f); + } + + public static ThinResource map(ThinResource a, Function f) { + return apply(a, pure(f)); + } + + public static Function, ThinResource> map(Function f) { + return apply(pure(f)); + } + + public static ThinResource bind(ThinResource a, Function> f) { + return join(map(a, f)); + } + + public static Function, ThinResource> bind(Function> f) { + return a -> bind(a, f); + } + + public static ThinResource map(ThinResource a, ThinResource b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction, ThinResource, ThinResource> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, BiFunction> function) { + return join(map(a, b, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, Function>> function) { + return join(map(a, b, function)); + } + + public static BiFunction, ThinResource, ThinResource> bind(BiFunction> function) { + return (a, b) -> bind(a, b, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction, ThinResource, ThinResource, ThinResource> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, TriFunction> function) { + return join(map(a, b, c, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, Function>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction, ThinResource, ThinResource, ThinResource> bind(TriFunction> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4, ThinResource, ThinResource, ThinResource, ThinResource> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, Function4> function) { + return join(map(a, b, c, d, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, Function>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function4> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, Function5> function) { + return join(map(a, b, c, d, e, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, Function>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function5> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, Function6> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, Function>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function6> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, Function7> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function7> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, Function8> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function8> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, Function9> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function9> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, Function10> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function10> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, Function11> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function11> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, Function12> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function12> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, Function13> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function13> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, Function14> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function14> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, Function15> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function15> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, Function16> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource> bind(Function16> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, Function17> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource> bind(Function17> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource, ThinResource> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, Function18> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource, ThinResource> bind(Function18> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource, ThinResource, ThinResource> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, Function19> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function19> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, ThinResource t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static ThinResource map(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, ThinResource t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, ThinResource t, Function20> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static ThinResource bind(ThinResource a, ThinResource b, ThinResource c, ThinResource d, ThinResource e, ThinResource f, ThinResource g, ThinResource h, ThinResource i, ThinResource j, ThinResource k, ThinResource l, ThinResource m, ThinResource n, ThinResource o, ThinResource

p, ThinResource q, ThinResource r, ThinResource s, ThinResource t, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource

, ThinResource, ThinResource, ThinResource, ThinResource, ThinResource> bind(Function20> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} From 97edd7ae305eab7aee4d65fc0d72be527dcb312e Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 16:18:00 -0800 Subject: [PATCH 015/159] Add MutableResource Adds MutableResource and supporting types for defining cells and effects. This design emphasizes separation of concerns in two primary ways: * First, since every resource dynamics carries an expiry and stepping behavior, we don't need to define a different cell type depending on how that value is computed (like an Accumulator) nor by what kind of dynamics are stored (e.g. Discrete vs. Real). * Second, since the DynamicsEffect interface defines a fully general effect type, we don't need to define a different cell type depending on the supported class of effects. Taken together, we can define a single cell type. This design also seeks to reduce overhead for modelers to handle effects the "right" way. By this, we mean using semantically correct effects, rather than (ab)using Registers for everything. * Instead of defining a new type for effects, we use a general DynamicsEffect interface. We also have the DynamicsMonad.effect method, so effects can be written against the base dynamics type, often as a small in-line lambda. * To support these "black-box" effects, we use an "automatic" effect trait by default, which tests concurrent effects for commutativity. Since effects are rarely concurrent in practice, this is performant enough in most use cases. Furthermore, it combines with the error-handling wrapper to bubble-up useful error messages, as well as let independent portions of the simulation continue normally. Taken together, the above means there's a single "default" way to build a cell, which provides enough flexibility and performance for most use cases. --- .../contrib/streamline/core/CellRefV2.java | 164 ++++++++++++++++++ .../streamline/core/DynamicsEffect.java | 8 + .../streamline/core/MutableResource.java | 105 +++++++++++ 3 files changed, 277 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/DynamicsEffect.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java new file mode 100644 index 0000000000..064f575411 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java @@ -0,0 +1,164 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ErrorCatchingMonad; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming; +import gov.nasa.jpl.aerie.merlin.framework.CellRef; +import gov.nasa.jpl.aerie.merlin.protocol.model.CellType; +import gov.nasa.jpl.aerie.merlin.protocol.model.EffectTrait; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.function.BinaryOperator; +import java.util.function.Predicate; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching.failure; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + +public final class CellRefV2 { + private CellRefV2() {} + + /** + * Allocate a new resource with an explicitly given effect type and effect trait. + */ + public static , E extends DynamicsEffect> CellRef> allocate(ErrorCatching> initialDynamics, EffectTrait effectTrait) { + return CellRef.allocate(new Cell<>(initialDynamics), new CellType<>() { + @Override + public EffectTrait getEffectType() { + return effectTrait; + } + + @Override + public Cell duplicate(Cell cell) { + return new Cell<>(cell.initialDynamics, cell.dynamics, cell.elapsedTime); + } + + @Override + public void apply(Cell cell, E effect) { + cell.initialDynamics = effect.apply(cell.dynamics).match( + ErrorCatching::success, + error -> failure(new RuntimeException( + "Applying '%s' failed.".formatted(getEffectName(effect)), error))); + cell.dynamics = cell.initialDynamics; + cell.elapsedTime = ZERO; + } + + @Override + public void step(Cell cell, Duration duration) { + // Avoid accumulated round-off error in imperfect stepping + // by always stepping up from the initial dynamics + cell.elapsedTime = cell.elapsedTime.plus(duration); + cell.dynamics = ErrorCatchingMonad.map(cell.initialDynamics, d -> + expiring(d.data().step(cell.elapsedTime), d.expiry().minus(cell.elapsedTime))); + } + }); + } + + public static > EffectTrait> noncommutingEffects() { + return resolvingConcurrencyBy((left, right) -> x -> { + throw new UnsupportedOperationException( + "Concurrent effects are not supported on this resource."); + }); + } + + public static > EffectTrait> commutingEffects() { + return resolvingConcurrencyBy((left, right) -> x -> right.apply(left.apply(x))); + } + + public static > EffectTrait> autoEffects() { + return autoEffects(testing((CommutativityTestInput input) -> input.leftResult.equals(input.rightResult))); + } + + public static > EffectTrait> autoEffects( + Predicate>>> commutativityTest) { + return resolvingConcurrencyBy((left, right) -> x -> { + final var lrx = left.apply(right.apply(x)); + final var rlx = right.apply(left.apply(x)); + if (commutativityTest.test(new CommutativityTestInput<>(x, lrx, rlx))) { + return lrx; + } else { + throw new UnsupportedOperationException( + "Detected non-commuting concurrent effects!"); + } + }); + } + + + /** + * Lift a commutativity test from data to dynamics, + * correctly comparing expiry and error information in the process. + */ + public static Predicate>>> testing(Predicate> test) { + return input -> input.leftResult.match( + leftExpiring -> input.rightResult.match( + rightExpiring -> leftExpiring.expiry().equals(rightExpiring.expiry()) && test.test(new CommutativityTestInput<>( + input.original.match(Expiring::data, $ -> leftExpiring.data()), + leftExpiring.data(), + rightExpiring.data())), + rightError -> false), + leftError -> input.rightResult.match( + rightExpiring -> false, + rightError -> Resources.equivalentExceptions(leftError, rightError))); + } + + public record CommutativityTestInput(D original, D leftResult, D rightResult) {} + + public static > EffectTrait> resolvingConcurrencyBy(BinaryOperator> combineConcurrent) { + return new EffectTrait<>() { + @Override + public DynamicsEffect empty() { + final DynamicsEffect result = x -> x; + name(result, "No-op"); + return result; + } + + @Override + public DynamicsEffect sequentially(final DynamicsEffect prefix, final DynamicsEffect suffix) { + final DynamicsEffect result = x -> suffix.apply(prefix.apply(x)); + name(result, "(%s) then (%s)".formatted(getEffectName(prefix), getEffectName(suffix))); + return result; + } + + @Override + public DynamicsEffect concurrently(final DynamicsEffect left, final DynamicsEffect right) { + try { + final DynamicsEffect combined = combineConcurrent.apply(left, right); + final DynamicsEffect result = x -> { + try { + return combined.apply(x); + } catch (Exception e) { + return failure(e); + } + }; + name(result, "(%s) and (%s)".formatted(getEffectName(left), getEffectName(right))); + return result; + } catch (Throwable e) { + final DynamicsEffect result = $ -> failure(e); + name(result, "Failed to combine concurrent effects: (%s) and (%s)".formatted( + getEffectName(left), getEffectName(right))); + return result; + } + } + }; + } + + private static , E extends DynamicsEffect> String getEffectName(E effect) { + return getName(effect).orElse("anonymous effect"); + } + + public static class Cell { + public ErrorCatching> initialDynamics; + public ErrorCatching> dynamics; + public Duration elapsedTime; + + public Cell(ErrorCatching> dynamics) { + this(dynamics, dynamics, ZERO); + } + + public Cell(ErrorCatching> initialDynamics, ErrorCatching> dynamics, Duration elapsedTime) { + this.initialDynamics = initialDynamics; + this.dynamics = dynamics; + this.elapsedTime = elapsedTime; + } + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/DynamicsEffect.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/DynamicsEffect.java new file mode 100644 index 0000000000..043a813ac6 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/DynamicsEffect.java @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +/** + * General interface for an effect applied to a {@link MutableResource} + */ +public interface DynamicsEffect> { + ErrorCatching> apply(ErrorCatching> dynamics); +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java new file mode 100644 index 0000000000..2c93aed0c5 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java @@ -0,0 +1,105 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ErrorCatchingMonad; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Context; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; +import gov.nasa.jpl.aerie.merlin.framework.CellRef; +import gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.Cell; +import gov.nasa.jpl.aerie.merlin.protocol.model.EffectTrait; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.allocate; +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.pure; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static java.util.stream.Collectors.joining; + +/** + * A resource to which effects can be applied. + */ +public interface MutableResource> extends Resource { + void emit(DynamicsEffect effect); + default void emit(String effectName, DynamicsEffect effect) { + name(effect, effectName); + emit(effect); + } + + static > MutableResource resource(D initial) { + return resource(pure(initial)); + } + + static > MutableResource resource(D initial, EffectTrait> effectTrait) { + return resource(pure(initial), effectTrait); + } + + static > MutableResource resource(ErrorCatching> initial) { + // Use autoEffects for a generic CellResource, on the theory that most resources + // have relatively few effects, and even fewer concurrent effects, so this is performant enough. + // If that doesn't hold, a more specialized solution can be constructed directly. + return resource(initial, autoEffects()); + } + + static > MutableResource resource(ErrorCatching> initial, EffectTrait> effectTrait) { + MutableResource result = new MutableResource<>() { + private final CellRef, Cell> cell = allocate(initial, effectTrait); + + @Override + public void emit(final DynamicsEffect effect) { + augmentEffectName(effect); + cell.emit(effect); + } + + @Override + public ErrorCatching> getDynamics() { + return cell.get().dynamics; + } + + private void augmentEffectName(DynamicsEffect effect) { + String effectName = getName(effect).orElse("anonymous effect"); + String resourceName = getName(this).orElse("anonymous resource"); + String augmentedName = effectName + " on " + resourceName + Context.get().stream().map(c -> " during " + c).collect(joining()); + name(effect, augmentedName); + } + }; + if (MutableResourceFlags.DETECT_BUSY_CELLS) { + result = Profiling.profileEffects(result); + } + return result; + } + + static > void set(MutableResource resource, D newDynamics) { + resource.emit("Set " + newDynamics, DynamicsMonad.effect(x -> newDynamics)); + } + + static > void set(MutableResource resource, Expiring newDynamics) { + resource.emit("Set " + newDynamics, ErrorCatchingMonad., Expiring>map($ -> newDynamics)::apply); + } + + /** + * Turn on busy cell detection. + * + *

+ * Calling this method once before constructing your model will profile effects on every cell. + * Profiling effects may be compute and/or memory intensive, and should not be used in production. + *

+ *

+ * If only a few cells are suspect, you can also call {@link Profiling#profileEffects} + * directly on just those cells, rather than profiling every cell. + *

+ *

+ * Call {@link Profiling#dump()} to see results. + *

+ */ + static void detectBusyCells() { + MutableResourceFlags.DETECT_BUSY_CELLS = true; + } +} + +/** + * Private global flags for configuring cell resources for debugging. + * Flags here are meant to be set once before constructing the model, + * and to apply to every cell that gets built. + */ +final class MutableResourceFlags { + public static boolean DETECT_BUSY_CELLS = false; +} From 545602e40b83ab3cd0bba29ab4134ad0d9ff8c05 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 16:58:20 -0800 Subject: [PATCH 016/159] Add debugging tools Adds tools for debugging incorrect or poorly-performing simulations. * Context and Naming - These allow us to attach names to scopes in the code and to objects in memory. At strategic points, we can query this information to bubble up to the user. For example, we can attach the context an effect was emitted in to that effect. If the effect fails, we can inject that context into the error, which is more useful to debugging than a stack trace. Similarly, we can name resources when they're registered, and we can derive names for one object from others. When debugging resources, this can de-anonymise the lambda functions that make up the bulk of a resource model. * Tracing - Since debuggers struggle to "step" across simulation engine iterations, we borrow tracing techniques from functional programming. Tracing attaches print statement when a resource, task, or condition starts and stops. We also respect nested tracing, yield a log that mirrors the model's structure and lets a programmer "unpack" derived values step-by-step. * Profiling - Since all resource calculations often have the same or very similar stack frames, profilers often don't supply a useful level of detail. We provide profiling tools that distinguish calls by instance, to tell which resources / cells / conditions / tasks are hot spots. * Dependencies - Since it's sometimes useful to visualize the structure of a resource model, we track resource-level dependencies, and these can be queried from a debugger or debugging code. --- .../contrib/streamline/debugging/Context.java | 147 +++++++++ .../streamline/debugging/Dependencies.java | 191 +++++++++++ .../contrib/streamline/debugging/Naming.java | 78 +++++ .../streamline/debugging/Profiling.java | 298 ++++++++++++++++++ .../contrib/streamline/debugging/Tracing.java | 103 ++++++ 5 files changed, 817 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Context.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Dependencies.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Context.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Context.java new file mode 100644 index 0000000000..f68f27b05b --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Context.java @@ -0,0 +1,147 @@ +package gov.nasa.jpl.aerie.contrib.streamline.debugging; + +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Unit.UNIT; + +/** + * Thread-local scope-bound description of the current context. + */ +public final class Context { + private Context() {} + + private static final ThreadLocal> contexts = ThreadLocal.withInitial(ArrayDeque::new); + + /** + * @see Context#inContext(String, Supplier) + */ + public static void inContext(String contextName, Runnable action) { + inContext(contextName, asSupplier(action)); + } + + /** + * Run action in a globally-visible context. + * Contexts stack, and contexts are removed when control leaves action for any reason. + */ + public static R inContext(String contextName, Supplier action) { + // Using a thread-local context stack maintains isolation for threaded tasks. + try { + contexts.get().push(contextName); + return action.get(); + // TODO: Should we add a catch clause here that would add context to the error? + } finally { + // Doing the tear-down in a finally block maintains isolation for replaying tasks. + contexts.get().pop(); + } + } + + /** + * @see Context#inContext(List, Supplier) + */ + public static void inContext(List contextStack, Runnable action) { + inContext(contextStack, asSupplier(action)); + } + + /** + * Run action in a context stack like that returned by {@link Context#get}. + * + *

+ * This can be used to "copy" a context into another task, e.g. + *

+   *     var context = Context.get();
+   *     spawn(() -> inContext(context, () -> { ... });
+   *   
+ *

+ * + * @see Context#contextualized + */ + public static R inContext(List contextStack, Supplier action) { + if (contextStack.isEmpty()) { + return action.get(); + } else { + int n = contextStack.size() - 1; + return inContext(contextStack.get(n), () -> + inContext(contextStack.subList(0, n), action)); + } + } + + /** + * @see Context#contextualized(Supplier) + */ + public static Runnable contextualized(Runnable action) { + return contextualized(asSupplier(action))::get; + } + + /** + * Adds the current context into action. + * + *

+ * This can be used to contextualize sub-tasks with their parents context: + *

+   *     inContext("parent", () -> {
+   *       // Capture parent context while calling spawn:
+   *       spawn(contextualized(() -> {
+   *         // Runs child task in context "parent"
+   *       }));
+   *     });
+   *   
+ *

+ * + * @see Context#contextualized(String, Runnable) + * @see Context#inContext(List, Runnable) + * @see Context#inContext(String, Runnable) + */ + public static Supplier contextualized(Supplier action) { + final var context = get(); + return () -> inContext(context, action); + } + + /** + * @see Context#contextualized(String, Supplier) + */ + public static Runnable contextualized(String childContext, Runnable action) { + return contextualized(childContext, asSupplier(action))::get; + } + + /** + * Adds the current context into action, as well as an additional child context. + * + *

+ * This can be used to contextualize sub-tasks with their parents context: + *

+   *     inContext("parent", () -> {
+   *       // Capture parent context while calling spawn:
+   *       spawn(contextualized("child", () -> {
+   *         // Runs child task in context ("child", "parent")
+   *       }));
+   *     });
+   *   
+ *

+ * + * @see Context#contextualized(Runnable) + * @see Context#inContext(List, Runnable) + * @see Context#inContext(String, Runnable) + */ + public static Supplier contextualized(String childContext, Supplier action) { + return contextualized(() -> inContext(childContext, action)); + } + + /** + * Returns the list of contexts, from innermost context out. + */ + public static List get() { + return contexts.get().stream().toList(); + } + + private static Supplier asSupplier(Runnable action) { + return () -> { + action.run(); + return UNIT; + }; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Dependencies.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Dependencies.java new file mode 100644 index 0000000000..08ffe411ce --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Dependencies.java @@ -0,0 +1,191 @@ +package gov.nasa.jpl.aerie.contrib.streamline.debugging; + +import java.util.*; + +import static java.util.Collections.newSetFromMap; +import static java.util.stream.Collectors.joining; + +public final class Dependencies { + private Dependencies() {} + + // Use a WeakHashMap so that describing a thing's dependencies + // doesn't prevent it from being garbage-collected. + private static final WeakHashMap> DEPENDENCIES = new WeakHashMap<>(); + private static final WeakHashMap> DEPENDENTS = new WeakHashMap<>(); + private static final String ANONYMOUS_NAME = "..."; + + /** + * Register that dependent depends on dependency. + */ + public static void addDependency(Object dependent, Object dependency) { + // Use WeakSet = newSetFromMap + WeakHashMap, to only weakly reference dependencies. + DEPENDENCIES.computeIfAbsent(dependent, $ -> newSetFromMap(new WeakHashMap<>())).add(dependency); + DEPENDENTS.computeIfAbsent(dependency, $ -> newSetFromMap(new WeakHashMap<>())).add(dependent); + } + + /** + * Get all registered dependencies of dependent. + */ + public static Set getDependencies(Object dependent) { + return DEPENDENCIES.getOrDefault(dependent, Set.of()); + } + + /** + * Get all registered dependents of dependency. + */ + public static Set getDependents(Object dependency) { + return DEPENDENTS.getOrDefault(dependency, Set.of()); + } + + /** + * Build a string formatting the dependency graph starting from source. + *

+ * The result is in Mermaid syntax + *

+ * + * @param elideAnonymousNodes When true, remove anonymous nodes and replace them with their dependencies. + */ + public static String describeDependencyGraph(Object source, boolean elideAnonymousNodes) { + return describeDependencyGraph(List.of(source), elideAnonymousNodes); + } + + /** + * Build a string formatting the entire dependency graph. + *

+ * The result is in Mermaid syntax + *

+ * + * @param elideAnonymousNodes When true, remove anonymous nodes and replace them with their dependencies. + */ + public static String describeDependencyGraph(boolean elideAnonymousNodes) { + return describeDependencyGraph(DEPENDENCIES.keySet(), elideAnonymousNodes); + } + + /** + * Build a string formatting the dependency graph starting from sources. + *

+ * The result is in DOT syntax. + *

+ * + * @param elideAnonymousNodes When true, remove anonymous nodes and replace them with their dependencies. + */ + public static String describeDependencyGraph(Collection sources, boolean elideAnonymousNodes) { + Map> dependencyGraph = new HashMap<>(); + Map> dependentGraph = new HashMap<>(); + for (var source : sources) { + buildClosure(source, dependencyGraph, dependentGraph); + } + + if (elideAnonymousNodes) { + // Collapse anonymous nodes out of the graph + for (Object node : new ArrayList<>(dependencyGraph.keySet())) { + if (nodeName(node).equals(ANONYMOUS_NAME)) { + var dependencies = dependencyGraph.remove(node); + var dependents = dependentGraph.remove(node); + for (Object dependent : dependents) { + dependencyGraph.get(dependent).remove(node); + dependencyGraph.get(dependent).addAll(dependencies); + } + for (Object dependency : dependencies) { + dependentGraph.get(dependency).remove(node); + dependentGraph.get(dependency).addAll(dependents); + } + } + } + } + + // Describe the result + Map nodeIds = new HashMap<>(); + StringBuilder builder = new StringBuilder(); + builder.append("digraph dependencies {\n"); + final Set visited = new HashSet<>(); + // To produce good-looking graphs, visit nodes in topological order starting from roots. + dependencyGraph + .keySet() + .stream() + .filter(node -> dependentGraph.getOrDefault(node, Set.of()).isEmpty()) + .forEach(root -> describeSubgraph(root, dependencyGraph, nodeIds, visited, builder)); + // Then, visit nodes starting from any sources involved in a cycle + // Finally, visit any remaining nodes arbitrarily + while (visited.size() < dependencyGraph.size()) { + var root = sources + .stream() + .filter(n -> !visited.contains(n)) + .map((Object $) -> $) + .findFirst().or(() -> dependencyGraph.keySet() + .stream() + .filter(n -> !visited.contains(n)) + .findAny()) + .orElseThrow(); + describeSubgraph(root, dependencyGraph, nodeIds, visited, builder); + } + builder.append("}"); + return builder.toString(); + } + + private static void describeSubgraph(Object root, Map> dependencyGraph, Map nodeIds, Set visited, StringBuilder builder) { + for (var node : topologicalSort(root, dependencyGraph, visited)) { + var dependencies = dependencyGraph.get(node); + builder.append(" ") + .append(nodeId(node, nodeIds)) + .append(" [label=\"") + .append(scrub(nodeName(node))) + .append("\"]\n"); + for (var dependency : dependencies) { + builder.append(" ") + .append(nodeId(node, nodeIds)) + .append(" -> ") + .append(nodeId(dependency, nodeIds)) + .append(";\n"); + } + } + } + + private static void buildClosure(Object node, Map> dependencyGraph, Map> dependentGraph) { + if (dependencyGraph.containsKey(node)) return; + var dependencies = getDependencies(node); + dependencyGraph.computeIfAbsent(node, $ -> new HashSet<>()).addAll(dependencies); + dependentGraph.computeIfAbsent(node, $ -> new HashSet<>()); + for (var dependency : dependencies) { + dependentGraph.computeIfAbsent(dependency, $ -> new HashSet<>()).add(node); + } + for (var dependency : dependencies) { + buildClosure(dependency, dependencyGraph, dependentGraph); + } + } + + /** + * Cycle-tolerant depth-first topological sorting + */ + private static List topologicalSort(Object root, Map> dependencyGraph, Set finished) { + // Algorithm from https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search + Set visited = new HashSet<>(); + List results = new LinkedList<>(); + tsVisit(root, dependencyGraph, visited, finished, results); + return results; + } + + private static void tsVisit(Object node, Map> dependencyGraph, Set visited, Set finished, List results) { + if (finished.contains(node)) return; + if (visited.contains(node)) return; + visited.add(node); + for (var dependency : dependencyGraph.get(node)) { + tsVisit(dependency, dependencyGraph, visited, finished, results); + } + visited.remove(node); + finished.add(node); + results.add(0, node); + } + + private static String nodeName(Object node) { + return Naming.getName(node, ANONYMOUS_NAME); + } + + private static String nodeId(Object node, Map nodeIds) { + return nodeIds.computeIfAbsent(node, $ -> "N" + nodeIds.size()); + } + + private static String scrub(String label) { + return label.replace("\"", "\\\""); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java new file mode 100644 index 0000000000..8929b57a5a --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java @@ -0,0 +1,78 @@ +package gov.nasa.jpl.aerie.contrib.streamline.debugging; + +import org.apache.commons.lang3.mutable.MutableObject; + +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + + +/** + * Allows anything that uses reference equality to be given a name. + * + *

+ * By handling naming in a static auxiliary data structure, we achieve several goals: + * 1) Naming doesn't bloat interfaces like Resource and DynamicsEffect. + * 2) Names can be applied to classes and interfaces after-the-fact, + * including applying names to classes and interfaces that can't be modified, like library code. + * 3) Naming is nevertheless globally available. + * (Unlike passing the name in a parallel parameter, for example.) + *

+ */ +public final class Naming { + private Naming() {} + + // Use a WeakHashMap so that naming a thing doesn't prevent it from being garbage-collected. + private static final WeakHashMap>> NAMES = new WeakHashMap<>(); + // Way to inject a temporary "anonymous" name, so derived names still work even when not all args are named. + private static final MutableObject> anonymousName = new MutableObject<>(Optional.empty()); + + /** + * Register a name for thing, as a function of args' names. + * If any of the args are anonymous, so is this thing. + */ + public static void name(Object thing, String nameFormat, Object... args) { + // Only capture weak references to arguments, so we don't leak memory + var args$ = Arrays.stream(args).map(WeakReference::new).toArray(WeakReference[]::new); + NAMES.put(thing, () -> { + Object[] argNames = new Object[args$.length]; + for (int i = 0; i < args$.length; ++i) { + // Try to resolve the argument name by first looking up and using its registered name, + // or by falling back to the anonymous name. + var argName$ = Optional.ofNullable(args$[i].get()) + .flatMap(Naming::getName) + .or(anonymousName::getValue); + if (argName$.isEmpty()) return Optional.empty(); + argNames[i] = argName$.get(); + } + return Optional.of(nameFormat.formatted(argNames)); + }); + } + + /** + * Get the name for thing. + * If thing has no registered name and no synonyms, + * returns empty. + */ + public static Optional getName(Object thing) { + return Optional.ofNullable(NAMES.get(thing)).flatMap(Supplier::get).or(anonymousName::getValue); + } + + /** + * Get the name for thing. + * Use anonymousName for anything without a name instead of returning empty. + */ + public static String getName(Object thing, String anonymousName) { + Naming.anonymousName.setValue(Optional.of(anonymousName)); + var result = getName(thing); + Naming.anonymousName.setValue(Optional.empty()); + // This will never throw, because anonymous name will guarantee that some name is found. + return result.orElseThrow(); + } + + public static String argsFormat(Collection collection) { + return "(" + IntStream.range(0, collection.size()).mapToObj($ -> "%s").collect(Collectors.joining(", ")) + ")"; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java new file mode 100644 index 0000000000..4a369b8322 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java @@ -0,0 +1,298 @@ +package gov.nasa.jpl.aerie.contrib.streamline.debugging; + +import gov.nasa.jpl.aerie.contrib.streamline.core.*; +import gov.nasa.jpl.aerie.merlin.framework.Condition; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static java.util.Comparator.comparingLong; + +/** + * Functions for profiling resources and conditions + * + *

+ * WARNING! Profiling tools use static variables + * that are not re-initialized when the simulation restarts. + * This may give inconsistent or erroneous output in unit tests or scheduling. + *

Do not depend on profiling data for model behavior!

+ *

+ *

+ * Additionally, all profiling methods short-circuit if a null or empty name is given. + * Profile only some invocations of a method by giving "" as a default profiling name + * in a non-profiled overload of the method, e.g. + *

+ *     void someMethod(int a, String b) {
+ *       // By passing "", we turn off profiling implicitly
+ *       someMethod("", a, b);
+ *     }
+ *     void someMethod(String profilingName, int a, String b) {
+ *       // some code that uses a profiling method
+ *     }
+ *   
+ *

+ */ +public final class Profiling { + private Profiling() {} + + private static final long overallStartTime = System.nanoTime(); + + /** + * Cumulative count of profiled nanoseconds, used to account for nested profiled calls. + */ + private static long cumulativeProfiledTime = 0; + + private static final Map resourceSamples = new HashMap<>(); + private static final Map conditionEvaluations = new HashMap<>(); + private static final Map taskExecutions = new HashMap<>(); + private static final Map effectsEmitted = new HashMap<>(); + + private static final Comparator SORT_BY_CALLS_MADE = comparingLong(c -> -c.callsMade); + private static final Comparator SORT_BY_OWN_NANOS = comparingLong(c -> -c.ownNanos); + + public static Resource profile(Resource resource) { + return profile(null, resource); + } + + public static Resource profile(String name, Resource resource) { + Resource result = new Resource<>() { + @Override + public ErrorCatching> getDynamics() { + return resourceSamples.computeIfAbsent(computeName(name, this), k -> new CallStats()) + .accrue(resource::getDynamics); + } + }; + assignName(result, name, resource); + return result; + } + + public static > MutableResource profile(MutableResource resource) { + return profile(null, resource); + } + + public static > MutableResource profile(String name, MutableResource resource) { + MutableResource result = new MutableResource<>() { + @Override + public void emit(DynamicsEffect effect) { + resource.emit(effect); + } + + @Override + public ErrorCatching> getDynamics() { + return resourceSamples.computeIfAbsent(computeName(name, this), k -> new CallStats()) + .accrue(resource::getDynamics); + } + }; + assignName(result, name, resource); + return result; + } + + public static Condition profile(Condition condition) { + return profile(null, condition); + } + + public static Condition profile(String name, Condition condition) { + Condition result = new Condition() { + @Override + public Optional nextSatisfied(boolean positive, Duration atEarliest, Duration atLatest) { + return accrue(conditionEvaluations, computeName(name, this), () -> condition.nextSatisfied(positive, atEarliest, atLatest)); + } + }; + assignName(result, name, condition); + return result; + } + + public static Supplier profile(Supplier conditionSupplier) { + return profile(null, conditionSupplier); + } + + public static Supplier profile(String name, Supplier conditionSupplier) { + return () -> profile(name, conditionSupplier.get()); + } + + public static Runnable profile(Runnable task) { + return profile(null, task); + } + + public static Runnable profile(String name, Runnable task) { + return () -> profileTask(name, () -> { task.run(); return Unit.UNIT; }); + } + + public static Supplier profileTask(Supplier task) { + return profileTask(null, task); + } + + public static Supplier profileTask(String name, Supplier task) { + Supplier result = new Supplier<>() { + @Override + public R get() { + return accrue(taskExecutions, computeName(name, this), task); + } + }; + assignName(result, name, task); + return result; + } + + private static long ANONYMOUS_CELL_RESOURCE_ID = 0; + public static > MutableResource profileEffects(MutableResource resource) { + return new MutableResource<>() { + private String name = null; + @Override + public void emit(DynamicsEffect effect) { + // Get the name the first time an effect is emitted, + // which will be after any registrations happen. + if (name == null) { + name = getName(this, "..."); + if (name.equals("...")) { + var generatedName = "CellResource" + (ANONYMOUS_CELL_RESOURCE_ID++); + name(this, generatedName); + name = generatedName; + } + } + resource.emit(x -> accrue(effectsEmitted, name, () -> effect.apply(x))); + } + + @Override + public ErrorCatching> getDynamics() { + return resource.getDynamics(); + } + }; + } + + private static String computeName(String explicitName, Object profiledThing) { + return explicitName != null ? explicitName : getName(profiledThing, "..."); + } + + private static void assignName(Object profiledThing, String explicitName, Object originalThing) { + if (explicitName == null) { + name(profiledThing, "%s", originalThing); + } else { + name(profiledThing, explicitName); + } + } + + private static R accrue(Map statsMap, String name, Supplier call) { + return statsMap.computeIfAbsent(name, k -> new CallStats()).accrue(call); + } + + public static void dump() { + long overallElapsedNanos = System.nanoTime() - overallStartTime; + System.out.printf("Overall time: %d ms%n", overallElapsedNanos / 1_000_000); + if (!resourceSamples.isEmpty()) { + System.out.println("Profiled resources:"); + dumpSampleMap(resourceSamples, overallElapsedNanos, SORT_BY_OWN_NANOS); + } + if (!conditionEvaluations.isEmpty()) { + // Conditions are usually quick to evaluate, but trigger tasks and resource computation. + // Therefore, calls are more important than time taken directly. + System.out.println("Profiled conditions:"); + dumpSampleMap(conditionEvaluations, overallElapsedNanos, SORT_BY_CALLS_MADE); + } + if (!taskExecutions.isEmpty()) { + System.out.println("Profiled tasks:"); + dumpSampleMap(taskExecutions, overallElapsedNanos, SORT_BY_OWN_NANOS); + } + if (!effectsEmitted.isEmpty()) { + System.out.println("Profiled effects:"); + // Effects are usually quick to evaluate, but trigger tasks and resource computation. + // Therefore, calls are more important than time taken directly. + dumpSampleMap(effectsEmitted, overallElapsedNanos, SORT_BY_CALLS_MADE); + } + } + + private static void dumpSampleMap(Map map, long overallElapsedNanos, Comparator sortBy) { + final var nameLength = Math.max(5, map.keySet().stream().mapToInt(String::length).max().orElse(1)); + final var totalCalls = map.values().stream().mapToLong(c1 -> c1.callsMade).sum(); + final var totalNanos = map.values().stream().mapToLong(c1 -> c1.ownNanos).sum(); + final var callsLength = Math.max(5, String.valueOf(totalCalls).length()); + final var millisLength = Math.max(7, String.valueOf(totalNanos / 1_000_000).length()); + final var titleFormat = + " %-" + nameLength + "s |" + + " %" + callsLength + "s %7s |" + + " %" + millisLength + "s %7s |" + + " %" + millisLength + "s %7s %7s %7s" + + "%n"; + final var lineFormat = + " %-" + nameLength + "s |" + + " %" + callsLength + "d %5.1f%% |" + + " %" + millisLength + "d %5.1f%% |" + + " %" + millisLength + "d %5.1f%% %5.1f%% %5.1f%%" + + "%n"; + System.out.printf( + titleFormat, + "Name", + "Calls", + "%Total", + "Call ms", + "%All", + "Self ms", + "%Call", + "%Total", + "%All"); + System.out.printf( + lineFormat, + "Total", + totalCalls, + 100.0, + // Adding up "total" times isn't sensible, since it multiple-counts time + 0, + Double.NaN, + // Adding up "self" times gives total profiled time + totalNanos / 1_000_000, + 100.0, + 100.0, + 100.0 * totalNanos / overallElapsedNanos); + map.entrySet() + .stream() + .sorted((a, b) -> sortBy.compare(a.getValue(), b.getValue())) + .forEachOrdered(entry -> { + var stats = entry.getValue(); + System.out.printf( + lineFormat, + entry.getKey(), + stats.callsMade, + 100.0 * stats.callsMade / totalCalls, + stats.totalNanos / 1_000_000, + 100.0 * stats.totalNanos / overallElapsedNanos, + stats.ownNanos / 1_000_000, + 100.0 * stats.ownNanos / stats.totalNanos, + 100.0 * stats.ownNanos / totalNanos, + 100.0 * stats.ownNanos / overallElapsedNanos); + }); + } + + private static final class CallStats { + public long callsMade = 0; + public long totalNanos = 0; + public long ownNanos = 0; + + public void accrue(Runnable call) { + accrue(() -> { call.run(); return Unit.UNIT; }); + } + + public R accrue(Supplier call) { + long startCumulative = cumulativeProfiledTime; + long start = System.nanoTime(); + var result = call.get(); + long end = System.nanoTime(); + long endCumulative = cumulativeProfiledTime; + + long totalNanosInThisCall = end - start; + long totalNanosInSubCalls = endCumulative - startCumulative; + long ownNanosInThisCall = totalNanosInThisCall - totalNanosInSubCalls; + + ++callsMade; + totalNanos += totalNanosInThisCall; + ownNanos += ownNanosInThisCall; + cumulativeProfiledTime += ownNanosInThisCall; + + return result; + } + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java new file mode 100644 index 0000000000..ebe68b3ff0 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java @@ -0,0 +1,103 @@ +package gov.nasa.jpl.aerie.contrib.streamline.debugging; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.DynamicsEffect; +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.merlin.framework.Condition; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; + +import java.util.Stack; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentTime; + +/** + * Functions for debugging resources by tracing their calculation. + */ +public final class Tracing { + private Tracing() {} + + private final static Stack activeTracePoints = new Stack<>(); + + public static Resource trace(Resource resource) { + return trace(() -> Naming.getName(resource).orElse("anonymous resource"), resource); + } + + public static Resource trace(String name, Resource resource) { + return trace(() -> name, resource); + } + + public static Resource trace(Supplier name, Resource resource) { + return () -> traceAction(name, resource::getDynamics); + } + + public static > MutableResource trace(MutableResource resource) { + return trace(() -> Naming.getName(resource).orElse("anonymous resource"), resource); + } + + public static > MutableResource trace(String name, MutableResource resource) { + return trace(() -> name, resource); + } + + public static > MutableResource trace(Supplier name, MutableResource resource) { + return new MutableResource<>() { + private final Resource tracedResoure = trace(name, (Resource) resource); + + @Override + public void emit(final DynamicsEffect effect) { + traceAction( + () -> String.format("Emit '%s' on %s", + Naming.getName(effect).orElse("anonymous effect"), + name.get()), + () -> { resource.emit(effect); return Unit.UNIT; }); + } + + @Override + public ErrorCatching> getDynamics() { + return tracedResoure.getDynamics(); + } + }; + } + + public static Condition trace(Condition condition) { + return trace(() -> Naming.getName(condition).orElse("anonymous condition"), condition); + } + + public static Condition trace(String name, Condition condition) { + return trace(() -> name, condition); + } + + public static Condition trace(Supplier name, Condition condition) { + return (positive, atEarliest, atLatest) -> + traceAction(() -> name.get() + " evaluate (%s, %s, %s)".formatted(positive, atEarliest, atLatest), () -> condition.nextSatisfied(positive, atEarliest, atLatest)); + } + + public static Supplier trace(Supplier condition) { + return trace(() -> Naming.getName(condition).orElse("anonymous condition"), condition); + } + + public static Supplier trace(String name, Supplier condition) { + return trace(() -> name, condition); + } + + public static Supplier trace(Supplier name, Supplier condition) { + // Trace calling the supplier separately from tracing the condition itself. + return () -> traceAction(() -> name.get() + " (generation)", () -> trace(name, condition.get())); + } + + private static T traceAction(Supplier name, Supplier action) { + activeTracePoints.push(name.get()); + System.out.printf("TRACE: %s - %s start...%n", currentTime(), formatStack()); + T result = action.get(); + System.out.printf("TRACE: %s - %s: %s%n", currentTime(), formatStack(), result); + activeTracePoints.pop(); + return result; + } + + private static String formatStack() { + return String.join("->", activeTracePoints); + } +} From e77d81c88ab660149c59c6b7d1476baf97641caa Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 17:22:31 -0800 Subject: [PATCH 017/159] Add streamline Registrar Adds a registrar wrapping and adapting the standard Merlin Registrar. This registrar unwraps the ErrorCatching and Expiring layers from resources, with options to either throw or log errors. --- .../streamline/modeling/Registrar.java | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java new file mode 100644 index 0000000000..a173fe683d --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java @@ -0,0 +1,175 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling; + +import gov.nasa.jpl.aerie.contrib.serialization.mappers.IntegerValueMapper; +import gov.nasa.jpl.aerie.contrib.serialization.mappers.NullableValueMapper; +import gov.nasa.jpl.aerie.contrib.serialization.mappers.StringValueMapper; +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ThinResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.merlin.framework.ValueMapper; +import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentData; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profile; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Tracing.trace; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar.ErrorBehavior.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.not; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.when; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteDynamicsMonad.effect; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad.map; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear.linear; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.waitUntil; +import static java.util.stream.Collectors.joining; + +/** + * Wrapper for {@link gov.nasa.jpl.aerie.merlin.framework.Registrar} specialized for {@link Resource}. + * + *

+ * Automatically creates and populates "errors" and "numberOfErrors" resources, if {@link ErrorBehavior#Log} is used. + *

+ */ +public class Registrar { + private final gov.nasa.jpl.aerie.merlin.framework.Registrar baseRegistrar; + private boolean trace = false; + private boolean profile = false; + private final MutableResource>>> errors; + private final ErrorBehavior errorBehavior; + + public enum ErrorBehavior { + /** + * Log errors to the error state, + * and replace resource value with null. + */ + Log, + /** + * Throw errors, crashing the simulation immediately. + */ + Throw + } + + public Registrar(final gov.nasa.jpl.aerie.merlin.framework.Registrar baseRegistrar, final ErrorBehavior errorBehavior) { + Resources.init(); + this.baseRegistrar = baseRegistrar; + this.errorBehavior = errorBehavior; + errors = resource(Discrete.discrete(Map.of())); + var errorString = map(errors, errors$ -> errors$.entrySet().stream().map(entry -> formatError(entry.getKey(), entry.getValue())).collect(joining("\n\n"))); + discrete("errors", errorString, new StringValueMapper()); + discrete("numberOfErrors", map(errors, Map::size), new IntegerValueMapper()); + } + + private static String formatError(Throwable e, Collection affectedResources) { + return "Error affecting %s:%n%s".formatted( + String.join(", ", affectedResources), + formatException(e)); + } + + private static String formatException(Throwable e) { + return ExceptionUtils.stream(e) + .map(ExceptionUtils::getMessage) + .collect(joining("\nCaused by: ")); + } + + public void setTrace() { + trace = true; + } + + public void clearTrace() { + trace = false; + } + + public void setProfile() { + profile = true; + } + + public void clearProfile() { + profile = true; + } + + public void discrete(final String name, final Resource> resource, final ValueMapper mapper) { + name(resource, name); + var debugResource = debug(name, resource); + gov.nasa.jpl.aerie.merlin.framework.Resource registeredResource = switch (errorBehavior) { + case Log -> () -> currentValue(debugResource, null); + case Throw -> wrapErrors(name, () -> currentValue(debugResource)); + }; + baseRegistrar.discrete(name, registeredResource, new NullableValueMapper<>(mapper)); + if (errorBehavior.equals(Log)) logErrors(name, debugResource); + } + + public void real(final String name, final Resource resource) { + name(resource, name); + var debugResource = debug(name, resource); + gov.nasa.jpl.aerie.merlin.framework.Resource registeredResource = switch (errorBehavior) { + case Log -> () -> realDynamics(currentData(debugResource, linear(0, 0))); + case Throw -> wrapErrors(name, () -> realDynamics(currentData(debugResource))); + }; + baseRegistrar.real(name, registeredResource); + if (errorBehavior.equals(Log)) logErrors(name, debugResource); + } + + private static RealDynamics realDynamics(Linear linear) { + return RealDynamics.linear(linear.extract(), linear.rate()); + } + + private Resource debug(String name, Resource resource) { + var tracedResource = trace ? trace(resource) : resource; + return profile ? profile(tracedResource) : tracedResource; + } + + private > void logErrors(String name, Resource resource) { + // TODO: Is there any way to avoid computing resources twice, once for sampling and separately for errors? + Resource> hasError = ThinResourceMonad.bind(resource, ec -> DiscreteResourceMonad.pure(ec.match(value -> false, error -> true)))::getDynamics; + whenever(hasError, () -> { + resource.getDynamics().match($ -> null, e -> logError(name, e)); + // Avoid infinite loops by waiting for resource to clear before logging a new error. + // TODO: This means we won't log if a resource changes from error1 to error2 without clearing in between. + // Maybe we should implement a condition like "is not the current error"? + waitUntil(when(not(hasError))); + }); + } + + // TODO: Consider pulling in a Guava MultiMap instead of doing this by hand below + private Unit logError(String resourceName, Throwable e) { + errors.emit(effect(s -> { + var s$ = new HashMap<>(s); + s$.compute(e, (e$, affectedResources) -> { + if (affectedResources == null) { + return Set.of(resourceName); + } else { + var affectedResources$ = new HashSet<>(affectedResources); + affectedResources$.add(resourceName); + return affectedResources$; + } + }); + return s$; + })); + return Unit.UNIT; + } + + private static gov.nasa.jpl.aerie.merlin.framework.Resource wrapErrors(String resourceName, gov.nasa.jpl.aerie.merlin.framework.Resource resource) { + return () -> { + try { + return resource.getDynamics(); + } catch (Throwable e) { + throw new RuntimeException("Error affecting " + resourceName, e); + } + }; + } +} From 1b9f13bc80c1d4371453aaed207cb73c7a6c2525 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 17:27:01 -0800 Subject: [PATCH 018/159] Add general Resource utilities Adds utilities applicable to all resources. In particular, adds the "dynamicsChange" condition, which detects when a resource changes from one profile segment to the next. --- .../contrib/streamline/core/Resources.java | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java new file mode 100644 index 0000000000..8d5de971a8 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java @@ -0,0 +1,311 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock; +import gov.nasa.jpl.aerie.merlin.framework.Condition; +import gov.nasa.jpl.aerie.merlin.framework.Scoped; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.neverExpiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.NEVER; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.wheneverDynamicsChange; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.map; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock.clock; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.*; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; + +/** + * Utility methods for {@link Resource}s. + */ +public final class Resources { + private Resources() {} + + /** + * Ensure that Resources are initialized. + * + *

+ * This method needs to be called during simulation initialization. + * This method is idempotent; calling it multiple times is the same as calling it once. + *

+ */ + public static void init() { + currentTime(); + } + + private static Resource CLOCK = resource(clock(ZERO)); + public static Duration currentTime() { + try { + return currentValue(CLOCK); + } catch (Scoped.EmptyDynamicCellException | IllegalArgumentException e) { + // If we're running unit tests, several simulations can happen without reloading the Resources class. + // In that case, we'll have discarded the clock resource we were using, and get the above exception. + // REVIEW: Is there a cleaner way to make sure this resource gets (re-)initialized? + CLOCK = resource(clock(ZERO)); + return currentValue(CLOCK); + } + } + + public static D currentData(Resource resource) { + return data(resource.getDynamics()); + } + + public static D currentData(Resource resource, D dynamicsIfError) { + return data(resource.getDynamics(), dynamicsIfError); + } + + public static > V currentValue(Resource resource) { + return value(resource.getDynamics()); + } + + public static > V currentValue(Resource resource, V valueIfError) { + return value(resource.getDynamics(), valueIfError); + } + + public static D data(ErrorCatching> dynamics) { + return dynamics.getOrThrow().data(); + } + + public static D data(ErrorCatching> dynamics, D dynamicsIfError) { + return dynamics.match(Expiring::data, error -> dynamicsIfError); + } + + public static > V value(ErrorCatching> dynamics) { + return data(dynamics).extract(); + } + + public static > V value(ErrorCatching> dynamics, V valueIfError) { + return dynamics.match(result -> result.data().extract(), error -> valueIfError); + } + + /** + * Condition that's triggered when the dynamics on resource change in a way + * that's different from just evolving with time. + * This can be due to effects on a cell used by this resource, + * or by some part of the derivation expiring. + */ + public static > Condition dynamicsChange(Resource resource) { + final var startingDynamics = resource.getDynamics(); + final Duration startTime = currentTime(); + Condition result = (positive, atEarliest, atLatest) -> { + var currentDynamics = resource.getDynamics(); + boolean haveChanged = startingDynamics.match( + start -> currentDynamics.match( + current -> !current.data().equals(start.data().step(currentTime().minus(startTime))), + ignored -> true), + startException -> currentDynamics.match( + ignored -> true, + // Use semantic comparison for exceptions, since derivation can generate the exception each invocation. + currentException -> !equivalentExceptions(startException, currentException))); + + return positive == haveChanged + ? Optional.of(atEarliest) + : positive + ? currentDynamics.match( + expiring -> expiring.expiry().value().filter(atLatest::noShorterThan), + exception -> Optional.empty()) + : Optional.empty(); + }; + name(result, "Dynamics Change (%s)", resource); + return result; + } + + /** + * A weaker form of {@link Resources#dynamicsChange}, + * which doesn't attempt to compare dynamics. + *

+ * This Condition is less robust, and may trigger spuriously. + * When used in a reaction loop like {@link Reactions#wheneverUpdates(Resource, Consumer)}, + * there is a known 1-tick "blindspot" after updates fires; if two updates happen to resource + * in back-to-back simulation ticks at the same time, only the first triggers the reaction loop. + * One way to handle this blindspot is with spawn and delay, like this: + *

+   * wheneverUpdates(resource, () -> {
+   *   // Handle the immediate update, if desired
+   *   spawn(replaying(() -> {
+   *     delay(ZERO);
+   *     // Handle an update 1 tick after the update that triggered this loop, if any happened.
+   *   }));
+   * });
+   * 
+ *

+ *

+ * However, this condition doesn't depend on dynamics having a well-behaved equals method. + *

+ */ + public static Condition updates(Resource resource) { + var result = new Condition() { + private boolean first = true; + + @Override + public Optional nextSatisfied( + final boolean positive, + final Duration atEarliest, + final Duration atLatest) + { + // Get resource to subscribe this condition to resource's cells + var dynamics = resource.getDynamics(); + if (first) { + first = false; + return dynamics.match(Expiring::expiry, e -> NEVER).value().filter(atLatest::noShorterThan); + } else { + return Optional.of(atEarliest); + } + } + }; + name(result, "Updates (%s)", resource); + return result; + } + + public static Condition expires(Resource resource) { + Condition result = (positive, atEarliest, atLatest) -> resource.getDynamics().match( + expiring -> expiring.expiry().value().filter(atLatest::noShorterThan).map(t -> Duration.max(t, atEarliest)), + error -> Optional.empty()); + name(result, "Expires (%s)", resource); + return result; + } + + /** + * Use a reaction loop to synchronize destination with source. + * This is used primarily for building feedback loops in a resource derivation. + */ + public static > void forward(Resource source, MutableResource destination) { + wheneverDynamicsChange(source, s -> destination.emit( + "Forward %s dynamics: %s".formatted(getName(source).orElse("anonymous"), s), + $ -> s)); + addDependency(destination, source); + } + + // TODO: Should this be moved somewhere else? + /** + * Tests if two exceptions are equivalent from the point of view of resource values. + * Two exceptions are equivalent if they have the same type and message. + */ + public static boolean equivalentExceptions(Throwable startException, Throwable currentException) { + return Objects.equals(startException.getClass(), currentException.getClass()) + && Objects.equals(startException.getMessage(), currentException.getMessage()); + } + + /** + * Cache this resource in a resource. + * + *

+ * Updates the resource when resource changes dynamics. + * This can be used to isolate a resource from effects + * which don't change the dynamics, so Aerie samples that + * resource only when strictly necessary. + *

+ *

+ * This introduces a small delay in deriving values. + * Specifically, the cached version of a resource changes two + * simulation engine cycles after its uncached version. + * It will show up as the same instant in the results, + * but beware that it could be momentarily out-of-sync + * with its sources during simulation. + *

+ */ + public static > Resource cache(Resource resource) { + var cell = resource(resource.getDynamics()); + forward(resource, cell); + name(cell, "Cache (%s)", resource); + return cell; + } + + /** + * Signal discrete changes in this resource's dynamics. + * + *

+ * For Aerie's resource sampling to work correctly, + * there must be an effect every time a resource changes dynamics. + * For most derived resources, this happens automatically. + * For some derivations, though, continuous changes in the source state + * can cause discrete changes in the result. + * For example, imagine a continuous numeric resource R, + * and a derived resource S := "R > 0". + * If R changes continuously from positive to negative, + * then S changes discretely from true to false, *without* an effect. + * If used directly, Aerie would not re-sample S at this time. + * This method emits a trivial effect when this happens so that S + * *would* be resampled correctly. + *

+ *

+ * Unlike {@link Resources#cache}, this method does *not* introduce + * a delay between the source and derived resources. + * Signalling resources use a resource "in parallel" rather than "in series" + * with the derivation process, thereby avoiding the delay. + * Like regular derived resources, signalling resources calculate their value + * through the derivation every time they are sampled. + *

+ */ + // REVIEW: Suggestion from Jonathan Castello to remove this method + // in favor of allowing resources to report expiry information directly. + // This would be cleaner and potentially more performant. + public static > Resource signalling(Resource resource) { + var cell = resource(discrete(Unit.UNIT)); + name(cell, "Signal for (%s)", resource); + wheneverDynamicsChange(resource, ignored -> cell.emit($ -> $)); + Resource result = () -> { + cell.getDynamics(); + return resource.getDynamics(); + }; + name(result, "Signalling (%s)", resource); + addDependency(result, resource); + addDependency(result, cell); + return result; + } + + public static > Resource shift(Resource resource, Duration interval, D initialDynamics) { + var cell = resource(initialDynamics); + delayedSet(cell, resource.getDynamics(), interval); + wheneverDynamicsChange(resource, newDynamics -> + delayedSet(cell, newDynamics, interval)); + name(cell, "Shifted (%s)", resource); + addDependency(cell, resource); + return cell; + } + + private static > void delayedSet( + MutableResource cell, ErrorCatching> newDynamics, Duration interval) + { + spawn(replaying(() -> { + delay(interval); + cell.emit($ -> newDynamics); + })); + } + + /** + * Erase expiry information from a resource. + * + *

+ * This is useful when a resource is defined through a feedback loop, + * to not propagate the expiry across iterations of that loop + *

+ */ + public static Resource eraseExpiry(Resource p) { + Resource result = () -> p.getDynamics().map(e -> neverExpiring(e.data())); + name(result, "Erase expiry (%s)", p); + addDependency(result, p); + return result; + } + + public static Resource reduce(Stream> operands, Resource identity, BiFunction, Resource, Resource> operation, String operationName) { + return reduce(operands.toList(), identity, operation, operationName); + } + + public static Resource reduce(Collection> operands, Resource identity, BiFunction, Resource, Resource> operation, String operationName) { + var result = operands.stream().reduce(identity, operation, operation::apply); + name(result, operationName + argsFormat(operands), operands.toArray()); + return result; + } +} From b75f6ab3e78a800a46783224b7a5195ea9ce8e99 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 17:33:06 -0800 Subject: [PATCH 019/159] Add Reaction utilities Adds Reactions, utilities for creating lightweight, looping tasks. --- .../contrib/streamline/core/Reactions.java | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java new file mode 100644 index 0000000000..5ae4597240 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java @@ -0,0 +1,115 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.merlin.framework.Condition; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.lang3.mutable.MutableObject; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.dynamicsChange; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.updates; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Context.contextualized; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.when; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.replaying; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.spawn; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.waitUntil; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + +/** + * Utilities to create lightweight looping tasks, + * usually spawned as daemons during modeling construction, + * to "react" to important events in the simulation. + * + *

+ * All reactions use the most efficient task setup for lightweight, short-lived tasks. + * At present, this means a trampolining-replaying task setup. + * Do not mutate state outside of cells across reaction iterations; this may produce nondeterminism or faults. + *

+ */ +public final class Reactions { + private Reactions() {} + + public static void whenever(Resource> conditionResource, Runnable action) { + whenever(when(conditionResource), action); + } + + public static void whenever(Condition condition, Runnable action) { + whenever(() -> condition, action); + } + + public static void whenever(Supplier trigger, Runnable action) { + final Condition condition = trigger.get(); + // Use replaying tasks to avoid threading overhead. + spawn(replaying(contextualized(() -> { + waitUntil(condition); + action.run(); + // Trampoline off this task to avoid replaying. + whenever(trigger, action); + }))); + } + + // Special case for dynamicsChange condition, since it's non-obvious that this needs to be run in lambda form + public static > void wheneverDynamicsChange(Resource resource, Consumer>> reaction) { + whenever(() -> dynamicsChange(resource), () -> reaction.accept(resource.getDynamics())); + } + + /** + * Run reaction whenever resource {@link Resources#updates}. + * Note there is a 1-tick blindspot when using this method; + * if there are updates on back-to-back simulation ticks in the same instant, + * only the first triggers reaction. + * See {@link Resources#updates} for a common pattern to mitigate this shortcoming. + */ + public static > void wheneverUpdates(Resource resource, Runnable reaction) { + whenever(() -> updates(resource), reaction); + } + + /** + * Run reaction whenever resource {@link Resources#updates}, + * with a 1-tick delay to mitigate the {@link Resources#updates} blindspot. + * See {@link Resources#updates} for a discussion of this shortcoming and its mitigations. + */ + public static > void wheneverUpdates(Resource resource, Consumer>> reaction) { + whenever(() -> updates(resource), () -> { + spawn(replaying(() -> { + /* + Spawn and delay zero, because we have a 1-tick blindspot when using "updates" + + Without the spawn/delay(0): + Simulation ticks resource updates reaction task + 0 update 0, delay(0) + "updates" condition satisfied + 1 update 1 reaction runs, sees resource update 0 ONLY, set "updates" condition again + "updates" condition unsatisfied + + With the spawn/delay(0): + Simulation ticks resource updates approximate task + 0 update 0, delay(0) + "updates" condition satisfied + 1 update 1 spawn task, set "updates" condition again + "updates" condition unsatisfied + 2 reaction runs, sees resource update 1 + + Updates spaced at least 2 ticks apart will be caught by the next "updates" condition. + */ + delay(ZERO); + reaction.accept(resource.getDynamics()); + })); + }); + } + + public static void every(Duration period, Runnable action) { + every(() -> period, action); + } + + public static void every(Supplier periodSupplier, Runnable action) { + spawn(replaying(contextualized(() -> { + delay(periodSupplier.get()); + action.run(); + every(periodSupplier, action); + }))); + } +} From 22a4623e91590132a8181a558440ab0d6c2979ed Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 17:35:35 -0800 Subject: [PATCH 020/159] Add Discrete resources Adds the Discrete dynamics type, as well as utilities for working with discrete Resources: * DiscreteEffects - utility methods for common operations on MutableResource, like increment/decrement, set value, etc. * DiscreteResources - utility methods for defining and deriving discrete resources, including integer and double-precision arithmetic, boolean logic, etc. Notable / unusual features of this code include: * `DiscreteResources.when` - Converts a discrete boolean resource into a condition, satisfied when the resource is true. This realizes the equivalence between conditions and boolean resources. By deriving boolean resources, we get the equivalent condition with no additional code. * `DiscreteResources.discreteResource` - Declares a discrete MutableResource, with special handling for Double values. When effects are applied to doubles, floating-point precision mismatches can make effects that logically commute appear not to commute. This special constructor for MutableResources uses a toleranced equality for checking commutativity to solve this problem. * Monads - The `Discrete` wrapper forms a (trivial) monad, which can be composed with the other major monads. This means derivations on discrete resources can use the value directly, and monad methods will lift that all the way to acting on resources. --- .../modeling/discrete/Discrete.java | 15 + .../modeling/discrete/DiscreteEffects.java | 204 +++++++ .../modeling/discrete/DiscreteResources.java | 399 ++++++++++++++ .../monads/DiscreteDynamicsMonad.java | 520 ++++++++++++++++++ .../discrete/monads/DiscreteMonad.java | 510 +++++++++++++++++ .../monads/DiscreteResourceMonad.java | 512 +++++++++++++++++ .../contrib/streamline/utils/DoubleUtils.java | 14 + 7 files changed, 2174 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/Discrete.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffects.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteResources.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteDynamicsMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteResourceMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/DoubleUtils.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/Discrete.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/Discrete.java new file mode 100644 index 0000000000..2f6ee6ee3d --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/Discrete.java @@ -0,0 +1,15 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +public record Discrete(V extract) implements Dynamics> { + @Override + public Discrete step(Duration t) { + return this; + } + + public static Discrete discrete(V value) { + return new Discrete<>(value); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffects.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffects.java new file mode 100644 index 0000000000..b62f341180 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffects.java @@ -0,0 +1,204 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteDynamicsMonad.effect; + +public final class DiscreteEffects { + private DiscreteEffects() {} + + // More convenient overload of "set" when using discrete dynamics + + /** + * Set the resource to the given value. + */ + public static void set(MutableResource> resource, A newValue) { + resource.emit("Set " + newValue, effect(x -> newValue)); + } + + // Flag/Switch style operations + + /** + * Set the resource to true. + */ + public static void turnOn(MutableResource> resource) { + set(resource, true); + } + + /** + * Set the resource to false. + */ + public static void turnOff(MutableResource> resource) { + set(resource, false); + } + + /** + * Toggle the resource value. + */ + public static void toggle(MutableResource> resource) { + resource.emit("Toggle", effect(x -> !x)); + } + + // Counter style operations + + /** + * Add one to the resource's value. + */ + public static void increment(MutableResource> resource) { + increment(resource, 1); + } + + /** + * Add the given amount to the resource's value. + */ + public static void increment(MutableResource> resource, int amount) { + resource.emit("Increment by " + amount, effect(x -> x + amount)); + } + + /** + * Subtract one from the resource's value. + */ + public static void decrement(MutableResource> resource) { + decrement(resource, 1); + } + + /** + * Subtract the given amount from the resource's value. + */ + public static void decrement(MutableResource> resource, int amount) { + resource.emit("Decrement by " + amount, effect(x -> x - amount)); + } + + // General numeric resources + + /** + * Add amount to resource's value + */ + public static void increase(MutableResource> resource, double amount) { + resource.emit("Increase by " + amount, effect(x -> x + amount)); + } + + /** + * Subtract amount from resource's value + */ + public static void decrease(MutableResource> resource, double amount) { + resource.emit("Decrease by " + amount, effect(x -> x - amount)); + } + + // Queue style operations, mirroring the Queue interface + + /** + * Add element to the end of the queue resource + */ + public static void add(MutableResource>> resource, T element) { + resource.emit("Add %s to queue".formatted(element), effect(q -> { + var q$ = new LinkedList<>(q); + q$.add(element); + return q$; + })); + } + + /** + * Remove an element from the front of the queue resource. + *

+ * Returns that element, or empty if the queue was already empty. + *

+ */ + public static Optional remove(MutableResource>> resource) { + final var currentQueue = currentValue(resource); + if (currentQueue.isEmpty()) return Optional.empty(); + + final T result = currentQueue.get(currentQueue.size() - 1); + resource.emit("Remove %s from queue".formatted(result), effect(q -> { + var q$ = new LinkedList<>(q); + T purportedResult = q$.removeLast(); + if (!result.equals(purportedResult)) { + throw new IllegalStateException("Detected effect conflicting with queue remove operation"); + } + return q$; + })); + return Optional.of(result); + } + + // Consumable style operations + + /** + * Subtract the given amount from resource. + */ + public static void consume(MutableResource> resource, double amount) { + resource.emit("Consume " + amount, effect(x -> x - amount)); + } + + /** + * Add the given amount to resource. + */ + public static void restore(MutableResource> resource, double amount) { + resource.emit("Restore " + amount, effect(x -> x + amount)); + } + + // Non-consumable style operations + + /** + * Decrease resource by amount while action is running. + */ + public static void using(MutableResource> resource, double amount, Runnable action) { + consume(resource, amount); + action.run(); + restore(resource, amount); + } + + // Atomic style operations + + /** + * Decrease resource by one while action is running. + */ + public static void using(MutableResource> resource, Runnable action) { + decrement(resource); + action.run(); + increment(resource); + } + + // Unit-aware effects: + + // More convenient overload of "set" when using discrete dynamics + + /** + * Set the resource to the given value. + */ + public static
void set(UnitAware>> resource, UnitAware newValue) { + set(resource.value(), newValue.value(resource.unit())); + } + + // Consumable style operations + + /** + * Subtract the given amount from resource. + */ + public static void consume(UnitAware>> resource, UnitAware amount) { + consume(resource.value(), amount.value(resource.unit())); + } + + /** + * Add the given amount to resource. + */ + public static void restore(UnitAware>> resource, UnitAware amount) { + restore(resource.value(), amount.value(resource.unit())); + } + + // Non-consumable style operations + + /** + * Decrease resource by amount while action is running. + */ + public static void using(UnitAware>> resource, UnitAware amount, Runnable action) { + consume(resource, amount); + action.run(); + restore(resource, amount); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteResources.java new file mode 100644 index 0000000000..bc13f99814 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteResources.java @@ -0,0 +1,399 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete; + +import gov.nasa.jpl.aerie.contrib.streamline.core.*; +import gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.CommutativityTestInput; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteDynamicsMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.utils.DoubleUtils; +import gov.nasa.jpl.aerie.merlin.framework.Condition; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Unit; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAwareResources; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.function.BiPredicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects; +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.testing; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.expiry; +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.every; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.*; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.bind; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.pure; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.ClockResources.clock; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects.set; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad.*; +import static java.util.Arrays.stream; + +public final class DiscreteResources { + private DiscreteResources() {} + + public static Resource> constant(T value) { + var result = DiscreteResourceMonad.pure(value); + name(result, value.toString()); + return result; + } + + // General discrete cell resource constructor + public static MutableResource> discreteResource(T initialValue) { + return resource(discrete(initialValue)); + } + + // Annoyingly, we need to repeat the specialization for integer resources, so that + // discreteMutableResource(42) doesn't become a double resource, due to the next overload + public static MutableResource> discreteResource(int initialValue) { + return resource(discrete(initialValue)); + } + + // specialized constructor for doubles, because they require a toleranced equality comparison + public static MutableResource> discreteResource(double initialValue) { + return resource(discrete(initialValue), autoEffects(testing( + (CommutativityTestInput> input) -> DoubleUtils.areEqualResults( + input.original().extract(), + input.leftResult().extract(), + input.rightResult().extract())))); + } + + /** + * Returns a condition that's satisfied whenever this resource is true. + */ + public static Condition when(Resource> resource) { + Condition result = (positive, atEarliest, atLatest) -> + resource.getDynamics().match( + dynamics -> Optional.of(atEarliest).filter($ -> dynamics.data().extract() == positive), + error -> Optional.empty()); + name(result, "when %s", resource); + return result; + } + + /** + * Cache resource, updating the cache when updatePredicate(cached value, resource value) is true. + */ + public static Resource> cache(Resource> resource, BiPredicate updatePredicate) { + final var cell = resource(resource.getDynamics()); + // TODO: Does the update predicate need to propagate expiry information? + BiPredicate>>, ErrorCatching>>> liftedUpdatePredicate = (eCurrent, eNew) -> + eCurrent.match( + current -> eNew.match( + value -> updatePredicate.test(current.data().extract(), value.data().extract()), + newException -> true), + currentException -> eNew.match( + value -> true, + newException -> !equivalentExceptions(currentException, newException))); + whenever(() -> { + var currentDynamics = resource.getDynamics(); + return when(() -> DiscreteDynamicsMonad.pure(liftedUpdatePredicate.test( + currentDynamics, + resource.getDynamics()))); + }, () -> { + final var newDynamics = resource.getDynamics(); + cell.emit($ -> newDynamics); + }); + name(cell, "Cache (%s)", resource); + addDependency(cell, resource); + return cell; + } + + /** + * Sample valueSupplier once every samplePeriod. + */ + public static > Resource> sampled(Supplier valueSupplier, Resource samplePeriod) { + var result = discreteResource(valueSupplier.get()); + every(() -> currentValue(samplePeriod, Duration.MAX_VALUE), + () -> set(result, valueSupplier.get())); + return result; + } + + /** + * Returns a discrete resource that follows a precomputed sequence of values. + * Resource value is the value associated with the greatest key in segments not exceeding + * the current simulation time, or valueBeforeFirstEntry if every key exceeds current simulation time. + */ + public static Resource> precomputed( + final V valueBeforeFirstEntry, final NavigableMap segments) { + var clock = clock(); + return signalling(bind(clock, (Clock clock$) -> { + var t = clock$.extract(); + var entry = segments.floorEntry(t); + var value = entry == null ? valueBeforeFirstEntry : entry.getValue(); + var nextTime = expiry(Optional.ofNullable(segments.higherKey(t))); + return pure(expiring(discrete(value), nextTime.minus(t))); + })); + } + + /** + * Returns a discrete resource that follows a precomputed sequence of values. + * Resource value is the value associated with the greatest key in segments not exceeding + * the current simulation time, or valueBeforeFirstEntry if every key exceeds current simulation time. + */ + public static Resource> precomputed( + final V valueBeforeFirstEntry, final NavigableMap segments, final Instant simulationStartTime) { + var segmentsUsingDurationKeys = new TreeMap(); + for (var entry : segments.entrySet()) { + segmentsUsingDurationKeys.put( + Duration.of(ChronoUnit.MICROS.between(simulationStartTime, entry.getKey()), Duration.MICROSECONDS), + entry.getValue()); + } + return precomputed(valueBeforeFirstEntry, segmentsUsingDurationKeys); + } + + /** + * Add units to a discrete double resource. + */ + public static UnitAware>> unitAware(Resource> resource, Unit unit) { + return UnitAwareResources.unitAware(resource, unit, DiscreteResources::discreteScaling); + } + + /** + * Add units to a discrete double resource. + */ + public static UnitAware>> unitAware(MutableResource> resource, Unit unit) { + return UnitAwareResources.unitAware(resource, unit, DiscreteResources::discreteScaling); + } + + private static Discrete discreteScaling(Discrete d, Double scale) { + return DiscreteMonad.map(d, $ -> $ * scale); + } + + // Generally applicable derivations + + public static Resource> equals(Resource> left, Resource> right) { + var result = map(left, right, Objects::equals); + name(result, "(%s) == (%s)", left, right); + return result; + } + + public static Resource> notEquals(Resource> left, Resource> right) { + var result = not(equals(left, right)); + name(result, "(%s) != (%s)", left, right); + return result; + } + + // Boolean logic + + /** + * Short-circuiting logical "and" + */ + public static Resource> and(Resource> left, Resource> right) { + // Short-circuiting and: Only gets right if left is true + var result = choose(left, right, constant(false)); + name(result, "(%s) and (%s)", left, right); + return result; + } + + /** + * Reduce operands using short-circuiting logical "and" + */ + @SafeVarargs + public static Resource> all(Resource>... operands) { + return all(stream(operands)); + } + + /** + * Reduce operands using short-circuiting logical "and" + */ + public static Resource> all(Stream>> operands) { + // Reduce using the short-circuiting and to improve efficiency + return reduce(operands, constant(true), DiscreteResources::and, "All"); + } + + /** + * Short-circuiting logical "or" + */ + public static Resource> or(Resource> left, Resource> right) { + // Short-circuiting or: Only gets right if left is false + var result = choose(left, constant(true), right); + name(result, "(%s) or (%s)", left, right); + return result; + } + + /** + * Reduce operands using short-circuiting logical "or" + */ + @SafeVarargs + public static Resource> any(Resource>... operands) { + return any(stream(operands)); + } + + /** + * Reduce operands using short-circuiting logical "or" + */ + public static Resource> any(Stream>> operands) { + // Reduce using the short-circuiting or to improve efficiency + return reduce(operands, constant(false), DiscreteResources::or, "Any"); + } + + /** + * Logical "not" + */ + public static Resource> not(Resource> operand) { + var result = map(operand, $ -> !$); + name(result, "not (%s)", operand); + return result; + } + + /** + * Resource-level if-then-else logic. + */ + public static Resource choose(Resource> condition, Resource thenCase, Resource elseCase) { + var result = bind(condition, c -> c.extract() ? thenCase : elseCase); + // Manually add dependencies, since short-circuiting will break automatic dependency tracking. + addDependency(result, thenCase); + addDependency(result, elseCase); + name(result, "(%s) ? (%s) : (%s)", condition, thenCase, elseCase); + return result; + } + + /** + * Assert that this resource is always true. + * Otherwise, this resource fails. + * Register this resource to detect that failure. + */ + public static Resource> assertThat(String description, Resource> assertion) { + var result = map(assertion, a -> { + if (a) return true; + throw new AssertionError(description); + }); + name(result, "Assertion: " + description); + return result; + } + + // Integer arithmetic + + /** + * Add integer resources + */ + @SafeVarargs + public static Resource> addInt(Resource>... operands) { + return sumInt(Arrays.stream(operands)); + } + + /** + * Add integer resources + */ + public static Resource> sumInt(Stream>> operands) { + return reduce(operands, constant(0), map(Integer::sum), "Sum"); + } + + /** + * Subtract integer resources + */ + public static Resource> subtractInt(Resource> left, Resource> right) { + var result = map(left, right, (l, r) -> l - r); + name(result, "(%s) - (%s)", left, right); + return result; + } + + /** + * Multiply integer resources + */ + @SafeVarargs + public static Resource> multiplyInt(Resource>... operands) { + return productInt(Arrays.stream(operands)); + } + + /** + * Multiply integer resources + */ + public static Resource> productInt(Stream>> operands) { + return reduce(operands, constant(1), map((x, y) -> x * y), "Product"); + } + + /** + * Divide integer resources + */ + public static Resource> divideInt(Resource> left, Resource> right) { + var result = map(left, right, (l, r) -> l / r); + name(result, "(%s) / (%s)", left, right); + return result; + } + + // Double arithmetic + + /** + * Add double resources + */ + @SafeVarargs + public static Resource> add(Resource>... operands) { + return sum(Arrays.stream(operands)); + } + + /** + * Add double resources + */ + public static Resource> sum(Stream>> operands) { + return reduce(operands, constant(0.0), map(Double::sum), "Sum"); + } + + /** + * Subtract double resources + */ + public static Resource> subtract(Resource> left, Resource> right) { + var result = map(left, right, (l, r) -> l - r); + name(result, "(%s) - (%s)", left, right); + return result; + } + + /** + * Multiply double resources + */ + @SafeVarargs + public static Resource> multiply(Resource>... operands) { + return product(Arrays.stream(operands)); + } + + /** + * Multiply double resources + */ + public static Resource> product(Stream>> operands) { + return reduce(operands, constant(1.0), map((x, y) -> x * y), "Product"); + } + + /** + * Divide double resources + */ + public static Resource> divide(Resource> left, Resource> right) { + var result = map(left, right, (l, r) -> l / r); + name(result, "(%s) / (%s)", left, right); + return result; + } + + // Collections + + /** + * Returns a resource that's true when the argument is empty + */ + public static > Resource> isEmpty(Resource> resource) { + var result = map(resource, Collection::isEmpty); + name(result, "(%s) is empty", resource); + return result; + } + + /** + * Returns a resource that's true when the argument is non-empty + */ + public static > Resource> isNonEmpty(Resource> resource) { + var result = not(isEmpty(resource)); + name(result, "(%s) is not empty", resource); + return result; + } + + public static > Resource> contains(Resource> collection, Resource> value) { + var result = map(collection, value, Collection::contains); + name(result, "(%s) contains (%s)", collection, value); + return result; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteDynamicsMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteDynamicsMonad.java new file mode 100644 index 0000000000..b055fe6f52 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteDynamicsMonad.java @@ -0,0 +1,520 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.DynamicsEffect; +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +public final class DiscreteDynamicsMonad { + private DiscreteDynamicsMonad() {} + + public static ErrorCatching>> pure(A a) { + return DynamicsMonad.pure(DiscreteMonad.pure(a)); + } + + public static ErrorCatching>> apply(ErrorCatching>> a, ErrorCatching>>> f) { + return DynamicsMonad.apply(a, DynamicsMonad.map(f, DiscreteMonad::apply)); + } + + private static ErrorCatching>> distribute(Discrete>> a) { + return DynamicsMonad.map(a.extract(), DiscreteMonad::pure); + } + + public static ErrorCatching>> join(ErrorCatching>>>>> a) { + return DynamicsMonad.map(DynamicsMonad.join(DynamicsMonad.map(a, DiscreteDynamicsMonad::distribute)), DiscreteMonad::join); + } + + // Not monadic, strictly speaking, but useful nonetheless. + + public static DynamicsEffect> effect(Function f) { + return DynamicsMonad.effect(DiscreteMonad.map(f)); + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function>>, ErrorCatching>>> apply(ErrorCatching>>> f) { + return a -> apply(a, f); + } + + public static ErrorCatching>> map(ErrorCatching>> a, Function f) { + return apply(a, pure(f)); + } + + public static Function>>, ErrorCatching>>> map(Function f) { + return apply(pure(f)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, Function>>> f) { + return join(map(a, f)); + } + + public static Function>>, ErrorCatching>>> bind(Function>>> f) { + return a -> bind(a, f); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction>>, ErrorCatching>>, ErrorCatching>>> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, BiFunction>>> function) { + return join(map(a, b, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, Function>>>> function) { + return join(map(a, b, function)); + } + + public static BiFunction>>, ErrorCatching>>, ErrorCatching>>> bind(BiFunction>>> function) { + return (a, b) -> bind(a, b, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, TriFunction>>> function) { + return join(map(a, b, c, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, Function>>>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(TriFunction>>> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, Function4>>> function) { + return join(map(a, b, c, d, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, Function>>>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function4>>> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, Function5>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, Function>>>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function5>>> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, Function6>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function6>>> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, Function7>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function7>>> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, Function8>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function8>>> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, Function9>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function9>>> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, Function10>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function10>>> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, Function11>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function11>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, Function12>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function12>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, Function13>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function13>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, Function14>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function14>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, Function15>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function15>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, Function16>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function16>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, Function17>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function17>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, Function18>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function18>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, Function19>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, Function>>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function19>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, ErrorCatching>> t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, ErrorCatching>> t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, ErrorCatching>> t, Function20>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static ErrorCatching>> bind(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, ErrorCatching>> t, Function>>>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> bind(Function20>>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteMonad.java new file mode 100644 index 0000000000..ba1abc6152 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteMonad.java @@ -0,0 +1,510 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +/** + * {@link Discrete} monad + */ +public final class DiscreteMonad { + private DiscreteMonad() {} + + public static Discrete pure(A a) { + return discrete(a); + } + + public static Discrete apply(Discrete a, Discrete> f) { + return discrete(f.extract().apply(a.extract())); + } + + public static Discrete join(Discrete> a) { + return a.extract(); + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function, Discrete> apply(Discrete> f) { + return a -> apply(a, f); + } + + public static Discrete map(Discrete a, Function f) { + return apply(a, pure(f)); + } + + public static Function, Discrete> map(Function f) { + return apply(pure(f)); + } + + public static Discrete bind(Discrete a, Function> f) { + return join(map(a, f)); + } + + public static Function, Discrete> bind(Function> f) { + return a -> bind(a, f); + } + + public static Discrete map(Discrete a, Discrete b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction, Discrete, Discrete> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static Discrete bind(Discrete a, Discrete b, BiFunction> function) { + return join(map(a, b, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Function>> function) { + return join(map(a, b, function)); + } + + public static BiFunction, Discrete, Discrete> bind(BiFunction> function) { + return (a, b) -> bind(a, b, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction, Discrete, Discrete, Discrete> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, TriFunction> function) { + return join(map(a, b, c, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Function>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction, Discrete, Discrete, Discrete> bind(TriFunction> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4, Discrete, Discrete, Discrete, Discrete> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Function4> function) { + return join(map(a, b, c, d, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Function>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4, Discrete, Discrete, Discrete, Discrete> bind(Function4> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Function5> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Function>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function5> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Function6> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Function>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function6> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Function7> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function7> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Function8> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function8> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Function9> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function9> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Function10> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function10> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Function11> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function11> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Function12> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function12> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Function13> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function13> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Function14> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function14> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Function15> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function15> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Function16> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete> bind(Function16> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Function17> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete> bind(Function17> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete, Discrete> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Function18> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete, Discrete> bind(Function18> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete, Discrete, Discrete> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Function19> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete, Discrete, Discrete> bind(Function19> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Discrete t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Discrete t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete, Discrete, Discrete, Discrete> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Discrete t, Function20> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Discrete bind(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Discrete q, Discrete r, Discrete s, Discrete t, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete, Discrete

, Discrete, Discrete, Discrete, Discrete, Discrete> bind(Function20> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteResourceMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteResourceMonad.java new file mode 100644 index 0000000000..8c22b34f57 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/monads/DiscreteResourceMonad.java @@ -0,0 +1,512 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +public final class DiscreteResourceMonad { + private DiscreteResourceMonad() {} + + public static Resource> pure(A a) { + return ResourceMonad.pure(DiscreteMonad.pure(a)); + } + + public static Resource> apply(Resource> a, Resource>> f) { + return ResourceMonad.apply(a, ResourceMonad.map(f, DiscreteMonad::apply)); + } + + private static Resource> distribute(Discrete> a) { + return ResourceMonad.map(a.extract(), DiscreteMonad::pure); + } + + public static Resource> join(Resource>>> a) { + return ResourceMonad.map(ResourceMonad.join(ResourceMonad.map(a, DiscreteResourceMonad::distribute)), DiscreteMonad::join); + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function>, Resource>> apply(Resource>> f) { + return a -> apply(a, f); + } + + public static Resource> map(Resource> a, Function f) { + return apply(a, pure(f)); + } + + public static Function>, Resource>> map(Function f) { + return apply(pure(f)); + } + + public static Resource> bind(Resource> a, Function>> f) { + return join(map(a, f)); + } + + public static Function>, Resource>> bind(Function>> f) { + return a -> bind(a, f); + } + + public static Resource> map(Resource> a, Resource> b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction>, Resource>, Resource>> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static Resource> bind(Resource> a, Resource> b, BiFunction>> function) { + return join(map(a, b, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Function>>> function) { + return join(map(a, b, function)); + } + + public static BiFunction>, Resource>, Resource>> bind(BiFunction>> function) { + return (a, b) -> bind(a, b, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction>, Resource>, Resource>, Resource>> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, TriFunction>> function) { + return join(map(a, b, c, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Function>>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction>, Resource>, Resource>, Resource>> bind(TriFunction>> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4>, Resource>, Resource>, Resource>, Resource>> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Function4>> function) { + return join(map(a, b, c, d, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Function>>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4>, Resource>, Resource>, Resource>, Resource>> bind(Function4>> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Function5>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Function>>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function5>> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Function6>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function6>> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Function7>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function7>> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Function8>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function8>> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Function9>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function9>> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Function10>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function10>> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Function11>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function11>> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Function12>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function12>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Function13>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function13>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Function14>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function14>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Function15>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function15>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Function16>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function16>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Function17>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function17>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Function18>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function18>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Function19>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function19>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Resource> t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Resource> t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Resource> t, Function20>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Resource> bind(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Resource> t, Function>>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> bind(Function20>> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/DoubleUtils.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/DoubleUtils.java new file mode 100644 index 0000000000..2e50017fb6 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/utils/DoubleUtils.java @@ -0,0 +1,14 @@ +package gov.nasa.jpl.aerie.contrib.streamline.utils; + +public final class DoubleUtils { + private DoubleUtils() {} + + /** + * Relative error tolerance for fuzzy equality on doubles. + */ + public static final double FUZZY_EQUALITY_TOLERANCE = 1e-14; + + public static boolean areEqualResults(double original, double x, double y) { + return Math.abs(x - y) <= Math.max(Math.abs(original), Math.max(Math.abs(x), Math.abs(y))) * FUZZY_EQUALITY_TOLERANCE; + } +} From 4ffd89e9c8c1d924e32bf1128c20d5ab33e21024 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 17:44:21 -0800 Subject: [PATCH 021/159] Add the Linear dynamics type Adds the Linear dynamics type, the analog of Merlin's Real type. This type is named Linear rather than Real, to distinguish it from Polynomial or other potentially real-valued dynamics types. --- .../streamline/modeling/linear/Linear.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/linear/Linear.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/linear/Linear.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/linear/Linear.java new file mode 100644 index 0000000000..5cb52fce44 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/linear/Linear.java @@ -0,0 +1,20 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.linear; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.Objects; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; + +// TODO: Implement better support for going to/from Linear +public record Linear(Double extract, Double rate) implements Dynamics { + @Override + public Linear step(Duration t) { + return linear(extract() + t.ratioOver(SECOND) * rate(), rate()); + } + + public static Linear linear(double value, double rate) { + return new Linear(value, rate); + } +} From 95ef136b64e99f40fc1d403767dcb795c25b9565 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 17:45:54 -0800 Subject: [PATCH 022/159] Add clock dynamics types Adds Clock and VariableClock dynamics types, both of which use Durations to exactly represent time. Using Duration avoids floating-point issues that crop up when using Linear or Polynomial resource to represent time, as well as not needing conversions to and from Duration. Of particular note are the stopwatch-style effects and resource-level comparison functions for VariableClock. Combining these with `DiscreteResources.when` and `Reactions` is especially useful for expressing time-based conditions and behaviors. --- .../streamline/modeling/clocks/Clock.java | 15 +++++ .../modeling/clocks/ClockEffects.java | 18 ++++++ .../modeling/clocks/ClockResources.java | 56 +++++++++++++++++ .../modeling/clocks/VariableClock.java | 29 +++++++++ .../modeling/clocks/VariableClockEffects.java | 39 ++++++++++++ .../clocks/VariableClockResources.java | 60 +++++++++++++++++++ 6 files changed, 217 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/Clock.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockEffects.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockResources.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClock.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockEffects.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockResources.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/Clock.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/Clock.java new file mode 100644 index 0000000000..8258d4576a --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/Clock.java @@ -0,0 +1,15 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +public record Clock(Duration extract) implements Dynamics { + @Override + public Clock step(Duration t) { + return clock(extract().plus(t)); + } + + public static Clock clock(Duration startingTime) { + return new Clock(startingTime); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockEffects.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockEffects.java new file mode 100644 index 0000000000..7d5ccf7463 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockEffects.java @@ -0,0 +1,18 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.effect; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock.clock; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + +public final class ClockEffects { + private ClockEffects() {} + + /** + * Reset clock to zero elapsed time. + */ + public static void restart(MutableResource stopwatch) { + stopwatch.emit("Restart", effect(c -> clock(ZERO))); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockResources.java new file mode 100644 index 0000000000..9b54d2ac8f --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/ClockResources.java @@ -0,0 +1,56 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.*; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.signalling; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.bind; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.not; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad.map; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.EPSILON; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + +public final class ClockResources { + private ClockResources() {} + + /** + * Create a clock starting at zero time. + */ + public static Resource clock() { + return resource(Clock.clock(ZERO)); + } + + public static Resource> lessThan(Resource clock, Resource> threshold) { + return signalling(bind(clock, threshold, (Clock c, Discrete t) -> { + final Duration crossoverTime = t.extract().minus(c.extract()); + return ResourceMonad.pure( + crossoverTime.isPositive() + ? expiring(discrete(true), crossoverTime) + : neverExpiring(discrete(false))); + })); + } + + public static Resource> lessThanOrEquals(Resource clock, Resource> threshold) { + // Since Duration is an integral type, implement strictness through EPSILON stepping + return lessThan(clock, map(threshold, EPSILON::plus)); + } + + public static Resource> greaterThan(Resource clock, Resource> threshold) { + return not(lessThanOrEquals(clock, threshold)); + } + + public static Resource> greaterThanOrEquals(Resource clock, Resource> threshold) { + return not(lessThan(clock, threshold)); + } + + public static Resource asLinear(Resource clock, Duration unit) { + return VariableClockResources.asLinear(VariableClockResources.asVariableClock(clock), unit); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClock.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClock.java new file mode 100644 index 0000000000..e40abdaf99 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClock.java @@ -0,0 +1,29 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + +public record VariableClock(Duration extract, int multiplier) implements Dynamics { + @Override + public VariableClock step(final Duration t) { + return new VariableClock(extract.plus(t.times(multiplier)), multiplier); + } + + public static VariableClock runningStopwatch() { + return runningStopwatch(ZERO); + } + + public static VariableClock runningStopwatch(Duration time) { + return new VariableClock(time, 1); + } + + public static VariableClock pausedStopwatch() { + return pausedStopwatch(ZERO); + } + + public static VariableClock pausedStopwatch(Duration time) { + return new VariableClock(time, 0); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockEffects.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockEffects.java new file mode 100644 index 0000000000..b14425b020 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockEffects.java @@ -0,0 +1,39 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.effect; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.VariableClock.*; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + +public final class VariableClockEffects { + private VariableClockEffects() {} + + /** + * Stop the clock without affecting the current time. + */ + public static void pause(MutableResource stopwatch) { + stopwatch.emit("Pause", effect(c -> pausedStopwatch(c.extract()))); + } + + /** + * Start the clock without affecting the current time. + */ + public static void start(MutableResource stopwatch) { + stopwatch.emit("Start", effect(c -> runningStopwatch(c.extract()))); + } + + /** + * Stop the clock and reset the time to zero. + */ + public static void reset(MutableResource stopwatch) { + stopwatch.emit("Reset", effect(c -> pausedStopwatch(ZERO))); + } + + /** + * Start the clock and reset the time to zero. + */ + public static void restart(MutableResource stopwatch) { + stopwatch.emit("Restart", effect(c -> runningStopwatch(ZERO))); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockResources.java new file mode 100644 index 0000000000..7727fceb72 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/clocks/VariableClockResources.java @@ -0,0 +1,60 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiry; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.signalling; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.bind; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.map; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.not; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear.linear; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.EPSILON; + +public final class VariableClockResources { + private VariableClockResources() {} + + public static Resource> lessThan(Resource clock, Resource> threshold) { + // Since Duration is an integral type, implement strictness through EPSILON stepping + return lessThanOrEquals(clock, DiscreteResourceMonad.map(threshold, t -> t.minus(EPSILON))); + } + + public static Resource> lessThanOrEquals(Resource clock, Resource> threshold) { + return signalling(bind(clock, threshold, (VariableClock c, Discrete t) -> { + final boolean result = c.extract().shorterThan(t.extract()); + // If multiplier is zero, or direction of clock is away from threshold, never expires. + final Expiry expiry; + if (c.multiplier() == 0 || (result == c.multiplier() < 0)) { + expiry = Expiry.NEVER; + } else { + // ceil( (h - c) / k ) = floor( (h - c - 1) / k ) + 1, where EPSILON = 1 and dividedBy does floor( ... / ... ) + // Define T = h - 1, where h = threshold + 1 if result, or threshold itself if not. + var T = result ? t.extract() : t.extract().minus(EPSILON); + expiry = Expiry.at(T.minus(c.extract()).dividedBy(c.multiplier()).plus(EPSILON)); + } + return ResourceMonad.pure(expiring(discrete(result), expiry)); + })); + } + + public static Resource> greaterThan(Resource clock, Resource> threshold) { + return not(lessThanOrEquals(clock, threshold)); + } + + public static Resource> greaterThanOrEquals(Resource clock, Resource> threshold) { + return not(lessThan(clock, threshold)); + } + + public static Resource asLinear(Resource clock, Duration unit) { + return map(clock, c -> linear(c.extract().ratioOver(unit), c.multiplier())); + } + + public static Resource asVariableClock(Resource clock) { + return map(clock, c -> new VariableClock(c.extract(), 1)); + } +} From 8048e00b9646225f540be20ea97c99023a793700 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 17:54:29 -0800 Subject: [PATCH 023/159] Add polynomial resources. Adds polynomial resources, which are the primary non-discrete resource type. Of particular note are the arithmetic and comparison functions for derivations to and from polynomials. These functions incorporate higher-order coefficients correctly, including performing root-finding to calculate when comparisons will expire. --- .../modeling/polynomial/Polynomial.java | 295 +++++++ .../polynomial/PolynomialEffects.java | 288 +++++++ .../polynomial/PolynomialResources.java | 755 ++++++++++++++++++ 3 files changed, 1338 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialEffects.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java new file mode 100644 index 0000000000..ab8d3cb541 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java @@ -0,0 +1,295 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiry; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ExpiringMonad; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import org.apache.commons.math3.analysis.solvers.LaguerreSolver; +import org.apache.commons.math3.complex.Complex; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.DoublePredicate; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.NEVER; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.expiry; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.EPSILON; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static org.apache.commons.math3.analysis.polynomials.PolynomialsUtils.shift; + +public record Polynomial(double[] coefficients) implements Dynamics { + + // TODO: Add Duration parameter for unit of formal parameter? + public static Polynomial polynomial(double... coefficients) { + int n = coefficients.length; + if (n == 0) { + return new Polynomial(new double[] { 0.0 }); + } + while (n > 1 && coefficients[n - 1] == 0) --n; + for (int m = 0; m < n; ++m) { + // Any NaN coefficient invalidates the whole polynomial + if (Double.isNaN(coefficients[m])) return new Polynomial(new double[] { Double.NaN }); + // Infinite coefficients invalidate later terms + if (Double.isInfinite(coefficients[m])) { + n = m + 1; + break; + } + } + return new Polynomial(Arrays.copyOf(coefficients, n)); + } + + @Override + public Double extract() { + return coefficients()[0]; + } + + @Override + public Polynomial step(Duration t) { + return t.isEqualTo(ZERO) ? this : polynomial(shift(coefficients(), t.ratioOver(SECOND))); + } + + public int degree() { + return coefficients().length - 1; + } + + public boolean isConstant() { + return degree() == 0; + } + + public boolean isNonFinite() { + return !Double.isFinite(coefficients[degree()]); + } + + public Polynomial add(Polynomial other) { + final double[] coefficients = coefficients(); + final double[] otherCoefficients = other.coefficients(); + final int minLength = Math.min(coefficients.length, otherCoefficients.length); + final int maxLength = Math.max(coefficients.length, otherCoefficients.length); + final double[] newCoefficients = new double[maxLength]; + for (int i = 0; i < minLength; ++i) { + newCoefficients[i] = coefficients[i] + otherCoefficients[i]; + } + if (coefficients.length > minLength) + System.arraycopy(coefficients, minLength, newCoefficients, minLength, coefficients.length - minLength); + if (otherCoefficients.length > minLength) + System.arraycopy( + otherCoefficients, minLength, newCoefficients, minLength, otherCoefficients.length - minLength); + return polynomial(newCoefficients); + } + + public Polynomial subtract(Polynomial other) { + return add(other.multiply(polynomial(-1))); + } + + public Polynomial multiply(Polynomial other) { + final double[] coefficients = coefficients(); + final double[] otherCoefficients = other.coefficients(); + // Length = degree + 1, so + // new length = 1 + new degree + // = 1 + (degree + other.degree) + // = 1 + (length - 1 + other.length - 1) + // = length + other.length - 1 + final double[] newCoefficients = new double[coefficients.length + otherCoefficients.length - 1]; + for (int exponent = 0; exponent < newCoefficients.length; ++exponent) { + newCoefficients[exponent] = 0.0; + // 0 <= k < length and 0 <= exponent - k < other.length + // implies k >= 0, k > exponent - other.length, + // k < length, and k <= exponent + for (int k = Math.max(0, exponent - otherCoefficients.length + 1); + k < Math.min(coefficients.length, exponent + 1); + ++k) { + newCoefficients[exponent] += coefficients[k] * otherCoefficients[exponent - k]; + } + } + return polynomial(newCoefficients); + } + + public Polynomial divide(double scalar) { + final double[] coefficients = coefficients(); + final double[] newCoefficients = new double[coefficients.length]; + for (int i = 0; i < coefficients.length; ++i) { + newCoefficients[i] = coefficients[i] / scalar; + } + return polynomial(newCoefficients); + } + + public Polynomial integral(double startingValue) { + final double[] coefficients = coefficients(); + final double[] newCoefficients = new double[coefficients.length + 1]; + newCoefficients[0] = startingValue; + for (int i = 0; i < coefficients.length; ++i) { + newCoefficients[i + 1] = coefficients[i] / (i + 1); + } + return polynomial(newCoefficients); + } + + public Polynomial derivative() { + final double[] coefficients = coefficients(); + final double[] newCoefficients = new double[coefficients.length - 1]; + for (int i = 1; i < coefficients.length; ++i) { + newCoefficients[i - 1] = coefficients[i] * i; + } + return polynomial(newCoefficients); + } + + public double evaluate(Duration t) { + // Although there are more efficient ways to evaluate a polynomial, + // it's *very* important to simulation stability that + // evaluate(t) agrees exactly with step(t).extract() + return step(t).extract(); + } + + /** + * Helper method for other comparison methods. + * Finds the first time the predicate is true, near the next root of this polynomial. + */ + private Expiry findExpiryNearRoot(Predicate expires) { + Duration root, start, end; + try { + var t$ = findFutureRoots().findFirst(); + if (t$.isEmpty()) return NEVER; + root = t$.get(); + + // Do an exponential search to bracket the transition time + boolean initialTestResult = expires.test(root); + Duration rangeSize = EPSILON.times(initialTestResult ? -1 : 1); + Duration testPoint = root.plus(rangeSize); + while (expires.test(testPoint) == initialTestResult) { + rangeSize = rangeSize.times(2); + testPoint = root.plus(rangeSize); + } + if (initialTestResult) { + start = testPoint; + end = root; + } else { + start = root; + end = testPoint; + } + + // TODO: There's an unhandled edge case here, where timePredicate is satisfied in a period we jumped over. + // Maybe try to use the precision of the arguments and the finer resolution polynomial "this" + // to do a more thorough but still efficient search? + } catch (ArithmeticException e) { + // If we overflowed looking for a bracketing range, it effectively never transitions. + return NEVER; + } + + // Do a binary search to find the exact transition time + while (end.longerThan(start.plus(EPSILON))) { + Duration midpoint = start.plus(end).dividedBy(2); + if (expires.test(midpoint)) { + end = midpoint; + } else { + start = midpoint; + } + } + return Expiry.at(end); + } + + private Expiring> greaterThan(Polynomial other, boolean strict) { + BiPredicate comp = strict ? (x, y) -> x > y : (x, y) -> x >= y; + boolean result = comp.test(this.extract(), other.extract()); + var expiry = this.subtract(other).findExpiryNearRoot( + t -> comp.test(this.evaluate(t), other.evaluate(t)) != result); + return expiring(discrete(result), expiry); + } + + public Expiring> greaterThan(Polynomial other) { + return greaterThan(other, true); + } + + public Expiring> greaterThanOrEquals(Polynomial other) { + return greaterThan(other, false); + } + + public Expiring> lessThan(Polynomial other) { + return other.greaterThan(this); + } + + public Expiring> lessThanOrEquals(Polynomial other) { + return other.greaterThanOrEquals(this); + } + + private boolean dominates$(Polynomial other) { + for (int i = 0; i <= Math.max(this.degree(), other.degree()); ++i) { + if (this.getCoefficient(i) > other.getCoefficient(i)) return true; + if (this.getCoefficient(i) < other.getCoefficient(i)) return false; + } + // Equal, so either answer is correct + return true; + } + + private Expiring> dominates(Polynomial other) { + boolean result = this.dominates$(other); + var expiry = this.subtract(other).findExpiryNearRoot(t -> this.step(t).dominates$(other.step(t)) != result); + return expiring(discrete(result), expiry); + } + + public Expiring min(Polynomial other) { + return ExpiringMonad.map(this.dominates(other), d -> d.extract() ? other : this); + } + + public Expiring max(Polynomial other) { + return ExpiringMonad.map(this.dominates(other), d -> d.extract() ? this : other); + } + + /** + * Finds all roots of this function in the future + */ + private Stream findFutureRoots() { + // If this polynomial can never have a root, fail immediately + if (this.isNonFinite() || this.isConstant()) { + return Stream.empty(); + } + + // Defining epsilon keeps the Laguerre solver fast and stable for poorly-behaved polynomials. + final double epsilon = 2 * Arrays.stream(coefficients).map(Math::ulp).max().orElseThrow(); + final Complex[] solutions = new LaguerreSolver(0, ABSOLUTE_ACCURACY_FOR_DURATIONS, epsilon) + .solveAllComplex(coefficients, 0); + return Arrays.stream(solutions) + .filter(solution -> Math.abs(solution.getImaginary()) < epsilon) + .map(Complex::getReal) + .filter(t -> t >= 0 && t <= MAX_SECONDS_FOR_DURATION) + .sorted() + .map(t -> Duration.roundNearest(t, SECOND)); + } + private static final double ABSOLUTE_ACCURACY_FOR_DURATIONS = EPSILON.ratioOver(SECOND); + private static final double MAX_SECONDS_FOR_DURATION = Duration.MAX_VALUE.ratioOver(SECOND); + + /** + * Get the nth coefficient. + * @param n the n. + * @return the nth coefficient + */ + public double getCoefficient(int n) { + return n >= coefficients().length ? 0.0 : coefficients()[n]; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Polynomial that = (Polynomial) o; + return Arrays.equals(coefficients, that.coefficients); + } + + @Override + public int hashCode() { + return Arrays.hashCode(coefficients); + } + + @Override + public String toString() { + return "Polynomial{" + + "coefficients=" + Arrays.toString(coefficients) + + '}'; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialEffects.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialEffects.java new file mode 100644 index 0000000000..45362d221e --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialEffects.java @@ -0,0 +1,288 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.StandardUnits; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware; + +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.replaying; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.spawn; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.effect; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; + +public final class PolynomialEffects { + private PolynomialEffects() {} + + // Consumable style operations + + /** + * Consume some amount of a resource instantaneously. + */ + public static void consume(MutableResource resource, double amount) { + resource.emit( + "Consume %.1e discretely".formatted(amount), + effect($ -> $.subtract(polynomial(amount)))); + } + + /** + * Consume resource according to a given polynomial profile while an action runs. + */ + public static void consuming(MutableResource resource, Polynomial profile, Runnable action) { + withConsumableEffects("consuming", resource, profile, action); + } + + /** + * Consume some amount of a resource at a uniform rate over a fixed period of time. + */ + public static void consumeUniformly(MutableResource resource, double amount, Duration time) { + consume(resource, amount / time.ratioOver(SECOND), time); + } + + /** + * Consume some resource a fixed rate during an action + */ + public static void consuming(MutableResource resource, double rate, Runnable action) { + consuming(resource, polynomial(0, rate), action); + } + + /** + * Consume some resource at a fixed rate for a fixed period of time, asynchronously. + */ + public static void consume(MutableResource resource, double rate, Duration time) { + spawn(replaying(() -> consuming(resource, rate, () -> delay(time)))); + } + + /** + * Restore resource according to a given polynomial profile while an action runs. + */ + public static void restoring(MutableResource resource, Polynomial profile, Runnable action) { + withConsumableEffects("restoring", resource, profile.multiply(polynomial(-1)), action); + } + + /** + * Consume some amount of a resource instantaneously. + */ + public static void restore(MutableResource resource, double amount) { + resource.emit( + "Restore %.1e discretely".formatted(amount), + effect($ -> $.add(polynomial(amount)))); + } + + /** + * Restore some amount of a resource at a uniform rate over a fixed period of time. + */ + public static void restoreUniformly(MutableResource resource, double amount, Duration time) { + restore(resource, amount / time.ratioOver(SECOND), time); + } + + /** + * Restore some resource a fixed rate during an action + */ + public static void restoring(MutableResource resource, double rate, Runnable action) { + restoring(resource, polynomial(0, rate), action); + } + + /** + * Restore some resource at a fixed rate for a fixed period of time, asynchronously. + */ + public static void restore(MutableResource resource, double rate, Duration time) { + spawn(replaying(() -> restoring(resource, rate, () -> delay(time)))); + } + + private static void withConsumableEffects(String verb, MutableResource resource, Polynomial profile, Runnable action) { + resource.emit("Start %s according to profile %s".formatted(verb, profile), effect($ -> $.subtract(profile))); + final Duration start = Resources.currentTime(); + action.run(); + final Duration elapsedTime = Resources.currentTime().minus(start); + // Nullify ongoing effects by adding a profile with the same behavior, + // but with an initial value of 0 + final Polynomial steppedProfile = profile.step(elapsedTime); + final Polynomial counteractingProfile = steppedProfile.subtract(polynomial(steppedProfile.extract())); + resource.emit("End %s according to profile %s".formatted(verb, profile), effect($ -> $.add(counteractingProfile))); + } + + // Non-consumable style operations + + /** + * Decrease resource according to a given polynomial profile while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void using(MutableResource resource, Polynomial profile, Runnable action) { + withNonConsumableEffect("using", resource, profile, action); + } + + /** + * Decrease resource by a fixed amount while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void using(MutableResource resource, double amount, Runnable action) { + using(resource, polynomial(amount), action); + } + + /** + * Decrease resource by a fixed amount for a fixed time, + * restoring the resource to its original profile when the action completes. + */ + public static void using(MutableResource resource, double amount, Duration time) { + spawn(replaying(() -> using(resource, amount, () -> delay(time)))); + } + + /** + * Increase resource according to a given polynomial profile while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void providing(MutableResource resource, Polynomial profile, Runnable action) { + withNonConsumableEffect("providing", resource, profile.multiply(polynomial(-1)), action); + } + + /** + * Increase resource by a fixed amount while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void providing(MutableResource resource, double amount, Runnable action) { + providing(resource, polynomial(amount), action); + } + + /** + * Increase resource by a fixed amount for a fixed time, + * restoring the resource to its original profile when the action completes. + */ + public static void providing(MutableResource resource, double amount, Duration time) { + spawn(replaying(() -> providing(resource, amount, () -> delay(time)))); + } + + private static void withNonConsumableEffect(String verb, MutableResource resource, Polynomial profile, Runnable action) { + resource.emit("Start %s profile %s".formatted(verb, profile), effect($ -> $.subtract(profile))); + final Duration start = Resources.currentTime(); + action.run(); + final Duration elapsedTime = Resources.currentTime().minus(start); + // Reset by adding a counteracting profile + final Polynomial counteractingProfile = profile.step(elapsedTime); + resource.emit("Finish %s profile %s".formatted(verb, profile), effect($ -> $.add(counteractingProfile))); + } + + // Consumable style operations + + /** + * Consume some amount of a resource instantaneously. + */ + public static void consume(UnitAware> resource, UnitAware amount) { + consume(resource.value(), amount.value(resource.unit())); + } + + /** + * Consume resource according to a given polynomial profile while an action runs. + */ + public static void consuming$(UnitAware> resource, UnitAware profile, Runnable action) { + consuming(resource.value(), profile.value(resource.unit()), action); + } + + /** + * Consume some amount of a resource at a uniform rate over a fixed period of time. + */ + public static void consumeUniformly(UnitAware> resource, UnitAware amount, Duration time) { + consumeUniformly(resource.value(), amount.value(resource.unit()), time); + } + + /** + * Consume some resource a fixed rate during an action + */ + public static void consuming(UnitAware> resource, UnitAware rate, Runnable action) { + consuming(resource.value(), rate.value(resource.unit().divide(StandardUnits.SECOND)), action); + } + + /** + * Consume some resource at a fixed rate for a fixed period of time, asynchronously. + */ + public static void consume(UnitAware> resource, UnitAware rate, Duration time) { + spawn(replaying(() -> consuming(resource, rate, () -> delay(time)))); + } + + /** + * Restore resource according to a given polynomial profile while an action runs. + */ + public static void restoring$(UnitAware> resource, UnitAware profile, Runnable action) { + restoring(resource.value(), profile.value(resource.unit()), action); + } + + /** + * Restore some amount of a resource instantaneously. + */ + public static void restore(UnitAware> resource, UnitAware amount) { + restore(resource.value(), amount.value(resource.unit())); + } + + /** + * Restore some amount of a resource at a uniform rate over a fixed period of time. + */ + public static void restoreUniformly(UnitAware> resource, UnitAware amount, Duration time) { + restoreUniformly(resource.value(), amount.value(resource.unit()), time); + } + + /** + * Restore some resource a fixed rate during an action + */ + public static void restoring(UnitAware> resource, UnitAware rate, Runnable action) { + restoring(resource.value(), rate.value(resource.unit().divide(StandardUnits.SECOND)), action); + } + + /** + * Restore some resource at a fixed rate for a fixed period of time, asynchronously. + */ + public static void restore(UnitAware> resource, UnitAware rate, Duration time) { + restore(resource.value(), rate.value(resource.unit()), time); + } + + // Non-consumable style operations + + /** + * Decrease resource according to a given polynomial profile while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void using$(UnitAware> resource, UnitAware profile, Runnable action) { + using(resource.value(), profile.value(resource.unit()), action); + } + + /** + * Decrease resource by a fixed amount while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void using(UnitAware> resource, UnitAware amount, Runnable action) { + using(resource.value(), amount.value(resource.unit()), action); + } + + /** + * Decrease resource by a fixed amount for a fixed time, + * restoring the resource to its original profile when the action completes. + */ + public static void using(UnitAware> resource, UnitAware amount, Duration time) { + using(resource.value(), amount.value(resource.unit()), time); + } + + /** + * Increase resource according to a given polynomial profile while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void providing$(UnitAware> resource, UnitAware profile, Runnable action) { + providing(resource.value(), profile.value(resource.unit()), action); + } + + /** + * Increase resource by a fixed amount while an action runs, + * restoring the resource to its original profile when the action completes. + */ + public static void providing(UnitAware> resource, UnitAware amount, Runnable action) { + providing(resource.value(), amount.value(resource.unit()), action); + } + + /** + * Increase resource by a fixed amount for a fixed time, + * restoring the resource to its original profile when the action completes. + */ + public static void providing(UnitAware> resource, UnitAware amount, Duration time) { + providing(resource.value(), amount.value(resource.unit()), time); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java new file mode 100644 index 0000000000..fc12ae44e2 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java @@ -0,0 +1,755 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial; + +import gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.CommutativityTestInput; +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.*; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.Domain; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.StandardUnits; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Unit; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAwareOperations; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAwareResources; +import gov.nasa.jpl.aerie.contrib.streamline.utils.DoubleUtils; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects; +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.testing; +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.neverExpiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.NEVER; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.wheneverDynamicsChange; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.*; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.bindEffect; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.*; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.approximate; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.relative; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.DifferentiableResources.asDifferentiable; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.byBoundingError; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates.errorByQuadraticApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.secantApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.ClockResources.clock; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.assertThat; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.choose; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.Comparison.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.LinearExpression.lx; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAwareResources.extend; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.*; +import static java.util.Arrays.stream; + +public final class PolynomialResources { + private PolynomialResources() {} + + public static Resource constant(double value) { + var result = pure(polynomial(value)); + name(result, Double.toString(value)); + return result; + } + + public static UnitAware> constant(UnitAware quantity) { + var result = unitAware(constant(quantity.value()), quantity.unit()); + name(result, quantity.toString()); + return result; + } + + public static MutableResource polynomialResource(double... initialCoefficients) { + return polynomialResource(polynomial(initialCoefficients)); + } + + public static MutableResource polynomialResource(Polynomial initialDynamics) { + return resource(initialDynamics, autoEffects(testing( + (CommutativityTestInput input) -> { + Polynomial original = input.original(); + Polynomial left = input.leftResult(); + Polynomial right = input.rightResult(); + return left.degree() == right.degree() && + IntStream.rangeClosed(0, left.degree()).allMatch( + i -> DoubleUtils.areEqualResults( + original.getCoefficient(i), + left.getCoefficient(i), + right.getCoefficient(i))); + }))); + } + + /** + * Treat a discrete resource as a polynomial with constant profile segments. + */ + public static Resource asPolynomial(Resource> discrete) { + var result = map(discrete, d -> polynomial(d.extract())); + name(result, "%s", discrete); + return result; + } + + /** + * Treat a discrete resource as a polynomial with constant profile segments. + */ + public static UnitAware> asUnitAwarePolynomial(UnitAware>> discrete) { + return unitAware(asPolynomial(discrete.value()), discrete.unit()); + } + + /** + * Treat a discrete resource as a polynomial with constant profile segments. + *

+ * Note that this requires dimension-checking every segment individually, which can degrade performance. + * If possible, try using UnitAware<Resource<Discrete<Double>>> + *

+ */ + public static UnitAware> asUnitAwarePolynomial(Resource>> discrete) { + var unit = currentValue(discrete).unit(); + return unitAware(asPolynomial(DiscreteResourceMonad.map(discrete, q -> q.value(unit))), unit); + } + + /** + * Treat a linear resource as a polynomial with linear profile segments. + */ + public static Resource asPolynomial$(Resource linear) { + return map(linear, l -> polynomial(l.extract(), l.rate())); + } + + /** + * Treat a linear resource as a polynomial with linear profile segments. + */ + public static UnitAware> asUnitAwarePolynomial$(UnitAware> linear) { + return unitAware(asPolynomial$(linear.value()), linear.unit()); + } + + /** + * Assume that polynomial is in fact linear. + * + *

+ * This method is very fast, but will throw an error if polynomial is not actually linear. + * To convert polynomials that may not be linear, try {@link PolynomialResources#approximateAsLinear} + *

+ */ + public static Resource assumeLinear(Resource polynomial) { + var result = map(polynomial, p -> { + if (p.degree() <= 1) { + return Linear.linear(p.getCoefficient(0), p.getCoefficient(1)); + } else { + throw new IllegalStateException( + "%s was assumed to be linear, but was actually degree %d".formatted( + getName(polynomial).orElse("Anonymous resource"), + p.degree())); + } + }); + // Since this method is often used to register a polynomial, + // propagate names backwards from the linear result to the polynomial input. + name(polynomial, "%s", result); + return result; + } + + /** + * {@link PolynomialResources#approximateAsLinear(Resource, double)} + * with relativeError = 1e-2 + */ + public static Resource approximateAsLinear(Resource polynomial) { + return approximateAsLinear(polynomial, 1e-2); + } + + /** + * {@link PolynomialResources#approximateAsLinear(Resource, double, double)} + * with epsilon = 1e-10 + */ + public static Resource approximateAsLinear(Resource polynomial, double relativeError) { + return approximateAsLinear(polynomial, relativeError, 1e-10); + } + + /** + * Builds a linear approximation of polynomial, using generally acceptable default settings. + * For more control over the approximation, see {@link Approximation#approximate} and related methods. + * + * @param polynomial The resource to approximate + * @param relativeError The maximum relative error to tolerate in the approximation + * @param epsilon The minimum positive value to distinguish from zero. This avoids oversampling near zero. + * + * @see Approximation#approximate + * @see SecantApproximation#secantApproximation + * @see IntervalFunctions#byBoundingError + * @see IntervalFunctions#byUniformSampling + * @see SecantApproximation.ErrorEstimates#errorByQuadraticApproximation() + * @see SecantApproximation.ErrorEstimates#errorByOptimization() + * @see Approximation#relative + */ + public static Resource approximateAsLinear(Resource polynomial, double relativeError, double epsilon) { + return approximate(asDifferentiable(polynomial), + secantApproximation(byBoundingError( + relativeError, + MINUTE, + duration(24 * 30, HOUR), + relative(errorByQuadraticApproximation(), epsilon)))); + } + + /** + * Returns a continuous resource that follows a precomputed sequence of values. + * Before the first key in segments, value is the first entry in segments. + * Between keys in segments, a linear interpolation between the two adjacent entries is used. + * After the last key in segments, value is the last entry in segments. + */ + public static Resource precomputed(final NavigableMap segments) { + if (segments.isEmpty()) { + throw new IllegalArgumentException("Segments map must have at least one segment"); + } + var clock = clock(); + return signalling(bind(clock, (Clock clock$) -> { + var t = clock$.extract(); + var start = segments.floorEntry(t); + var end = segments.higherEntry(t); + Expiring result; + if (end == null) { + result = neverExpiring(polynomial(start.getValue())); + } else if (start == null) { + result = expiring(polynomial(end.getValue()), end.getKey().minus(t)); + } else { + // interpolate between start and end + var startTime = start.getKey(); + var endTime = end.getKey(); + var slope = (end.getValue() - start.getValue()) / endTime.minus(startTime).ratioOver(SECOND); + var data = polynomial(start.getValue(), slope).step(t.minus(startTime)); + result = expiring(data, endTime.minus(t)); + } + return pure(result); + })); + } + + /** + * Returns a continuous resource that follows a precomputed sequence of values. + * Before the first key in segments, value is valueBeforeFirstEntry. + * Between keys in segments, a linear interpolation between the two adjacent entries is used. + * After the last key in segments, value is the last entry in segments. + */ + public static Resource precomputed( + final NavigableMap segments, final Instant simulationStartTime) { + var segmentsUsingDurationKeys = new TreeMap(); + for (var entry : segments.entrySet()) { + segmentsUsingDurationKeys.put( + Duration.of( + ChronoUnit.MICROS.between(simulationStartTime, entry.getKey()), + Duration.MICROSECONDS), + entry.getValue()); + } + return precomputed(segmentsUsingDurationKeys); + } + + /** + * Add polynomial resources. + */ + @SafeVarargs + public static Resource add(Resource... summands) { + return sum(stream(summands)); + } + + public static Resource sum(Stream> summands) { + return reduce(summands, constant(0), map(Polynomial::add), "Sum"); + } + + /** + * Subtract polynomial resources. + */ + public static Resource subtract(Resource p, Resource q) { + var result = map(p, q, Polynomial::subtract); + name(result, "(%s) - (%s)", p, q); + return result; + } + + /** + * Flip the sign of a polynomial resource. + */ + public static Resource negate(Resource p) { + var result = multiply(constant(-1), p); + name(result, "-(%s)", p); + return result; + } + + public static Resource scale(Resource p, double scalar) { + return multiply(p, constant(scalar)); + } + + /** + * Multiply polynomial resources. + */ + @SafeVarargs + public static Resource multiply(Resource... factors) { + return product(stream(factors)); + } + + /** + * Multiply polynomial resources. + */ + public static Resource product(Stream> factors) { + return reduce(factors, constant(1), map(Polynomial::multiply), "Product"); + } + + /** + * Divide a polynomial by a discrete resource. + *

+ * The divisor must be discrete, because the quotient of two polynomials is not necessarily a polynomial. + *

+ */ + public static Resource divide(Resource p, Resource> q) { + var result = map(p, q, (p$, q$) -> p$.divide(q$.extract())); + name(result, "(%s) / (%s)", p, q); + return result; + } + + /** + * Compute the integral of integrand, starting at startingValue. + *

+ * This method allocates a cell, so must be called during initialization, not simulation. + *

+ */ + public static Resource integrate(Resource integrand, double startingValue) { + var cell = resource(DynamicsMonad.map(integrand.getDynamics(), (Polynomial $) -> $.integral(startingValue))); + // Use integrand's expiry but not integral's, since we're refreshing the integral + wheneverDynamicsChange(integrand, integrandDynamics -> + cell.emit(bindEffect(integral -> DynamicsMonad.map(integrandDynamics, integrand$ -> + integrand$.integral(integral.extract()))))); + name(cell, "Integral (%s)", integrand); + addDependency(cell, integrand); + return cell; + } + + /** + * Compute the integral of integrand, starting at startingValue. + * Also clamp the integral between lowerBound and upperBound (inclusive). + *

+ * Note that clampedIntegrate(r, l, u, s) differs from + * clamp(integrate(r, s), l, u) in how they handle reversing from a boundary. + *

+ *

+ * To see how, consider bounds of [0, 5], with an integrand of 1 for 10 seconds, then -1 for 10 seconds. + *

+ *

+ * clamp and integrate: + *

+ *
+   *   5       /----------\
+   *          /            \
+   *         /              \
+   *        /                \
+   *   0   /                  \
+   * time  0        10        20
+   * 
+ *

+ * clampedIntegrate: + *

+ *
+   *   5       /-----\
+   *          /       \
+   *         /         \
+   *        /           \
+   *   0   /             \-----
+   * time  0        10        20
+   * 
+ */ + public static ClampedIntegrateResult clampedIntegrate( + Resource integrand, Resource lowerBound, Resource upperBound, double startingValue) { + LinearBoundaryConsistencySolver rateSolver = new LinearBoundaryConsistencySolver("clampedIntegrate rate solver"); + var integral = resource(polynomial(startingValue)); + + // Solve for the rate as a function of value + var overflowRate = rateSolver.variable("overflowRate", Domain::lowerBound); + var underflowRate = rateSolver.variable("underflowRate", Domain::lowerBound); + var rate = rateSolver.variable("rate", Domain::upperBound); + + // Set up slack variables for under-/overflow + rateSolver.declare(lx(overflowRate), GreaterThanOrEquals, lx(0)); + rateSolver.declare(lx(underflowRate), GreaterThanOrEquals, lx(0)); + rateSolver.declare(lx(rate).add(lx(underflowRate)).subtract(lx(overflowRate)), Equals, lx(integrand)); + + // Set up rate clamping conditions + var integrandUB = choose( + greaterThanOrEquals(integral, upperBound), + differentiate(upperBound), + constant(Double.POSITIVE_INFINITY)); + var integrandLB = choose( + lessThanOrEquals(integral, lowerBound), + differentiate(lowerBound), + constant(Double.NEGATIVE_INFINITY)); + + rateSolver.declare(lx(rate), LessThanOrEquals, lx(integrandUB)); + rateSolver.declare(lx(rate), GreaterThanOrEquals, lx(integrandLB)); + + // Use a simple feedback loop on volumes to do the integration and clamping. + // Clamping here takes care of discrete under-/overflows and overshooting bounds due to discrete time steps. + var clampedCell = clamp(integral, lowerBound, upperBound); + var correctedCell = map(clampedCell, rate.resource(), (v, r) -> r.integral(v.extract())); + // Use the corrected integral values to set volumes, but erase expiry information in the process to avoid loops + forward(eraseExpiry(correctedCell), integral); + + name(integral, "Clamped Integral (%s)", integrand); + name(overflowRate.resource(), "Overflow of %s", integral); + name(underflowRate.resource(), "Underflow of %s", integral); + return new ClampedIntegrateResult( + integral, + overflowRate.resource(), + underflowRate.resource()); + } + + /** + * The result of a {@link PolynomialResources#clampedIntegrate(Resource, Resource, Resource, double)} call. + * + * @param integral The clamped integral value. + * @param overflow The rate of overflow, when integral hits upper bound. + * Integrate this to get cumulative overflow. + * @param underflow The rate of underflow, when integral hits lower bound. + * Integrate this to get cumulative underflow. + */ + public record ClampedIntegrateResult( + Resource integral, + Resource overflow, + Resource underflow + ) {} + + /** + * Returns the derivative of this resource. + */ + public static Resource differentiate(Resource p) { + var result = map(p, Polynomial::derivative); + name(result, "Derivative (%s)", p); + return result; + } + + /** + * Return a resource which is the average of the operand over the last interval time. + */ + public static Resource movingAverage(Resource p, Duration interval) { + var pIntegral = integrate(p, 0); + var shiftedIntegral = shift(pIntegral, interval, polynomial(0)); + var result = divide(subtract(pIntegral, shiftedIntegral), DiscreteResourceMonad.pure(interval.ratioOver(SECOND))); + name(result, "Moving Average (%s)", p); + return result; + } + + public static Resource> greaterThan(Resource p, double threshold) { + return greaterThan(p, constant(threshold)); + } + + public static Resource> greaterThanOrEquals(Resource p, double threshold) { + return greaterThanOrEquals(p, constant(threshold)); + } + + public static Resource> lessThan(Resource p, double threshold) { + return lessThan(p, constant(threshold)); + } + + public static Resource> lessThanOrEquals(Resource p, double threshold) { + return lessThanOrEquals(p, constant(threshold)); + } + + public static Resource> greaterThan(Resource p, Resource q) { + var result = signalling(bind(p, q, (Polynomial p$, Polynomial q$) -> pure(p$.greaterThan(q$)))); + name(result, "(%s) > (%s)", p, q); + return result; + } + + public static Resource> greaterThanOrEquals(Resource p, Resource q) { + var result = signalling(bind(p, q, (Polynomial p$, Polynomial q$) -> pure(p$.greaterThanOrEquals(q$)))); + name(result, "(%s) >= (%s)", p, q); + return result; + } + + public static Resource> lessThan(Resource p, Resource q) { + var result = signalling(bind(p, q, (Polynomial p$, Polynomial q$) -> pure(p$.lessThan(q$)))); + name(result, "(%s) < (%s)", p, q); + return result; + } + + public static Resource> lessThanOrEquals(Resource p, Resource q) { + var result = signalling(bind(p, q, (Polynomial p$, Polynomial q$) -> pure(p$.lessThanOrEquals(q$)))); + name(result, "(%s) <= (%s)", p, q); + return result; + } + + /** + * Bin values of p like a histogram. + * + * @param p Polynomial to use as a key + * @param bins Map from inclusive lower bound of a range, to the label for that range. + * The next entry in the map is the exclusive upper bound of that range. + */ + public static
Resource> binned(Resource p, Resource>> bins) { + return signalling(bind(p, bins, (Polynomial p$, Discrete> bins$) -> { + var entry = bins$.extract().floorEntry(p$.extract()); + if (entry == null) { + throw new IllegalStateException( + "%s did not contain an entry for value %f".formatted( + Naming.getName(bins).orElse("Bins"), p$.extract())); + } + Double cutoff = bins$.extract().higherKey(p$.extract()); + var upperExpiry = cutoff == null ? NEVER : p$.greaterThanOrEquals(polynomial(cutoff)).expiry(); + var lowerExpiry = p$.lessThan(polynomial(entry.getKey())).expiry(); + return pure(expiring(discrete(entry.getValue()), upperExpiry.or(lowerExpiry))); + })); + } + + @SafeVarargs + public static Resource min(Resource... args) { + return min(stream(args)); + } + + public static Resource min(Stream> args) { + return signalling(reduce(args, constant(Double.POSITIVE_INFINITY), bind((p, q) -> pure(p.min(q))), "Min")); + } + + @SafeVarargs + public static Resource max(Resource... args) { + return max(stream(args)); + } + + public static Resource max(Stream> args) { + return signalling(reduce(args, constant(Double.NEGATIVE_INFINITY), bind((p, q) -> pure(p.max(q))), "Max")); + } + + /** + * Absolute value + */ + public static Resource abs(Resource p) { + var result = max(p, negate(p)); + name(result, "| %s |", p); + return result; + } + + /** + * Returns min(max(p, lowerBound), upperBound). + *

+ * If lowerBound ever exceeds upperBound, this resource fails. + *

+ */ + public static Resource clamp(Resource p, Resource lowerBound, Resource upperBound) { + // Bind an assertion into the resource to error it out if the bounds cross over each other. + var value = max(lowerBound, min(upperBound, p)); + var result = map( + assertThat( + "Clamp lowerBound must be less than or equal to upperBound", + lessThanOrEquals(lowerBound, upperBound)), + value, + (a, v) -> v); + name(result, "Clamp (%s)", p); + return result; + } + + private static Polynomial scalePolynomial(Polynomial p, double s) { + return p.multiply(polynomial(s)); + } + + /** + * Add units to a polynomial resource. + */ + public static UnitAware> unitAware(Resource p, Unit unit) { + return UnitAwareResources.unitAware(p, unit, PolynomialResources::scalePolynomial); + } + + /** + * Add units to a polynomial resource. + */ + public static UnitAware> unitAware(MutableResource p, Unit unit) { + return UnitAwareResources.unitAware(p, unit, PolynomialResources::scalePolynomial); + } + + /** + * Add polynomial resources. + */ + @SafeVarargs + public static UnitAware> add(UnitAware>... summands) { + if (summands.length == 0) { + throw new IllegalArgumentException("Cannot perform unit-aware addition of zero arguments."); + } + final Unit unit = summands[0].unit(); + return unitAware(sum(stream(summands).map(r -> r.value(unit))), unit); + } + + /** + * Add polynomial resources. + */ + public static UnitAware> sum$(Stream>> summands) { + return add(summands.>>toArray(UnitAware[]::new)); + } + + /** + * Subtract polynomial resources. + */ + public static UnitAware> subtract(UnitAware> p, UnitAware> q) { + return UnitAwareOperations.subtract(extend(PolynomialResources::scalePolynomial), + PolynomialResources::subtract, + p, q); + } + + /** + * Multiply polynomial resources. + */ + @SafeVarargs + public static UnitAware> multiply(UnitAware>... factors) { + return unitAware( + product(stream(factors).map(UnitAware::value)), + stream(factors).map(UnitAware::unit).reduce(Unit.SCALAR, Unit::multiply)); + } + + /** + * Multiply polynomial resources. + */ + public static UnitAware> product$(Stream>> factors) { + return multiply(factors.>>toArray(UnitAware[]::new)); + } + + /** + * Divide a polynomial by a discrete resource. + *

+ * The divisor must be discrete, because the quotient of two polynomials is not necessarily a polynomial. + *

+ */ + public static UnitAware> divide(UnitAware> p, UnitAware>> q) { + return UnitAwareOperations.divide(extend(PolynomialResources::scalePolynomial), PolynomialResources::divide, p, q); + } + + /** + * Compute the integral of integrand, starting at startingValue. + *

+ * This method allocates a cell, so must be called during initialization, not simulation. + *

+ */ + public static UnitAware> integrate(UnitAware> p, UnitAware startingValue) { + return UnitAwareOperations.integrate(extend(PolynomialResources::scalePolynomial), + PolynomialResources::integrate, + p, startingValue); + } + + /** + * Compute the integral of integrand, starting at startingValue. + * Also clamp the integral between lowerBound and upperBound (inclusive). + *

+ * Note that clampedIntegrate(r, l, u, s) differs from + * clamp(integrate(r, s), l, u) in how they handle reversing from a boundary. + *

+ *

+ * To see how, consider bounds of [0, 5], with an integrand of 1 for 10 seconds, then -1 for 10 seconds. + *

+ *

+ * clamp and integrate: + *

+ *
+   *   5       /----------\
+   *          /            \
+   *         /              \
+   *        /                \
+   *   0   /                  \
+   * time  0        10        20
+   * 
+ *

+ * clampedIntegrate: + *

+ *
+   *   5       /-----\
+   *          /       \
+   *         /         \
+   *        /           \
+   *   0   /             \-----
+   * time  0        10        20
+   * 
+ */ + public static UnitAwareClampedIntegrateResult clampedIntegrate(UnitAware> p, UnitAware> lowerBound, UnitAware> upperBound, UnitAware startingValue) { + final Unit resultUnit = p.unit().multiply(StandardUnits.SECOND); + var unitNaiveResult = clampedIntegrate( + p.value(), + lowerBound.value(resultUnit), + upperBound.value(resultUnit), + startingValue.value(resultUnit)); + return new UnitAwareClampedIntegrateResult( + unitAware(unitNaiveResult.integral(), resultUnit), + unitAware(unitNaiveResult.overflow(), p.unit()), + unitAware(unitNaiveResult.underflow(), p.unit())); + } + + /** + * The result of a {@link PolynomialResources#clampedIntegrate(UnitAware, UnitAware, UnitAware, UnitAware)} call. + * + * @param integral The clamped integral value. + * @param overflow The rate of overflow, when integral hits upper bound. + * Integrate this to get cumulative overflow. + * @param underflow The rate of underflow, when integral hits lower bound. + * Integrate this to get cumulative underflow. + */ + public record UnitAwareClampedIntegrateResult( + UnitAware> integral, + UnitAware> overflow, + UnitAware> underflow + ) {} + + /** + * Returns the derivative of this resource. + */ + public static UnitAware> differentiate(UnitAware> p) { + return UnitAwareOperations.differentiate(extend(PolynomialResources::scalePolynomial), + PolynomialResources::differentiate, + p); + } + + // Ugly $ suffix is to avoid ambiguous overloading after erasure. + public static Resource> greaterThan$(UnitAware> p, UnitAware threshold) { + return greaterThan(p.value(), threshold.value(p.unit())); + } + + public static Resource> greaterThanOrEquals$(UnitAware> p, UnitAware threshold) { + return greaterThanOrEquals(p.value(), threshold.value(p.unit())); + } + + public static Resource> lessThan$(UnitAware> p, UnitAware threshold) { + return lessThan(p.value(), threshold.value(p.unit())); + } + + public static Resource> lessThanOrEquals$(UnitAware> p, UnitAware threshold) { + return lessThanOrEquals(p.value(), threshold.value(p.unit())); + } + + public static Resource> greaterThan(UnitAware> p, UnitAware> q) { + return greaterThan(subtract(p, q).value(), 0); + } + + public static Resource> greaterThanOrEquals(UnitAware> p, UnitAware> q) { + return greaterThanOrEquals(subtract(p, q).value(), 0); + } + + public static Resource> lessThan(UnitAware> p, UnitAware> q) { + return lessThan(subtract(p, q).value(), 0); + } + + public static Resource> lessThanOrEquals(UnitAware> p, UnitAware> q) { + return lessThanOrEquals(subtract(p, q).value(), 0); + } + + public static UnitAware> min(UnitAware> p, UnitAware> q) { + return unitAware(min(p.value(), q.value(p.unit())), p.unit()); + } + + public static UnitAware> max(UnitAware> p, UnitAware> q) { + return unitAware(max(p.value(), q.value(p.unit())), p.unit()); + } + + /** + * Returns min(max(p, lowerBound), upperBound). + *

+ * If lowerBound ever exceeds upperBound, this resource fails. + *

+ */ + public static UnitAware> clamp(UnitAware> p, UnitAware> lowerBound, UnitAware> upperBound) { + return unitAware(clamp(p.value(), lowerBound.value(p.unit()), upperBound.value(p.unit())), p.unit()); + } +} From 50964d729a2b65ea2a50af435c7236c83ad1dcd0 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 18:12:05 -0800 Subject: [PATCH 024/159] Add LinearBoundaryConsistencySolver Adds a solver for linear inequality constraints posed on polynomial resources, which proceeds by boundary consistency without backtracking. For some resource problems, specifying behavior in terms of comparisons and arithmetic can be hard to ready and error-prone. In particular, PolynomialResources.clampedIntegrate was found to be this way. These problems are often readily formulated as linear inequality constraints over variables with polynomial dynamics segments for values. Since polynomials form a vector space over the real numbers, such linear inequalities are amenable to solving by boundary consistency without backtracking.* Additionally, by specifying how to resolve under-constrained problems, a simple greedy optimizer can be defined for linear objective functions. See `PolynomialResources.clampedIntegrate` for an example of how this solver is used. *Linear programming can't be used because that requires division. --- .../LinearBoundaryConsistencySolver.java | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java new file mode 100644 index 0000000000..9f7f258854 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java @@ -0,0 +1,391 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiry; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ExpiringMonad; +import gov.nasa.jpl.aerie.merlin.framework.Condition; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching.failure; +import static gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching.success; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.neverExpiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ExpiringMonad.bind; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.eraseExpiry; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Context.contextualized; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.getName; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.name; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.GeneralConstraint.constraint; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.InequalityComparison.GreaterThanOrEquals; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.InequalityComparison.LessThanOrEquals; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.subtract; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.*; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +/** + * Special methods for setting up a substepping resource solver + * using linear constraints and arc consistency. + * + *

+ * Constraints are linear in a set of variables, + * and each variable is a polynomial resource. + * When a driving variable changes, or the current solution expires, + * the solver runs as part of the next Aerie simulation step. + *

+ */ +public final class LinearBoundaryConsistencySolver { + private final List> drivenTerms = new LinkedList<>(); + private final List variables = new LinkedList<>(); + private final List constraints = new LinkedList<>(); + private final Map> neighboringConstraints = new HashMap<>(); + + public LinearBoundaryConsistencySolver(String name) { + name(this, name); + + spawn(contextualized(name + " solving", () -> { + // Don't solve for the first time until sim starts. + // This ensures all variables are initialized and all constraints are declared. + buildNeighboringConstraints(); + solve(); + // After that, solve whenever any of the driven terms change + // OR a solved variable changes (which can only happen when it expires) + whenever( + contextualized(name + " resolving condition", () -> Stream.concat( + drivenTerms.stream(), + variables.stream().map(Variable::resource)) + .map(Resources::dynamicsChange) + .reduce(Condition.FALSE, (c1, c2) -> c1.or(c2))), + this::solve); + })); + } + + public Variable variable(String name, Function> selectionPolicy) { + var variable = new Variable(name, resource(polynomial(0)), selectionPolicy); + variables.add(variable); + // All variables depend on the solver, because all of them change together when the solver runs. + addDependency(variable.resource(), this); + return variable; + } + + public void declare(LinearExpression left, Comparison comparison, LinearExpression right) { + declare(constraint(left, comparison, right)); + } + + public void declare(GeneralConstraint constraint) { + var normalizedConstraint = constraint.normalize(); + drivenTerms.add(normalizedConstraint.drivenTerm); + constraints.addAll(normalizedConstraint.standardize()); + // The solver depends on the normalized driven term, which will depend on any driven terms in the general constraint, + // because any change in any driven term could trigger the solver. + addDependency(this, normalizedConstraint.drivenTerm); + } + + private void buildNeighboringConstraints() { + for (var variable : variables) { + neighboringConstraints.put(variable, new HashSet<>()); + } + for (var constraint : constraints) { + for (var drivingVariable : constraint.drivingVariables) { + neighboringConstraints.get(drivingVariable).add(constraint); + } + } + } + + private void solve() { + final var domains = variables.stream().collect(toMap(identity(), Domain::new)); + final Queue constraintsLeft = new LinkedList<>(constraints); + DirectionalConstraint constraint; + try { + // While we either have constraints to apply or domains to solve... + while (!constraintsLeft.isEmpty() || domains.values().stream().anyMatch(Domain::isUnsolved)) { + // Apply all constraints through simple arc consistency + while ((constraint = constraintsLeft.poll()) != null) { + var V = constraint.constrainedVariable; + var D = domains.get(V); + var newBound = constraint.bound.apply(domains).getDynamics().getOrThrow(); + boolean domainChanged = switch (constraint.comparison) { + case LessThanOrEquals -> D.restrictUpper(newBound); + case GreaterThanOrEquals -> D.restrictLower(newBound); + }; + if (domainChanged) { + if (D.isEmpty()) { + throw new IllegalStateException( + "LinearBoundaryConsistencySolver %s failed. Domain for %s is empty: [%s, %s]".formatted( + getName(this).orElseThrow(), D.variable, D.lowerBound, D.upperBound)); + } + // TODO: Make this more efficient by not adding constraints that are already in the queue. + constraintsLeft.addAll(neighboringConstraints.get(D.variable)); + } + } + // If that didn't fully solve all variables, choose the first unsolved variable + // and use the selection policy to pick a solution arbitrarily, then restart arc consistency. + variables + .stream() + .map(domains::get) + .filter(Domain::isUnsolved) + .findFirst() + .ifPresent(D -> { + D.lowerBound = D.upperBound = D.variable.selectionPolicy.apply(D); + constraintsLeft.addAll(neighboringConstraints.get(D.variable)); + }); + } + // All domains are solved and non-empty, emit solution + // Expiry for entire solution is taken as a whole: + Expiry solutionExpiry = variables + .stream() + .map(v -> { + var D = domains.get(v); + return D.lowerBound.expiry().or(D.upperBound.expiry()); + }) + .reduce(Expiry.NEVER, Expiry::or); + for (var v : variables) { + // Overwrite failures if we recover + var result = success(expiring(domains.get(v).lowerBound.data(), solutionExpiry)); + v.resource.emit($ -> result); + } + } catch (Exception e) { + // Solving failed, so populate all outputs with the failure. + ErrorCatching> result = failure(e); + for (var v : variables) { + // Don't emit failures on cells that have already failed, though. + // That would make those cells unnecessarily noisy. + if (!(v.resource.getDynamics() instanceof ErrorCatching.Failure>)) { + v.resource.emit($ -> result); + } + } + } + } + + public static final class Variable { + private final MutableResource resource; + private final Function> selectionPolicy; + + public Variable( + String name, + MutableResource resource, + Function> selectionPolicy) { + name(this, name); + name(resource, name); + this.resource = resource; + this.selectionPolicy = selectionPolicy; + } + + @Override + public String toString() { + return getName(this).orElseThrow(); + } + + // Expose resource as Resource, not CellResource, + // because only the solver should emit effects on it. + public Resource resource() { + return resource; + } + } + + public enum Comparison { + LessThanOrEquals, + GreaterThanOrEquals, + Equals + } + public enum InequalityComparison { + LessThanOrEquals, + GreaterThanOrEquals; + + InequalityComparison opposite() { + return switch (this) { + case LessThanOrEquals -> GreaterThanOrEquals; + case GreaterThanOrEquals -> LessThanOrEquals; + }; + } + } + /** + * Expression drivenTerm + sum of c_i * s_i over entries c_i -> s_i in controlledTerm + */ + public record LinearExpression(Resource drivenTerm, Map controlledTerm) { + public static LinearExpression lx(double value) { + return lx(constant(value)); + } + public static LinearExpression lx(Resource drivenTerm) { + return new LinearExpression(drivenTerm, Map.of()); + } + public static LinearExpression lx(Variable controlledTerm) { + return new LinearExpression(constant(0), Map.of(controlledTerm, 1.0)); + } + public LinearExpression add(LinearExpression other) { + return new LinearExpression( + PolynomialResources.add(drivenTerm, other.drivenTerm), + addControlledTerms(controlledTerm, other.controlledTerm)); + } + public LinearExpression subtract(LinearExpression other) { + return this.add(other.multiply(-1)); + } + public LinearExpression multiply(double scale) { + if (scale == 0) { + // Short circuit to avoid unnecessary dependencies. + return lx(constant(0)); + } else { + return new LinearExpression( + PolynomialResources.multiply(drivenTerm, constant(scale)), + scaleControlledTerm(controlledTerm, scale)); + } + } + + private Map scaleControlledTerm(Map controlledTerm, double scale) { + var result = new HashMap<>(controlledTerm); + for (var v : result.keySet()) { + result.computeIfPresent(v, (v$, s) -> s * scale); + } + return result; + } + + private static Map addControlledTerms(Map left, Map right) { + var result = new HashMap(); + var allVariables = new HashSet<>(left.keySet()); + allVariables.addAll(right.keySet()); + for (var v : allVariables) { + double scale = left.getOrDefault(v, 0.0) + right.getOrDefault(v, 0.0); + if (scale != 0.0) { + result.put(v, scale); + } + } + return result; + } + } + + // The following three kinds of constraints are equivalent, but are best suited to different use cases. + // General is easiest to read and write in model code, as it's the most flexible. + public record GeneralConstraint(LinearExpression left, Comparison comparison, LinearExpression right) { + NormalizedConstraint normalize() { + var drivenTerm = subtract(right.drivenTerm, left.drivenTerm); + var controlledTerm = new HashMap(); + var allVariables = new HashSet<>(left.controlledTerm().keySet()); + allVariables.addAll(right.controlledTerm().keySet()); + for (var v : allVariables) { + double scale = left.controlledTerm().getOrDefault(v, 0.0) - right.controlledTerm().getOrDefault(v, 0.0); + if (scale != 0.0) { + controlledTerm.put(v, scale); + } + } + return new NormalizedConstraint(controlledTerm, comparison, drivenTerm); + } + + public static GeneralConstraint constraint(LinearExpression left, Comparison comparison, LinearExpression right) { + return new GeneralConstraint(left, comparison, right); + } + } + // Normalized is like General without redundant information. Also, drivenTerm can be used to trigger solving. + private record NormalizedConstraint( + Map controlledTerm, + Comparison comparison, + Resource drivenTerm) { + List standardize() { + return controlledTerm.keySet().stream().flatMap(this::directionalConstraints).toList(); + } + private Stream directionalConstraints(Variable constrainedVariable) { + double inverseScale = 1 / controlledTerm.get(constrainedVariable); + var drivingVariables = new HashSet<>(controlledTerm.keySet()); + drivingVariables.remove(constrainedVariable); + Stream inequalityComparisons = switch (comparison) { + case LessThanOrEquals -> Stream.of(LessThanOrEquals); + case GreaterThanOrEquals -> Stream.of(GreaterThanOrEquals); + case Equals -> Stream.of(LessThanOrEquals, GreaterThanOrEquals); + }; + return inequalityComparisons.map(c -> new DirectionalConstraint(constrainedVariable, inverseScale > 0 ? c : c.opposite(), domains -> { + // Expiry for driven terms is captured by re-solving rather than expiring the solution. + // If solver has a feedback loop from last iteration (which is common) + // feeding that expiry in here can loop the solver forever. + var result = eraseExpiry(drivenTerm); + for (var drivingVariable : drivingVariables) { + var scale = controlledTerm.get(drivingVariable); + var domain = domains.get(drivingVariable); + var useLowerBound = (scale > 0) == (c == LessThanOrEquals); + var domainBound = ExpiringMonad.map( + useLowerBound ? domain.lowerBound() : domain.upperBound(), + b -> b.multiply(polynomial(-scale))); + result = add(result, () -> success(domainBound)); + } + return multiply(result, constant(inverseScale)); + }, drivingVariables)); + } + } + // Directional constraints are useful for arc consistency, since they have input (driving) and output (constrained) variables. + // However, many directional constraints are required in general to express one General constraint. + private record DirectionalConstraint(Variable constrainedVariable, InequalityComparison comparison, Function, Resource> bound, Set drivingVariables) {} + + public static final class Domain { + public final Variable variable; + private Expiring lowerBound; + private Expiring upperBound; + + public Domain(Variable variable) { + this.variable = variable; + this.lowerBound = neverExpiring(polynomial(Double.NEGATIVE_INFINITY)); + this.upperBound = neverExpiring(polynomial(Double.POSITIVE_INFINITY)); + } + + public Expiring lowerBound() { + return lowerBound; + } + + public Expiring upperBound() { + return upperBound; + } + + public boolean restrictLower(Expiring newLowerBound) { + var oldLowerBound = lowerBound; + lowerBound = bind(lowerBound, lb -> bind(newLowerBound, nlb -> lb.max(turnNanInto(nlb, Double.NEGATIVE_INFINITY)))); + return !lowerBound.equals(oldLowerBound); + } + + public boolean restrictUpper(Expiring newUpperBound) { + var oldUpperBound = upperBound; + upperBound = bind(upperBound, ub -> bind(newUpperBound, nub -> ub.min(turnNanInto(nub, Double.POSITIVE_INFINITY)))); + return !upperBound.equals(oldUpperBound); + } + + private static Polynomial turnNanInto(Polynomial p, double replacement) { + // NaN indicates a lack of information. This can be used to re-interpret it as needed, depending on context. + for (int n = 0; n <= p.degree(); ++n) { + if (Double.isNaN(p.getCoefficient(n))) { + var newCoefficients = Arrays.copyOf(p.coefficients(), n + 1); + newCoefficients[n] = replacement; + return polynomial(newCoefficients); + } + } + return p; + } + + @Override + public String toString() { + return "Domain[" + lowerBound + ", " + upperBound + ']'; + } + + public boolean isEmpty() { + return lowerBound().data().extract() > upperBound().data().extract(); + } + + public boolean isUnsolved() { + return !lowerBound().data().equals(upperBound().data()); + } + } +} From 7fa327ad6d32f66641c600ee859fb3146925faba Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 18:12:36 -0800 Subject: [PATCH 025/159] Add unit-awareness Adds types and utilities for unit-awareness. In particular, adds the `UnitAware` interface, which is generalized on the type of values to which units are attached. This is to support adding units to both value types, like `Double`, and resource types, like `Resource` or `Resource>`. Units are restricted to "absolute" units to simplify conversion, representation, and usage. Units are represented by a Dimension and a floating-point scale compared to a base unit for that dimension. Dimensions are represented exactly, as a product of rational powers of incommensurate base dimensions. When using units on resources, we wrap UnitAware around Resource, and not vice-versa. This applies a single unit to the entire lifetime of the resource, rather than letting the unit vary over time. This lets us check dimensionality on resources once during initialization, then "bake in" constant conversion factors to be used during simulation, a setup that has proven performant in Clipper's model. --- .../streamline/unit_aware/Dimension.java | 175 ++++++++++++++++++ .../streamline/unit_aware/Quantities.java | 65 +++++++ .../streamline/unit_aware/Rational.java | 76 ++++++++ .../unit_aware/StandardDimensions.java | 21 +++ .../streamline/unit_aware/StandardUnits.java | 41 ++++ .../contrib/streamline/unit_aware/Unit.java | 106 +++++++++++ .../streamline/unit_aware/UnitAware.java | 97 ++++++++++ .../unit_aware/UnitAwareOperations.java | 59 ++++++ .../unit_aware/UnitAwareResources.java | 65 +++++++ 9 files changed, 705 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Dimension.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Quantities.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Rational.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardDimensions.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardUnits.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Unit.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAware.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareOperations.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareResources.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Dimension.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Dimension.java new file mode 100644 index 0000000000..f9d9d6f231 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Dimension.java @@ -0,0 +1,175 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static java.util.Collections.reverseOrder; +import static java.util.Map.Entry.comparingByValue; +import static java.util.stream.Collectors.joining; + +/** + * A kind of quantity, which can be measured. + * For example, length, time, energy, or data rate. + * + *

+ * Quantities with the same dimension but different units, like meters and miles, + * can be compared, added, and subtracted. + * Quantities with different dimensions, like meters and seconds, + * cannot be added or subtracted, and are never equal. + *

+ * + *

+ * Base dimensions are declared using {@link Dimension#createBase}. Base dimensions are definitionally all distinct + * from each other, and are distinct from all combinations of other base dimensions. + *

+ * + *

+ * Dimensions can be composed by multiplication, division, and exponentiation by a constant + * to derive new dimensions, and composite units correlate to the composite dimension. + * For example, Energy is the dimension defined as Mass * Length^2 / Time^2, and + * Newton is a unit of Energy defined as Kilogram * Meter^2 / Second^2. + * Internally, all dimensions are a map from base dimensions to their power. For example, Mass is stored (loosely) as + * {@code {"Mass": 1}} and Energy is {@code {"Mass": 1, "Length": 2, "Time": -2}}. + *

+ */ +public sealed interface Dimension { + Dimension SCALAR = new DerivedDimension(Map.of()); + + Map basePowers(); + boolean isBase(); + + + default Dimension multiply(Dimension other) { + var resultBasePowers = new HashMap<>(basePowers()); + for (var dimensionPower : other.basePowers().entrySet()) { + var power = dimensionPower.getValue(); + resultBasePowers.compute( + dimensionPower.getKey(), + (k, p) -> p == null ? power : power.add(p)); + } + return create(resultBasePowers); + } + + default Dimension divide(Dimension other) { + var resultBasePowers = new HashMap<>(basePowers()); + for (var dimensionPower : other.basePowers().entrySet()) { + var power = dimensionPower.getValue().negate(); + resultBasePowers.compute( + dimensionPower.getKey(), + (k, p) -> p == null ? power : power.add(p)); + } + return create(resultBasePowers); + } + + default Dimension power(Rational power) { + var resultBasePowers = new HashMap(); + for (var dimensionPower : basePowers().entrySet()) { + resultBasePowers.put(dimensionPower.getKey(), dimensionPower.getValue().multiply(power)); + } + return create(resultBasePowers); + } + + private static Dimension create(Map basePowers) { + var normalizedBasePowers = new HashMap(); + for (var entry : basePowers.entrySet()) { + if (!entry.getValue().equals(Rational.ZERO)) { + normalizedBasePowers.put(entry.getKey(), entry.getValue()); + } + } + + if (normalizedBasePowers.isEmpty()) { + return Dimension.SCALAR; + } else if (normalizedBasePowers.size() == 1) { + final var solePower = normalizedBasePowers.entrySet().stream().findAny().get(); + if (solePower.getValue().equals(Rational.ONE)) { + // This actually *is* the base dimension, so return that instead + // Normalizing like this lets us bootstrap using reference equality on base dimensions. + return solePower.getKey(); + } + } + + // Otherwise, this is some composite dimension, build it anew. + return new DerivedDimension(normalizedBasePowers); + } + + static Dimension createBase(String name) { + return new BaseDimension(name); + } + + final class BaseDimension implements Dimension { + public final String name; + + private BaseDimension(final String name) { + this.name = name; + } + + @Override + public Map basePowers() { + return Map.of(this, Rational.ONE); + } + + @Override + public boolean isBase() { + return true; + } + + // Reference equality is sufficient here. Do *not* override equals/hashCode. + + @Override + public String toString() { + return name; + } + } + + final class DerivedDimension implements Dimension { + private final Map basePowers; + + private DerivedDimension(final Map basePowers) { + this.basePowers = basePowers; + } + + @Override + public Map basePowers() { + return basePowers; + } + + @Override + public boolean isBase() { + return false; + } + + // Use semantic equality defined by the base powers map + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DerivedDimension that = (DerivedDimension) o; + return Objects.equals(basePowers, that.basePowers); + } + + @Override + public int hashCode() { + return Objects.hash(basePowers); + } + + @Override + public String toString() { + return basePowers.entrySet().stream() + .sorted(reverseOrder(comparingByValue())) + .map(basePower -> formatBasePower(basePower.getKey(), basePower.getValue())) + .collect(joining(" ")); + } + + private static String formatBasePower(BaseDimension d, Rational p) { + if (p.equals(Rational.ONE)) { + return "[%s]".formatted(d.name); + } else if (p.denominator() == 1) { + return "[%s]^%s".formatted(d.name, p.numerator()); + } else { + return "[%s]^(%d/%d)".formatted(d.name, p.numerator(), p.denominator()); + } + } + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Quantities.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Quantities.java new file mode 100644 index 0000000000..10e811b1cb --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Quantities.java @@ -0,0 +1,65 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware.unitAware; + +/** + * Utilities for UnitAware<Double>, aka a "Quantity" + */ +public final class Quantities { + public static UnitAware quantity(double amount) { + return quantity(amount, Unit.SCALAR); + } + + public static UnitAware quantity(double amount, Unit unit) { + return unitAware(amount, unit, Quantities::scaling); + } + + public static UnitAware quantity(Duration duration) { + return unitAware(duration.ratioOver(Duration.SECOND), StandardUnits.SECOND, Quantities::scaling); + } + + public static UnitAware add(UnitAware p, UnitAware q) { + return UnitAwareOperations.add(Quantities::scaling, (x, y) -> x + y, p, q); + } + + public static UnitAware subtract(UnitAware p, UnitAware q) { + return UnitAwareOperations.subtract(Quantities::scaling, (x, y) -> x - y, p, q); + } + + public static UnitAware multiply(UnitAware p, UnitAware q) { + return UnitAwareOperations.multiply(Quantities::scaling, (x, y) -> x * y, p, q); + } + + public static UnitAware divide(UnitAware p, UnitAware q) { + return UnitAwareOperations.divide(Quantities::scaling, (x, y) -> x / y, p, q); + } + + /** + * Absolute value + */ + public static UnitAware abs(UnitAware p) { + return p.map(Math::abs); + } + + public static boolean lessThan(UnitAware p, UnitAware q) { + return p.value() < q.value(p.unit()); + } + + public static boolean lessThanOrEquals(UnitAware p, UnitAware q) { + return p.value() <= q.value(p.unit()); + } + + public static boolean greaterThan(UnitAware p, UnitAware q) { + return p.value() > q.value(p.unit()); + } + + public static boolean greaterThanOrEquals(UnitAware p, UnitAware q) { + return p.value() >= q.value(p.unit()); + } + + private static double scaling(double x, double y) { + return x * y; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Rational.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Rational.java new file mode 100644 index 0000000000..c6131a40ea --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Rational.java @@ -0,0 +1,76 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import static java.lang.Integer.signum; +import static org.apache.commons.math3.util.ArithmeticUtils.gcd; + +/** + * Lightweight exact rational number, used primarily for tracking dimensionality + * in the QUDV system. + */ +public record Rational(int numerator, int denominator) implements Comparable { + public static final Rational ZERO = new Rational(0, 1); + public static final Rational ONE = new Rational(1, 1); + + public Rational(final int numerator, final int denominator) { + if (denominator == 0) { + throw new ArithmeticException("Cannot create a Rational with 0 denominator."); + } + + // Normalize by dividing by the GCD and forcing the denominator to be positive. + final int gcd = gcd(numerator, denominator); + final int s = signum(denominator); + this.numerator = s * numerator / gcd; + this.denominator = s * denominator / gcd; + } + + public static Rational rational(final int numerator, final int denominator) { + return new Rational(numerator, denominator); + } + + public static Rational rational(final int value) { + return new Rational(value, 1); + } + + public Rational add(Rational other) { + return new Rational( + numerator * other.denominator + denominator * other.numerator, + denominator * other.denominator); + } + + public Rational negate() { + return new Rational(-numerator, denominator); + } + + public Rational subtract(Rational other) { + return this.add(other.negate()); + } + + public Rational multiply(Rational other) { + return new Rational( + numerator * other.numerator, + denominator * other.denominator); + } + + /** + * Flip numerator and divisor, equivalent to raising to the power -1. + */ + public Rational invert() { + return new Rational(denominator, numerator); + } + + public Rational divide(Rational other) { + return this.multiply(other.invert()); + } + + @Override + public int compareTo(final Rational o) { + return Integer.compare(numerator * o.denominator, denominator * o.numerator); + } + + /** + * Approximate this as a floating-point number. + */ + public double doubleValue() { + return ((double) numerator) / denominator; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardDimensions.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardDimensions.java new file mode 100644 index 0000000000..f587183aba --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardDimensions.java @@ -0,0 +1,21 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +/** + * Collection of standard dimensions, including all SI base dimensions. + */ +public final class StandardDimensions { + private StandardDimensions() {} + + // SI base dimensions: + public static final Dimension TIME = Dimension.createBase("Time"); + public static final Dimension LENGTH = Dimension.createBase("Length"); + public static final Dimension MASS = Dimension.createBase("Mass"); + public static final Dimension CURRENT = Dimension.createBase("Current"); + public static final Dimension TEMPERATURE = Dimension.createBase("Temperature"); + public static final Dimension LUMINOUS_INTENSITY = Dimension.createBase("Luminous Intensity"); + public static final Dimension AMOUNT = Dimension.createBase("Amount of Substance"); + + // Additional base dimensions we've found useful in practice + public static final Dimension INFORMATION = Dimension.createBase("Information"); + public static final Dimension ANGLE = Dimension.createBase("Angle"); +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardUnits.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardUnits.java new file mode 100644 index 0000000000..9fc3bd0e71 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/StandardUnits.java @@ -0,0 +1,41 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +/** + * Collection of standard units, including all SI base units. + */ +public final class StandardUnits { + private StandardUnits() {} + + // Base units, should correspond 1-1 with base Dimensions + public static final Unit SECOND = Unit.createBase("s", "second", StandardDimensions.TIME); + public static final Unit METER = Unit.createBase("m", "meter", StandardDimensions.LENGTH); + public static final Unit KILOGRAM = Unit.createBase("kg", "kilogram", StandardDimensions.MASS); + public static final Unit AMPERE = Unit.createBase("A", "ampere", StandardDimensions.CURRENT); + public static final Unit KELVIN = Unit.createBase("K", "Kelvin", StandardDimensions.TEMPERATURE); + public static final Unit CANDELA = Unit.createBase("cd", "candela", StandardDimensions.LUMINOUS_INTENSITY); + public static final Unit MOLE = Unit.createBase("mol", "mole", StandardDimensions.AMOUNT); + public static final Unit BIT = Unit.createBase("b", "bit", StandardDimensions.INFORMATION); + public static final Unit RADIAN = Unit.createBase("rad", "radian", StandardDimensions.ANGLE); + + // REVIEW: What derived units should be included here? + // Including a few arbitrarily to show a few different styles of derivation + public static final Unit MILLISECOND = Unit.derived("ms", "millisecond", 1e-3, SECOND); + public static final Unit MINUTE = Unit.derived("min", "minute", 60, SECOND); + public static final Unit HOUR = Unit.derived("hr", "hour", 60, MINUTE); + public static final Unit KILOMETER = Unit.derived("km", "kilometer", 1000, METER); + public static final Unit BYTE = Unit.derived("B", "byte", 8, BIT); + public static final Unit NEWTON = Unit.derived("N", "newton", KILOGRAM.multiply(METER).divide(SECOND.power(2))); + public static final Unit MEGABIT_PER_SECOND = Unit.derived("Mbps", "megabit per second", 1e6, BIT.divide(SECOND)); + public static final Unit DEGREE = Unit.derived("deg", "degree", 180 / Math.PI, RADIAN); + + public static final Unit JOULE = Unit.derived("J", "joule", NEWTON.multiply(METER)); + public static final Unit WATT = Unit.derived("W", "watt", JOULE.divide(SECOND)); + public static final Unit COULOMB = Unit.derived("C", "coulomb", AMPERE.multiply(SECOND)); + public static final Unit VOLT = Unit.derived("V", "volt", JOULE.divide(COULOMB)); + + /** + * Astronomical unit as defined by + *
IAU 2012 Resolution B2. + */ + public static final Unit ASTRONOMICAL_UNIT = Unit.derived("au", "astronomical unit", 149_597_870_700.0, METER); +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Unit.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Unit.java new file mode 100644 index 0000000000..7e57a1afe2 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/Unit.java @@ -0,0 +1,106 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import java.util.Objects; + +/** + * A unit of measure in the QUDV system. + */ +public final class Unit { + public static final Unit SCALAR = new Unit(Dimension.SCALAR, 1, "(scalar)", "(scalar)"); + + public final Dimension dimension; + public final double multiplier; + public final String longName; + public final String shortName; + + private Unit(final Dimension dimension, final double multiplier) { + this(dimension, multiplier, null, null); + } + + private Unit(final Dimension dimension, final double multiplier, final String longName, final String shortName) { + this.dimension = dimension; + this.multiplier = multiplier; + this.longName = longName; + this.shortName = shortName; + } + + public static Unit createBase(final String shortName, final String longName, final Dimension dimension) { + // TODO: Track base units, to detect and prevent collisions + assert(dimension.isBase()); + return new Unit(dimension, 1, longName, shortName); + } + + /** + * Create a "local" unit. Local units denote a locally-relevant concept that doesn't need to be integrated across models into the broader unit system. + * + *

+ * Local units are given their own unique base dimension with the same name, so are distinct from all other base dimensions. + *

+ * + *

+ * For example, to record that instrument A takes 6 observations per hour, we could declare a local unit in the instrument A model for observations and use it to derive "observations / hour", like so: + *
+ * + * Unit Observations = Unit.createLocalUnit("observations");
+ * UnitAware<Double> observationRate = quantity(6, Observations.divide(HOUR)); + *
+ *
+ * If instrument B also declared a local unit called "observations", this would be incommensurate with instrument A's "observations" unit. + *

+ */ + public static Unit createLocalUnit(final String name) { + return createBase(name, name, Dimension.createBase(name)); + } + + // Used for naming compound units + public static Unit derived(final String shortName, final String longName, final Unit definingUnit) { + return derived(shortName, longName, 1, definingUnit); + } + + public static Unit derived(final String shortName, final String longName, final UnitAware definingQuantity) { + return derived(shortName, longName, definingQuantity.value(), definingQuantity.unit()); + } + + public static Unit derived(final String shortName, final String longName, final double multiplier, final Unit baseUnit) { + return new Unit(baseUnit.dimension, baseUnit.multiplier * multiplier, longName, shortName); + } + + public Unit multiply(Unit other) { + return new Unit(dimension.multiply(other.dimension), multiplier * other.multiplier); + } + + public Unit divide(Unit other) { + return new Unit(dimension.divide(other.dimension), multiplier / other.multiplier); + } + + public Unit power(int power) { + return power(Rational.rational(power)); + } + + public Unit power(Rational power) { + return new Unit(dimension.power(power), Math.pow(multiplier, power.doubleValue())); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Unit unit = (Unit) o; + return Double.compare(unit.multiplier, multiplier) == 0 && Objects.equals(dimension, unit.dimension); + } + + @Override + public int hashCode() { + return Objects.hash(dimension, multiplier); + } + + @Override + public String toString() { + if (longName != null) { + return longName; + } else { + // TODO: Better name derivation, by tracking named base units + return "Unit{" + multiplier + " in " + dimension + "}"; + } + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAware.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAware.java new file mode 100644 index 0000000000..f24c11aa8f --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAware.java @@ -0,0 +1,97 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import java.util.Comparator; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import static java.util.function.Function.identity; + +/** + * A value of type T with an attached unit. + * This can be rescaled to other units measuring the same dimension. + */ +public interface UnitAware { + + default T value(Unit desiredUnit) { + return in(desiredUnit).value(); + } + + T value(); + + Unit unit(); + + UnitAware in(Unit desiredUnit); + + UnitAware map(UnaryOperator unitInvariantFunction); + + /** + * General constructor, used primarily by library code. + */ + static UnitAware unitAware(T value, Unit unit, Function> rescale) { + + return new UnitAware<>() { + @Override + public T value() { + return value; + } + + @Override + public Unit unit() { + return unit; + } + + @Override + public UnitAware in(Unit desiredUnit) { + if (unit == desiredUnit) { + // Short-circuit for performance + return this; + } + if (!unit.dimension.equals(desiredUnit.dimension)) { + // TODO: Should this be its own kind of exception? A UnitConversionException or DimensionMismatchException? + throw new IllegalArgumentException("Cannot convert %s to desired unit %s due to dimension mismatch (%s vs %s)." + .formatted(unit, desiredUnit, unit.dimension, desiredUnit.dimension)); + } + return rescale.apply(desiredUnit); + } + + @Override + public UnitAware map(final UnaryOperator unitInvariantFunction) { + return unitAware(unitInvariantFunction.apply(value), unit, rescale); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (UnitAware) obj; + return Objects.equals(value, that.value()) && Objects.equals(unit, that.unit()); + } + + @Override + public int hashCode() { + return Objects.hash(value, unit); + } + + @Override + public String toString() { + return "UnitAware[" + "value=" + value + ", " + "unit=" + unit + ']'; + } + }; + } + + /** + * Scaling-function constructor, used primarily in code outside this library. + */ + static UnitAware unitAware(T value, Unit unit, BiFunction scaling) { + return unitAware( + value, + unit, + desiredUnit -> unitAware(scaling.apply(value, unit.multiplier / desiredUnit.multiplier), desiredUnit, scaling)); + } + + static > Comparator> comparator() { + return Comparator.comparing(identity(), (p, q) -> p.value().compareTo(q.value(p.unit()))); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareOperations.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareOperations.java new file mode 100644 index 0000000000..c2a8ff0a6a --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareOperations.java @@ -0,0 +1,59 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware.unitAware; + +/** + * Utilities for working with unit-aware objects correctly. + * Primarily includes arithmetic and comparison functions. + */ +public final class UnitAwareOperations { + private UnitAwareOperations() {} + + public static UnitAware add(BiFunction scaling, BiFunction addition, UnitAware a, UnitAware b) { + return unitAware(addition.apply(a.value(), b.value(a.unit())), a.unit(), scaling); + } + + public static UnitAware subtract(BiFunction scaling, BiFunction subtraction, UnitAware a, UnitAware b) { + return add(scaling, subtraction, a, b); + } + + public static UnitAware multiply(BiFunction scaling, BiFunction multiplication, UnitAware a, UnitAware b) { + return unitAware(multiplication.apply(a.value(), b.value()), a.unit().multiply(b.unit()), scaling); + } + + public static UnitAware divide(BiFunction scaling, BiFunction division, UnitAware a, UnitAware b) { + return unitAware(division.apply(a.value(), b.value()), a.unit().divide(b.unit()), scaling); + } + + public static UnitAware integrate(BiFunction scaling, BiFunction integration, UnitAware a, UnitAware b) { + final Unit newUnit = a.unit().multiply(StandardUnits.SECOND); + return unitAware(integration.apply(a.value(), b.value(newUnit)), newUnit, scaling); + } + + public static UnitAware differentiate(BiFunction scaling, Function differentiation, UnitAware a) { + return unitAware(differentiation.apply(a.value()), a.unit().divide(StandardUnits.SECOND), scaling); + } + + public static > int compare(UnitAware a, UnitAware b) { + return a.value().compareTo(b.value(a.unit())); + } + + public static > boolean lessThan(UnitAware a, UnitAware b) { + return compare(a, b) < 0; + } + + public static > boolean lessThanOrEquals(UnitAware a, UnitAware b) { + return compare(a, b) <= 0; + } + + public static > boolean greaterThan(UnitAware a, UnitAware b) { + return compare(a, b) > 0; + } + + public static > boolean greaterThanOrEquals(UnitAware a, UnitAware b) { + return compare(a, b) >= 0; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareResources.java new file mode 100644 index 0000000000..c26fcce427 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/UnitAwareResources.java @@ -0,0 +1,65 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.DynamicsEffect; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Quantities.quantity; + +public final class UnitAwareResources { + private UnitAwareResources() {} + + public static UnitAware> unitAware(Resource resource, Unit unit, BiFunction scaling) { + return UnitAware.unitAware(resource, unit, extend(scaling, ResourceMonad::map)); + } + + public static > UnitAware> unitAware(MutableResource resource, Unit unit, BiFunction scaling) { + final BiFunction>, Double, ErrorCatching>> extendedScaling = extend(scaling, DynamicsMonad::map); + return UnitAware.unitAware(resource, unit, (cellResource, scale) -> new MutableResource() { + @Override + public void emit(final DynamicsEffect effect) { + // Use an effect in the scaled domain by first scaling the dynamics, + // then applying the effect, then de-scaling the result back + DynamicsEffect scaledEffect = unscaledDynamics -> + extendedScaling.apply(effect.apply(extendedScaling.apply(unscaledDynamics, scale)), 1 / scale); + name(effect, "%s", scaledEffect); + cellResource.emit(scaledEffect); + } + + @Override + public ErrorCatching> getDynamics() { + return extendedScaling.apply(cellResource.getDynamics(), scale); + } + }); + } + + public static BiFunction extend(BiFunction scaling, BiFunction, MA> map) { + return (ma, s) -> map.apply(ma, a -> scaling.apply(a, s)); + } + + public static BiFunction, Double, Resource> extend(BiFunction scaling) { + return extend(scaling, ResourceMonad::map); + } + + public static > UnitAware currentValue(UnitAware> resource, UnitAware valueIfError) { + return quantity(Resources.currentValue(resource.value(), valueIfError.value(resource.unit())), resource.unit()); + } + + public static > UnitAware currentValue(UnitAware> resource) { + return quantity(Resources.currentValue(resource.value()), resource.unit()); + } + + public static > UnitAware> cache(UnitAware> resource) { + return resource.map(Resources::cache); + } +} From f294683a532cc4eb68ebb221ff2d7c134acccf37 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 18:20:24 -0800 Subject: [PATCH 026/159] Add Unstructured and Differentiable resources. Unlike other dynamics types, Unstructured and Differentiable dynamics do not present enough information to globally describe their behavior. This means they can express functions that are not otherwise exactly representable, like trignometry functions for Differentiable, or even calls to external libraries through Unstructured. Unstructured resources can represent any function of time and/or other resources, but need to be approximated before being given to a Registrar or some other components. Unstructured resources can also represent continuously-varying values of unusual types. For example, a string representing the current time down to the microsecond. While these kinds of values are not often registered directly, they can form intermediate steps when deriving other (unstructured) resources, which can be approximated and registered. While the Unstructured type itself forms a monad over the values it contains, it does not compose with Resource or Dynamics monads. Instead, we revert to an applicative, which still lets us write derivations on unstructured resources as functions on their plain values. --- .../modeling/black_box/Differentiable.java | 122 ++++ .../black_box/DifferentiableResources.java | 109 ++++ .../modeling/black_box/Unstructured.java | 59 ++ .../black_box/UnstructuredResources.java | 79 +++ .../UnstructuredDynamicsApplicative.java | 285 ++++++++++ .../black_box/monads/UnstructuredMonad.java | 531 ++++++++++++++++++ .../UnstructuredResourceApplicative.java | 283 ++++++++++ 7 files changed, 1468 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Differentiable.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DifferentiableResources.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Unstructured.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredDynamicsApplicative.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredMonad.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredResourceApplicative.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Differentiable.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Differentiable.java new file mode 100644 index 0000000000..9fac486208 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Differentiable.java @@ -0,0 +1,122 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.function.DoubleUnaryOperator; +import java.util.function.UnaryOperator; + +public interface Differentiable extends Dynamics { + Differentiable derivative(); + + static Differentiable constant(double value) { + return new Differentiable() { + @Override + public Differentiable derivative() { + return constant(0); + } + + @Override + public Double extract() { + return value; + } + + @Override + public Differentiable step(Duration t) { + return this; + } + }; + } + + static Differentiable differentiable(Differentiable d, DoubleUnaryOperator f, UnaryOperator fPrime) { + return new Differentiable() { + @Override + public Differentiable derivative() { + return fPrime.apply(d).multiply(d.derivative()); + } + + @Override + public Double extract() { + return f.applyAsDouble(d.extract()); + } + + @Override + public Differentiable step(final Duration t) { + return differentiable(d.step(t), f, fPrime); + } + }; + } + + default Differentiable add(Differentiable d) { + return new Differentiable() { + @Override + public Differentiable derivative() { + return Differentiable.this.derivative().add(d.derivative()); + } + + @Override + public Double extract() { + return Differentiable.this.extract() + d.extract(); + } + + @Override + public Differentiable step(final Duration t) { + return Differentiable.this.step(t).add(d.step(t)); + } + }; + } + + default Differentiable subtract(Differentiable d) { + return Differentiable.this.add(d.multiply(-1)); + } + + default Differentiable multiply(double scalar) { + return new Differentiable() { + @Override + public Differentiable derivative() { + return Differentiable.this.derivative().multiply(scalar); + } + + @Override + public Double extract() { + return Differentiable.this.derivative().extract() * scalar; + } + + @Override + public Differentiable step(final Duration t) { + return Differentiable.this.derivative().step(t).multiply(scalar); + } + }; + } + + default Differentiable multiply(Differentiable d) { + return new Differentiable() { + @Override + public Differentiable derivative() { + return Differentiable.this.derivative().multiply(d).add(Differentiable.this.multiply(d.derivative())); + } + + @Override + public Double extract() { + return Differentiable.this.extract() * d.extract(); + } + + @Override + public Differentiable step(final Duration t) { + return Differentiable.this.step(t).multiply(d.step(t)); + } + }; + } + + default Differentiable divide(double scalar) { + return multiply(1 / scalar); + } + + default Differentiable divide(Differentiable d) { + return this.multiply(d.power(-1)); + } + + default Differentiable power(int exponent) { + return differentiable(this, x -> Math.pow(x, exponent), d -> d.power(exponent - 1).multiply(exponent)); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DifferentiableResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DifferentiableResources.java new file mode 100644 index 0000000000..d2c75de030 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DifferentiableResources.java @@ -0,0 +1,109 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.reduce; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.map; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.name; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Differentiable.differentiable; +import static java.util.Arrays.stream; + +public final class DifferentiableResources { + private DifferentiableResources() {} + + public static Resource constant(double value) { + var result = ResourceMonad.pure(Differentiable.constant(value)); + name(result, Double.toString(value)); + return result; + } + + public static Differentiable asDifferentiable(Polynomial polynomial) { + return new Differentiable() { + @Override + public Differentiable derivative() { + return asDifferentiable(polynomial.derivative()); + } + + @Override + public Double extract() { + return polynomial.extract(); + } + + @Override + public Differentiable step(final Duration t) { + return asDifferentiable(polynomial.step(t)); + } + }; + } + + public static Resource asDifferentiable(Resource polynomial) { + var result = map(polynomial, DifferentiableResources::asDifferentiable); + name(result, "%s", polynomial); + return result; + } + + @SafeVarargs + public static Resource add(Resource... summands) { + return sum(stream(summands)); + } + + public static Resource sum(Stream> summands) { + return reduce(summands, constant(0), map(Differentiable::add), "Sum"); + } + + public static Resource subtract(Resource left, Resource right) { + var result = map(left, right, Differentiable::subtract); + name(result, "(%s) - (%s)", left, right); + return result; + } + + @SafeVarargs + public static Resource multiply(Resource... factors) { + return product(stream(factors)); + } + + public static Resource product(Stream> factors) { + return reduce(factors, constant(0), map(Differentiable::multiply), "Product"); + } + + public static Resource divide(Resource left, Resource right) { + var result = map(left, right, Differentiable::subtract); + name(result, "(%s) / (%s)", left, right); + return result; + } + + private static Differentiable cos(Differentiable argument) { + return differentiable(argument, Math::cos, d -> sin(d).multiply(-1)); + } + + private static Differentiable sin(Differentiable argument) { + return differentiable(argument, Math::sin, DifferentiableResources::cos); + } + + private static Differentiable exp(Differentiable argument) { + return differentiable(argument, Math::exp, DifferentiableResources::exp); + } + + public static Resource sin(Resource argument) { + var result = map(argument, DifferentiableResources::sin); + name(result, "sin(%s)", argument); + return result; + } + + public static Resource cos(Resource argument) { + var result = map(argument, DifferentiableResources::cos); + name(result, "cos(%s)", argument); + return result; + } + + public static Resource exp(Resource argument) { + var result = map(argument, DifferentiableResources::exp); + name(result, "exp(%s)", argument); + return result; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Unstructured.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Unstructured.java new file mode 100644 index 0000000000..00bd47e977 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Unstructured.java @@ -0,0 +1,59 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + +/** + * Dynamics with no observable structure. + * While very general, these need to be approximated by more structured + * dynamics to report out to Aerie. + */ +public interface Unstructured extends Dynamics> { + static Unstructured constant(T value) { + return new Unstructured() { + @Override + public T extract() { + return value; + } + + @Override + public Unstructured step(Duration t) { + return this; + } + }; + } + + static Unstructured timeBased(Function valueOverTime) { + return new Unstructured() { + @Override + public T extract() { + return valueOverTime.apply(ZERO); + } + + @Override + public Unstructured step(final Duration t) { + return timeBased(valueOverTime.compose(t::plus)); + } + }; + } + + static > Unstructured unstructured(D dynamics) { + return new Unstructured<>() { + @Override + public T extract() { + return dynamics.extract(); + } + + @Override + public Unstructured step(Duration t) { + return unstructured(dynamics.step(t)); + } + }; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java new file mode 100644 index 0000000000..faba22ce1f --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java @@ -0,0 +1,79 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.monads.UnstructuredResourceApplicative; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.map; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.name; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.approximate; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.relative; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.byBoundingError; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.secantApproximation; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.*; + +public final class UnstructuredResources { + private UnstructuredResources() {} + + public static Resource> constant(A value) { + var result = UnstructuredResourceApplicative.pure(value); + name(result, value.toString()); + return result; + } + + public static Resource> timeBased(Function f) { + // Put this in a cell so it'll be stepped up appropriately + return resource(Unstructured.timeBased(f)); + } + + public static > Resource> asUnstructured(Resource resource) { + return map(resource, Unstructured::unstructured); + } + + /** + * {@link UnstructuredResources#approximateAsLinear(Resource, double)} + * with relativeError = 1e-2 + */ + public static Resource approximateAsLinear(Resource> resource) { + return approximateAsLinear(resource, 1e-2); + } + + /** + * {@link UnstructuredResources#approximateAsLinear(Resource, double, double)} + * with epsilon = 1e-10 + */ + public static Resource approximateAsLinear(Resource> resource, double relativeError) { + return approximateAsLinear(resource, relativeError, 1e-10); + } + + /** + * Builds a linear approximation of resource, using generally acceptable default settings. + * For more control over the approximation, see {@link Approximation#approximate} and related methods. + * + * @param resource The resource to approximate + * @param relativeError The maximum relative error to tolerate in the approximation + * @param epsilon The minimum positive value to distinguish from zero. This avoids oversampling near zero. + * + * @see Approximation#approximate + * @see SecantApproximation#secantApproximation + * @see IntervalFunctions#byBoundingError + * @see IntervalFunctions#byUniformSampling + * @see SecantApproximation.ErrorEstimates#errorByOptimization() + * @see Approximation#relative + */ + public static Resource approximateAsLinear(Resource> resource, double relativeError, double epsilon) { + return approximate(resource, + secantApproximation(byBoundingError( + relativeError, + MINUTE, + duration(24 * 30, HOUR), + relative(ErrorEstimates.>errorByOptimization(), epsilon)))); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredDynamicsApplicative.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredDynamicsApplicative.java new file mode 100644 index 0000000000..263007a7d7 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredDynamicsApplicative.java @@ -0,0 +1,285 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Unstructured; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +/** + * The applicative functor (but not a monad) formed by composing + * {@link DynamicsMonad} with {@link UnstructuredMonad}. + */ +public final class UnstructuredDynamicsApplicative { + private UnstructuredDynamicsApplicative() {} + + public static ErrorCatching>> pure(A a) { + return DynamicsMonad.pure(UnstructuredMonad.pure(a)); + } + + public static ErrorCatching>> apply(ErrorCatching>> a, ErrorCatching>>> f) { + return DynamicsMonad.apply(a, DynamicsMonad.map(f, UnstructuredMonad::apply)); + } + + // Unstructured>> has a success status and expiry that can vary with time, as the dynamics are stepped forward. + // ErrorCatching>> has a single success status and expiry, and only the value varies over time. + // Since the direction required below would lose information, we can't write it in general. + // This downgrades this structure to an applicative functor, rather than a monad. + + // private static ErrorCatching>> distribute(Unstructured>> a) { + // } + + // public static ErrorCatching>> join(ErrorCatching>>>>> a) { + // return DynamicsMonad.map(DynamicsMonad.join(DynamicsMonad.map(a, UnstructuredDynamicsMonad::distribute)), UnstructuredMonad::join); + // } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function>>, ErrorCatching>>> apply(ErrorCatching>>> f) { + return a -> apply(a, f); + } + + public static ErrorCatching>> map(ErrorCatching>> a, Function f) { + return apply(a, pure(f)); + } + + public static Function>>, ErrorCatching>>> map(Function f) { + return apply(pure(f)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction>>, ErrorCatching>>, ErrorCatching>>> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, ErrorCatching>> t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static ErrorCatching>> map(ErrorCatching>> a, ErrorCatching>> b, ErrorCatching>> c, ErrorCatching>> d, ErrorCatching>> e, ErrorCatching>> f, ErrorCatching>> g, ErrorCatching>> h, ErrorCatching>> i, ErrorCatching>> j, ErrorCatching>> k, ErrorCatching>> l, ErrorCatching>> m, ErrorCatching>> n, ErrorCatching>> o, ErrorCatching>> p, ErrorCatching>> q, ErrorCatching>> r, ErrorCatching>> s, ErrorCatching>> t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>, ErrorCatching>>> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredMonad.java new file mode 100644 index 0000000000..b55b620a08 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredMonad.java @@ -0,0 +1,531 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Unstructured; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +/** + * The {@link Unstructured} monad + */ +public final class UnstructuredMonad { + private UnstructuredMonad() {} + + public static Unstructured pure(A a) { + return Unstructured.constant(a); + } + + public static Unstructured apply(Dynamics a, Dynamics, ?> f) { + return new Unstructured<>() { + @Override + public B extract() { + return f.extract().apply(a.extract()); + } + + @Override + public Unstructured step(Duration t) { + return apply(a.step(t), f.step(t)); + } + }; + } + + public static Unstructured join(Dynamics, ?> a) { + return new Unstructured<>() { + @Override + public A extract() { + return a.extract().extract(); + } + + @Override + public Unstructured step(Duration t) { + return join(a.step(t)); + } + }; + } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function, Unstructured> apply(Unstructured> f) { + return a -> apply(a, f); + } + + public static Unstructured map(Unstructured a, Function f) { + return apply(a, pure(f)); + } + + public static Function, Unstructured> map(Function f) { + return apply(pure(f)); + } + + public static Unstructured bind(Unstructured a, Function> f) { + return join(map(a, f)); + } + + public static Function, Unstructured> bind(Function> f) { + return a -> bind(a, f); + } + + public static Unstructured map(Unstructured a, Unstructured b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction, Unstructured, Unstructured> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, BiFunction> function) { + return join(map(a, b, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Function>> function) { + return join(map(a, b, function)); + } + + public static BiFunction, Unstructured, Unstructured> bind(BiFunction> function) { + return (a, b) -> bind(a, b, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction, Unstructured, Unstructured, Unstructured> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, TriFunction> function) { + return join(map(a, b, c, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Function>>> function) { + return join(map(a, b, c, function)); + } + + public static TriFunction, Unstructured, Unstructured, Unstructured> bind(TriFunction> function) { + return (a, b, c) -> bind(a, b, c, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4, Unstructured, Unstructured, Unstructured, Unstructured> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Function4> function) { + return join(map(a, b, c, d, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Function>>>> function) { + return join(map(a, b, c, d, function)); + } + + public static Function4, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function4> function) { + return (a, b, c, d) -> bind(a, b, c, d, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Function5> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Function>>>>> function) { + return join(map(a, b, c, d, e, function)); + } + + public static Function5, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function5> function) { + return (a, b, c, d, e) -> bind(a, b, c, d, e, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Function6> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Function>>>>>> function) { + return join(map(a, b, c, d, e, f, function)); + } + + public static Function6, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function6> function) { + return (a, b, c, d, e, f) -> bind(a, b, c, d, e, f, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Function7> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Function>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, function)); + } + + public static Function7, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function7> function) { + return (a, b, c, d, e, f, g) -> bind(a, b, c, d, e, f, g, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Function8> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Function>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, function)); + } + + public static Function8, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function8> function) { + return (a, b, c, d, e, f, g, h) -> bind(a, b, c, d, e, f, g, h, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Function9> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Function>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function9, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function9> function) { + return (a, b, c, d, e, f, g, h, i) -> bind(a, b, c, d, e, f, g, h, i, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Function10> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Function>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function10, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function10> function) { + return (a, b, c, d, e, f, g, h, i, j) -> bind(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Function11> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Function>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function11, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function11> function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> bind(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Function12> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Function>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function12, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function12> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Function13> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Function>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function13, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function13> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Function14> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Function>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function14, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function14> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Function15> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Function>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function15, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function15> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Function16> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Function>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function16, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured> bind(Function16> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Function17> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Function>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function17, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured> bind(Function17> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured, Unstructured> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Function18> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Function>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function18, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured, Unstructured> bind(Function18> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured, Unstructured, Unstructured> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Function19> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Function>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function19, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function19> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Unstructured t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Unstructured t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Unstructured t, Function20> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Unstructured bind(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Unstructured q, Unstructured r, Unstructured s, Unstructured t, Function>>>>>>>>>>>>>>>>>>>> function) { + return join(map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function)); + } + + public static Function20, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured

, Unstructured, Unstructured, Unstructured, Unstructured, Unstructured> bind(Function20> function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> bind(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredResourceApplicative.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredResourceApplicative.java new file mode 100644 index 0000000000..77ce3184be --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/monads/UnstructuredResourceApplicative.java @@ -0,0 +1,283 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.monads; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Unstructured; +import gov.nasa.jpl.aerie.contrib.streamline.utils.*; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; + +/** + * The applicative (not a monad) formed by composing + * {@link ResourceMonad} and {@link UnstructuredMonad} + */ +public final class UnstructuredResourceApplicative { + private UnstructuredResourceApplicative() {} + + public static Resource> pure(A a) { + return ResourceMonad.pure(UnstructuredMonad.pure(a)); + } + + public static Resource> apply(Resource> a, Resource>> f) { + return ResourceMonad.apply(a, ResourceMonad.map(f, UnstructuredMonad::apply)); + } + + // Unstructured> has a success status and expiry that can vary with time, as the dynamics are stepped forward. + // Resource> has a single success status and expiry once getDynamics is called. + // Since the direction required below would lose information, we can't write it in general. + // This downgrades this structure to an applicative functor, rather than a monad. + // private static Resource> distribute(Unstructured> a) { + // } + + // public static Resource> join(Resource>>> a) { + // return ResourceMonad.map(ResourceMonad.join(ResourceMonad.map(a, UnstructuredResourceMonad::distribute)), UnstructuredMonad::join); + // } + + // GENERATED CODE START + // Supplemental methods generated by generate_monad_methods.py on 2023-12-06. + + public static Function>, Resource>> apply(Resource>> f) { + return a -> apply(a, f); + } + + public static Resource> map(Resource> a, Function f) { + return apply(a, pure(f)); + } + + public static Function>, Resource>> map(Function f) { + return apply(pure(f)); + } + + public static Resource> map(Resource> a, Resource> b, BiFunction function) { + return map(a, b, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Function> function) { + return apply(b, map(a, function)); + } + + public static BiFunction>, Resource>, Resource>> map(BiFunction function) { + return (a, b) -> map(a, b, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, TriFunction function) { + return map(a, b, c, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Function>> function) { + return apply(c, map(a, b, function)); + } + + public static TriFunction>, Resource>, Resource>, Resource>> map(TriFunction function) { + return (a, b, c) -> map(a, b, c, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Function4 function) { + return map(a, b, c, d, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Function>>> function) { + return apply(d, map(a, b, c, function)); + } + + public static Function4>, Resource>, Resource>, Resource>, Resource>> map(Function4 function) { + return (a, b, c, d) -> map(a, b, c, d, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Function5 function) { + return map(a, b, c, d, e, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Function>>>> function) { + return apply(e, map(a, b, c, d, function)); + } + + public static Function5>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function5 function) { + return (a, b, c, d, e) -> map(a, b, c, d, e, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Function6 function) { + return map(a, b, c, d, e, f, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Function>>>>> function) { + return apply(f, map(a, b, c, d, e, function)); + } + + public static Function6>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function6 function) { + return (a, b, c, d, e, f) -> map(a, b, c, d, e, f, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Function7 function) { + return map(a, b, c, d, e, f, g, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Function>>>>>> function) { + return apply(g, map(a, b, c, d, e, f, function)); + } + + public static Function7>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function7 function) { + return (a, b, c, d, e, f, g) -> map(a, b, c, d, e, f, g, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Function8 function) { + return map(a, b, c, d, e, f, g, h, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Function>>>>>>> function) { + return apply(h, map(a, b, c, d, e, f, g, function)); + } + + public static Function8>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function8 function) { + return (a, b, c, d, e, f, g, h) -> map(a, b, c, d, e, f, g, h, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Function9 function) { + return map(a, b, c, d, e, f, g, h, i, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Function>>>>>>>> function) { + return apply(i, map(a, b, c, d, e, f, g, h, function)); + } + + public static Function9>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function9 function) { + return (a, b, c, d, e, f, g, h, i) -> map(a, b, c, d, e, f, g, h, i, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Function10 function) { + return map(a, b, c, d, e, f, g, h, i, j, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Function>>>>>>>>> function) { + return apply(j, map(a, b, c, d, e, f, g, h, i, function)); + } + + public static Function10>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function10 function) { + return (a, b, c, d, e, f, g, h, i, j) -> map(a, b, c, d, e, f, g, h, i, j, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Function11 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Function>>>>>>>>>> function) { + return apply(k, map(a, b, c, d, e, f, g, h, i, j, function)); + } + + public static Function11>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function11 function) { + return (a, b, c, d, e, f, g, h, i, j, k) -> map(a, b, c, d, e, f, g, h, i, j, k, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Function12 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Function>>>>>>>>>>> function) { + return apply(l, map(a, b, c, d, e, f, g, h, i, j, k, function)); + } + + public static Function12>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function12 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l) -> map(a, b, c, d, e, f, g, h, i, j, k, l, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Function13 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Function>>>>>>>>>>>> function) { + return apply(m, map(a, b, c, d, e, f, g, h, i, j, k, l, function)); + } + + public static Function13>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function13 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Function14 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Function>>>>>>>>>>>>> function) { + return apply(n, map(a, b, c, d, e, f, g, h, i, j, k, l, m, function)); + } + + public static Function14>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function14 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Function15 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Function>>>>>>>>>>>>>> function) { + return apply(o, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, function)); + } + + public static Function15>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function15 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Function>>>>>>>>>>>>>>> function) { + return apply(p, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, function)); + } + + public static Function16>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function16 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Function17 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Function>>>>>>>>>>>>>>>> function) { + return apply(q, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, function)); + } + + public static Function17>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function17 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Function18 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Function>>>>>>>>>>>>>>>>> function) { + return apply(r, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, function)); + } + + public static Function18>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function18 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Function19 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Function>>>>>>>>>>>>>>>>>> function) { + return apply(s, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, function)); + } + + public static Function19>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function19 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Resource> t, Function20 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, curry(function)); + } + + public static Resource> map(Resource> a, Resource> b, Resource> c, Resource> d, Resource> e, Resource> f, Resource> g, Resource> h, Resource> i, Resource> j, Resource> k, Resource> l, Resource> m, Resource> n, Resource> o, Resource> p, Resource> q, Resource> r, Resource> s, Resource> t, Function>>>>>>>>>>>>>>>>>>> function) { + return apply(t, map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, function)); + } + + public static Function20>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>, Resource>> map(Function20 function) { + return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) -> map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, function); + } + // GENERATED CODE END +} From 1653ed3c1105a41dd5557698a3214192742edd21 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 18:28:24 -0800 Subject: [PATCH 027/159] Add approximation utilities Adds utilities for constructing approximations. These are particularly useful for Unstructured resources with Double as a value, but some approximations generalize beyond this. Approximation is broken into several stages: 1. Choose an approximation type. The ready-made choices here are "discrete", "secant", or "Taylor" approximations, but the interface permits others to be defined later. 2. Choose a strategy for running those approximations - choosing a divergence estimator or interval function, depending on the approximation type. There are ready-made strategies for using uniformly-spaced samples or attempting to bound the final error. 3. If error-bounding is used, choose an error tolerance and error measurement. Again, there are several ready-made options, based on both direct methods and analytic estimates, depending on the available information. Further parameters may be needed to configure these error estimates. Breaking the problem down like this allows for more focused testing, as well as component re-use. Doing approximation on an Unstructured resource allows the value of the result to influence the sampling strategy - rather than heuristically choosing a sampling strategy to get "good enough" data, we can measure the result to choose samples. This can be an efficiency boon when the approximated resource is cheap, but downstream resources or tasks are expensive. --- .../modeling/black_box/Approximation.java | 97 ++++++++++++ .../black_box/DiscreteApproximation.java | 32 ++++ .../black_box/DivergenceEstimators.java | 38 +++++ .../modeling/black_box/IntervalFunctions.java | 84 ++++++++++ .../black_box/SecantApproximation.java | 147 ++++++++++++++++++ .../black_box/TaylorApproximation.java | 33 ++++ 6 files changed, 431 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Approximation.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DiscreteApproximation.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DivergenceEstimators.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximation.java create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/TaylorApproximation.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Approximation.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Approximation.java new file mode 100644 index 0000000000..75197d3001 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/Approximation.java @@ -0,0 +1,97 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.wheneverUpdates; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.expires; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ExpiringMonad.bind; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; + +/** + * General framework for approximating resources. + */ +public final class Approximation { + private Approximation() {} + + /** + * Approximate a resource. + * Result updates whenever resource updates or approximation expires. + */ + public static , E extends Dynamics> Resource approximate( + Resource resource, Function, Expiring> approximation) { + var result = resource(resource.getDynamics().map(approximation)); + // Register the "updates" and "expires" conditions separately + // so that the "updates" condition isn't triggered spuriously. + wheneverUpdates(resource, newResourceDynamics -> updateApproximation(newResourceDynamics, approximation, result)); + whenever(expires(result), () -> updateApproximation(resource.getDynamics(), approximation, result)); + // Approximation is often used when registering resources, so result propagates its name to resource. + name(resource, "%s", result); + addDependency(result, resource); + return result; + } + + private static , E extends Dynamics> void updateApproximation( + ErrorCatching> resourceDynamics, Function, Expiring> approximation, MutableResource result) { + var newDynamics = resourceDynamics.map(approximation); + result.emit("Update approximation to " + newDynamics, $ -> newDynamics); + } + + /** + * Build an approximation by first choosing an interval, then approximating over that interval. + */ + public static Function, Expiring> intervalApproximation( + BiFunction, Duration, Expiring> intervalApproximation, Function, Duration> intervalSelector) { + return d -> intervalApproximation.apply(d, intervalSelector.apply(d)); + } + + /** + * Build an approximation by first choosing an approximating dynamics, + * then estimating when that approximation diverges. + */ + public static Function, Expiring> divergingApproximation( + Function baseApproximation, BiFunction divergenceEstimator) { + return d -> bind(d, d$ -> { + var e$ = baseApproximation.apply(d$); + return expiring(e$, divergenceEstimator.apply(d$, e$)); + }); + } + + /** + * Interprets maximumError as a relative error, using the given absolute error estimate. + *

+ * Gets the function value at the interval midpoint to estimate a magnitude M, + * then computes absolute error as (relative error) * (M + epsilon). + *

+ *

+ * Epsilon should be close to the minimum value that is functionally different from zero for your application, + * to give enough accuracy everywhere without over-sampling near zero. + *

+ */ + public static > Function, Double> relative( + Function, Double> absoluteErrorEstimate, double epsilon) { + if (epsilon <= 0) { + throw new IllegalArgumentException("epsilon must be positive"); + } + return input -> { + var d = input.actualDynamics(); + var t = input.intervalInSeconds(); + var maxRelativeError = input.maximumError(); + var valueMagnitude = Math.abs(d.step(Duration.roundNearest(t / 2, SECOND)).extract()); + var maxAbsoluteError = maxRelativeError * (valueMagnitude + epsilon); + return absoluteErrorEstimate.apply(new IntervalFunctions.ErrorEstimateInput<>(d, t, maxAbsoluteError)) / (valueMagnitude + epsilon); + }; + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DiscreteApproximation.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DiscreteApproximation.java new file mode 100644 index 0000000000..b8578d8c11 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DiscreteApproximation.java @@ -0,0 +1,32 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.divergingApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; + +/** + * Utilities to build discrete approximations of {@link Unstructured} resources. + */ +public final class DiscreteApproximation { + private DiscreteApproximation() {} + + /** + * Build an approximation function, for use with {@link Approximation#approximate}, which takes discrete samples. + * Uses the provided divergence estimator to determine when each sample expires. + * + *

+ * Pre-built divergence estimators are available in {@link DivergenceEstimators}. + *

+ */ + public static > Function, Expiring>> discreteApproximation( + BiFunction, Duration> divergenceEstimator) { + return divergingApproximation(d -> discrete(d.extract()), divergenceEstimator); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DivergenceEstimators.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DivergenceEstimators.java new file mode 100644 index 0000000000..0667b1fcae --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/DivergenceEstimators.java @@ -0,0 +1,38 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; + +public final class DivergenceEstimators { + private DivergenceEstimators() {} + + /** + * Uses a direct solver to estimate when an approximation diverges beyond a maximum tolerable error. + * Assumes approximation diverges monotonically from the true value. + * Requires minimum and maximum sample period to ensure efficient convergence. + */ + public static , E extends Dynamics> BiFunction byBoundingError(double maximumError, Duration minimumSamplePeriod, Duration maximumSamplePeriod, BiFunction errorEstimate) { + // Calculating divergence time is just calculating a sample interval, with a slightly different signature. + return (d, e) -> IntervalFunctions.byBoundingError( + maximumError, + minimumSamplePeriod, + maximumSamplePeriod, + input -> { + var T = Duration.roundNearest(input.intervalInSeconds(), SECOND); + return errorEstimate.apply(d.step(T).extract(), e.step(T).extract()); + }).apply(Expiring.neverExpiring(d)); + } + + /** + * Approximation diverges in a time independent of approximation itself. + */ + public static BiFunction byTime(Supplier divergenceTime) { + return (d, e) -> divergenceTime.get(); + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java new file mode 100644 index 0000000000..3024d654d9 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java @@ -0,0 +1,84 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.solvers.BrentSolver; +import org.apache.commons.math3.exception.NoBracketingException; +import org.apache.commons.math3.exception.NumberIsTooLargeException; +import org.apache.commons.math3.exception.TooManyEvaluationsException; + +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; + +public final class IntervalFunctions { + private IntervalFunctions() {} + + /** + * Use uniform point spacing for an approximation + */ + public static Function, Duration> byUniformSampling(Duration samplePeriod) { + return exp -> Duration.min(samplePeriod, exp.expiry().value().orElse(Duration.MAX_VALUE)); + } + + /** + * Chooses sample intervals which attempt to bound the maximum error. + * To ensure efficient convergence, a minimum and maximum size for the interval must be supplied. + * Error is measured by the provided error estimate function. + * Pre-built error functions are available in {@link SecantApproximation.ErrorEstimates}. + * For example, + *
+   * Resource<Differentiable> real;
+   * Resource<Linear> approx = secantApproximation(real,
+   *     byBoundingError(1e-4, Duration.MINUTE, Duration.HOUR,
+   *         relative(errorByQuadraticApproximation(), 1e-8)));
+   * 
+ */ + public static > Function, Duration> byBoundingError( + double maximumError, + Duration minimumSamplePeriod, + Duration maximumSamplePeriod, + Function, Double> errorEstimate) + { + if (maximumError <= 0) { + throw new IllegalArgumentException("maximumError must be positive"); + } + if (!minimumSamplePeriod.isPositive()) { + throw new IllegalArgumentException("minimumSamplePeriod must be positive"); + } + if (maximumSamplePeriod.shorterThan(minimumSamplePeriod)) { + throw new IllegalArgumentException("maximumSamplePeriod must be at least minimumSamplePeriod"); + } + + return exp -> { + var e = exp.expiry().value().orElse(Duration.MAX_VALUE); + var solver = new BrentSolver(); + UnivariateFunction errorFn = t -> maximumError - errorEstimate.apply(new ErrorEstimateInput<>( + exp.data(), + t, + maximumError)); + try { + double intervalSize = solver.solve( + 100, + errorFn, + Duration.min(e, minimumSamplePeriod).ratioOver(SECOND), + Duration.min(e, maximumSamplePeriod).ratioOver(SECOND)); + return Duration.roundNearest(intervalSize, SECOND); + } catch (NoBracketingException x) { + if (errorFn.value(minimumSamplePeriod.ratioOver(SECOND)) > 0) { + // maximum error > estimated error, best case + return maximumSamplePeriod; + } else { + // maximum error < estimated error, worst case + return minimumSamplePeriod; + } + } catch (TooManyEvaluationsException | NumberIsTooLargeException x) { + return minimumSamplePeriod; + } + }; + } + + public record ErrorEstimateInput(D actualDynamics, Double intervalInSeconds, Double maximumError) {} +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximation.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximation.java new file mode 100644 index 0000000000..13682cbec4 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximation.java @@ -0,0 +1,147 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ExpiringMonad; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.ErrorEstimateInput; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.math3.exception.TooManyEvaluationsException; +import org.apache.commons.math3.optim.MaxEval; +import org.apache.commons.math3.optim.nonlinear.scalar.GoalType; +import org.apache.commons.math3.optim.univariate.BrentOptimizer; +import org.apache.commons.math3.optim.univariate.SearchInterval; +import org.apache.commons.math3.optim.univariate.UnivariateObjectiveFunction; + +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.expiring; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.intervalApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear.linear; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; + +/** + * Utilities to build a secant approximation of {@link Unstructured} or {@link Differentiable} resources. + */ +public final class SecantApproximation { + private SecantApproximation() {} + + /** + * Approximate a resource using secants. + * Intervals for the secants are given using an interval function. + * Pre-built interval functions are available in {@link IntervalFunctions}. + * For example: + *
+   * Resource<BlackBox<Double>> real;
+   * Resource<Linear> approx = approximate(resource, secantApproximation(byUniformSampling(Duration.HOUR)));
+   * 
+ */ + public static > Function, Expiring> secantApproximation(Function, Duration> intervalFunction) { + return intervalApproximation(SecantApproximation::secant, intervalFunction); + } + + private static Expiring secant(Expiring> d, Duration interval) { + return ExpiringMonad.bind(d, d$ -> expiring(secant(d$, interval), interval)); + } + + public static Linear secant(Dynamics d, Duration interval) { + var s = d.extract(); + var e = d.step(interval).extract(); + return linear(s, (e - s) / interval.ratioOver(SECOND)); + } + + public static final class ErrorEstimates { + private ErrorEstimates() {} + + /** + * Expands a second-order Taylor approximation at the midpoint of a proposed interval + * to estimate the error of taking a secant across that interval. + */ + public static Function, Double> errorByQuadraticApproximation() { + return input -> { + var d = input.actualDynamics(); + var t = input.intervalInSeconds(); + + // Shift d to the interval midpoint + var dMidpoint = d.step(Duration.roundNearest(t / 2, SECOND)); + var dPrimeMidpoint = dMidpoint.derivative(); + + // Taylor expansion at the midpoint + var midpointValue = dMidpoint.extract(); + var midpointSlope = dPrimeMidpoint.extract(); + var midpointCurvature = dPrimeMidpoint.derivative().extract(); + + // Secant across the interval + var tDuration = Duration.roundNearest(t, SECOND); + var secant = secant(d, tDuration); + var secantStartValue = secant.extract(); + var secantSlope = secant.rate(); + var secantMidValue = secantStartValue + (secantSlope * t / 2); + + // Error is measured by Taylor expansion minus secant, which is a quadratic ax^2 + bx + c + // Note that this curve is expressed with x = 0 at the interval midpoint + var a = midpointCurvature / 2; + var b = midpointSlope - secantSlope; + var c = midpointValue - secantMidValue; + + double extremumError; + if (a == 0) { + extremumError = 0; + } else { + // Find the location of the extremum: + var extremePoint = -b / (2 * a); + // If the extreme point is within the interval, consider the error there + extremumError = ((-t / 2) < extremePoint && extremePoint < (t / 2)) + ? Math.abs(c - (b * b / (4 * a))) : 0; + } + + // Also evaluate the error function at +/- (t/2), the interval start and end points. + var startSecantError = Math.abs((a * t * t / 4) - (b * t / 2) + c); + var endSecantError = Math.abs((a * t * t / 4) + (b * t / 2) + c); + // Compute the maximum error between the Taylor expansion and the secant + var secantError = Math.max(extremumError, Math.max(startSecantError, endSecantError)); + + // Assuming that the Taylor expansion monotonically diverges from the true value, + // we can bound the error between the Taylor expansion and the true value by measuring the endpoints. + var startTrueValue = d.extract(); + var endTrueValue = d.step(tDuration).extract(); + var startTaylorError = Math.abs(((midpointCurvature * t * t / 8) - (midpointSlope * t / 2) + midpointValue) - startTrueValue); + var endTaylorError = Math.abs(((midpointCurvature * t * t / 8) + (midpointSlope * t / 2) + midpointValue) - endTrueValue); + var taylorError = Math.max(startTaylorError, endTaylorError); + + // Finally, we can bound the error across the entire interval as + // error between secant and Taylor expansion plus error between Taylor expansion and true value. + return secantError + taylorError; + }; + } + + /** + * Uses a direct optimizer to numerically estimate the error of a secant interval. + */ + public static > Function, Double> errorByOptimization() { + return input -> { + var d = input.actualDynamics(); + var t = input.intervalInSeconds(); + var E = input.maximumError(); + + var secant = secant(d, Duration.roundNearest(t, SECOND)); + var secantValue = secant.extract(); + var secantSlope = secant.rate(); + // We only need to compare error between secants, so the error calculation can be pretty coarse. + var optimizer = new BrentOptimizer(1e-2, 1e-2 * E); + try { + return optimizer.optimize( + GoalType.MAXIMIZE, + new SearchInterval(0, t), + new MaxEval(100), + new UnivariateObjectiveFunction( + s -> Math.abs( (secantValue + secantSlope * s) - d.step(Duration.roundNearest(s, SECOND)).extract() ))) + .getValue(); + } catch (TooManyEvaluationsException e) { + // If we can't evaluate the error, play it safe by returning infinite error. + return Double.POSITIVE_INFINITY; + } + }; + } + } +} diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/TaylorApproximation.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/TaylorApproximation.java new file mode 100644 index 0000000000..5562fc2f64 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/TaylorApproximation.java @@ -0,0 +1,33 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; + +public final class TaylorApproximation { + private TaylorApproximation() {} + + /** + * Fixed-degree Taylor approximation. + */ + public static Function, Expiring> taylorApproximation(int degree, BiFunction divergenceEstimator) { + return divergingApproximation(d -> expand(d, degree), divergenceEstimator); + } + + public static Polynomial expand(Differentiable d, int degree) { + double[] coefficients = new double[degree + 1]; + int iFactorial = 1; + for (int i = 0; i <= degree; ++i) { + coefficients[i] = d.extract() / iFactorial; + iFactorial *= i + 1; + } + return polynomial(coefficients); + } +} From 8c864a96cede9fdd74d3ae375499b54d577e99b3 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 18:42:07 -0800 Subject: [PATCH 028/159] Add Demo.java Adds a small, non-executable Demo class. This class shows some important features of the framework in a condensed way. --- .../contrib/streamline/modeling/Demo.java | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Demo.java diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Demo.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Demo.java new file mode 100644 index 0000000000..12da74f904 --- /dev/null +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Demo.java @@ -0,0 +1,182 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling; + +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.*; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.monads.UnstructuredResourceApplicative; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialEffects; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.math3.geometry.euclidean.threed.Vector3D; + +import java.util.Optional; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.approximate; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.DifferentiableResources.asDifferentiable; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.DifferentiableResources.divide; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.DivergenceEstimators.byBoundingError; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.byBoundingError; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.byUniformSampling; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates.errorByOptimization; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates.errorByQuadraticApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.secantApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.UnstructuredResources.asUnstructured; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects.set; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects.toggle; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects.using; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.assertThat; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.discreteResource; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad.map; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialEffects.consume; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.asPolynomial; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.asUnitAwarePolynomial; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.clamp; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.constant; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.integrate; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.lessThan; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.lessThan$; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.polynomialResource; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.unitAware; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Quantities.quantity; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.StandardUnits.*; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.*; +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; + +public final class Demo { + + // Unit-naive version of a model, to demonstrate some core concepts: + + // Consumable, continuous: + MutableResource fuel_kg = PolynomialResources.polynomialResource(20.0); + // Non-consumable, discrete: + MutableResource> power_w = discreteResource(120.0); + // Atomic non-consumable: + MutableResource> rwaControl = DiscreteResources.discreteResource(1); + // Settable / enum state: + MutableResource> enumSwitch = DiscreteResources.discreteResource(OnOff.ON); + // Toggle / flag: + MutableResource> boolSwitch = DiscreteResources.discreteResource(true); + + // Derived states: + Resource> derivedEnumSwitch = map(boolSwitch, b -> b ? OnOff.ON : OnOff.OFF); + Resource batterySOC_J = integrate(asPolynomial(power_w), 100); + Resource> clampedPower_w = map(power_w, p -> p < 0 ? 0 : p); + Resource clampedBatterySOC_J = clamp(batterySOC_J, constant(0), constant(100)); + Resource> lowPower = lessThan(batterySOC_J, 20); + Resource> badness = map( + lowPower, enumSwitch, + (lowPower$, switch$) -> + lowPower$ && switch$ == OnOff.OFF); + + { + using(power_w, 10, () -> { + using(rwaControl, () -> { + // Consume 5.4 kg of fuel over the next minute, linearly + PolynomialEffects.consumeUniformly(fuel_kg, 5.4, Duration.MINUTE); + // Separately, we could be doing things during that minute. + delay(Duration.MINUTE); + }); + set(enumSwitch, OnOff.OFF); + toggle(boolSwitch); + }); + + set(boolSwitch, false); + } + + + // The exact same model again, but this time made unit-aware throughout. + // States without units have been re-used instead of being re-defined + + // Consumable, continuous: + // CellResource fuel_kg = polynomialCellResource(20.0); + UnitAware> fuel = unitAware( + PolynomialResources.polynomialResource(20.0), KILOGRAM); + // Non-consumable, discrete: + UnitAware>> power = DiscreteResources.unitAware( + discreteResource(120.0), WATT); + + UnitAware> batterySOC = integrate(asUnitAwarePolynomial(power), quantity(100, JOULE)); + UnitAware>> clampedPower = DiscreteResources.unitAware(map(power.value(WATT), p -> p < 0 ? 0 : p), WATT); + UnitAware>> clampedPower_v2 = /* map(power, p -> lessThan(p, quantity(0, WATT)) ? quantity(0, WATT) : p) */ + null; + UnitAware> clampedBatterySOC = clamp(batterySOC, constant(quantity(0, JOULE)), constant(quantity(100, JOULE))); + Resource> lowPower$ = lessThan$(batterySOC, quantity(20, JOULE)); + + { + using(power, quantity(10, WATT), () -> { + using(rwaControl, () -> { + // Consume 5.4 kg of fuel over the next minute, linearly + consume(fuel, quantity(5.4, KILOGRAM), Duration.MINUTE); + // Separately, we could be doing things during that minute. + delay(Duration.MINUTE); + }); + set(enumSwitch, OnOff.OFF); + toggle(boolSwitch); + }); + } + + // Example of using unstructured resources + approximation to represent functions that aren't + // easily represented by analytic derivations + Resource p = PolynomialResources.polynomialResource(1, 2, 3); + Resource q = PolynomialResources.polynomialResource(6, 5, 4); + Resource> quotient = UnstructuredResourceApplicative.map(asUnstructured(p), asUnstructured(q), (p$, q$) -> p$ / q$); + Resource approxQuotient = approximate(quotient, secantApproximation(IntervalFunctions.>byBoundingError( + 1e-6, Duration.SECOND, Duration.HOUR.times(24), errorByOptimization()))); + + Resource>> positionAndVelocity = resource(Unstructured.timeBased(t -> /* some spice call */ null)); + Resource>> approxPosVel = approximate( + positionAndVelocity, + DiscreteApproximation., Unstructured>>discreteApproximation( + byBoundingError( + 1e-6, + Duration.SECOND, + Duration.HOUR.times(24), + (u, v) -> Math.max( + u.getLeft().distance(v.getLeft()) / v.getLeft().getNorm(), + u.getRight().distance(v.getRight()) / v.getRight().getNorm())))); + + // Example of the semi-structured "differentiable" resources, and using the additional information to approximate: + Resource pDiff = asDifferentiable(p); + Resource qDiff = asDifferentiable(q); + Resource quotient2 = divide(pDiff, qDiff); + Resource approxQuotient2 = approximate( + quotient2, + secantApproximation(byBoundingError( + 1e-6, + Duration.SECOND, + Duration.HOUR.times(24), + errorByQuadraticApproximation()))); + + // Another example, pushing a polynomial down to a linear for registering: + Resource r; + Resource approxR = approximate(r, SecantApproximation.secantApproximation(byUniformSampling(Duration.HOUR))); + + + // Example of a locking state: + + MutableResource> importantHardware = DiscreteResources.discreteResource(42); + MutableResource>> importantHardwareLock = DiscreteResources.discreteResource(Optional.empty()); + Resource> importantHardwareLockAssertion = assertThat( + "Important hardware does not change state while locked", + map(importantHardwareLock, importantHardware, (lock, state) -> lock.map(state::equals).orElse(true))); + void lockImportantHardware() { + if (currentValue(importantHardwareLock).isPresent()) { + throw new IllegalStateException("Already locked!"); + } else { + set(importantHardwareLock, Optional.of(currentValue(importantHardware))); + } + } + + void unlockImportantHardware() { + set(importantHardwareLock, Optional.empty()); + } + + public enum OnOff { ON, OFF } +} From 943cb8857837d139ab4e88673d87b55a84358a08 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 18:44:56 -0800 Subject: [PATCH 029/159] Add the streamline-demo example model Add an executable example model used to test and demonstrate the framework. The example model comprises three main components: * ErrorTestingModel * DataModel * ApproximationModel The ErrorTestingModel demonstrates error-handling behavior for resources. Using the `CauseError` activity, we can trigger one of several kinds of errors from the plan, including effects which fail directly, concurrent effects which conflict, and resources which violate a constraint. We can also test out different combinations of naming and error handling behaviors, to see what information is propagated back to the user for debugging. Finally, we can observe that, when logging errors, portions of the model can fail while independent portions of the model continue to function normally. The DataModel demonstrates a complicated resource modeling problem, leveraging the LinearBoundaryConsistencySolver. This problem models a data system with bin space shared across multiple buckets, where some buckets have priority over others, and each bucket independently sets desired write or delete rates. The problem of allocating bin space to each bucket, allowing higher-priority buckets to overwrite lower-priority ones only if needed, is phrased as linear inequalities on polynomial resources and given to the solver. The `ChangeDesiredRate` activity can be used to set up different scenarios for this model. The ApproximationModel shows three resources, a polynomial, a rational function, and a complicated trig function, approximated various ways. The `ChangeApproximationInput` activity can be used to change the polynomials feeding this model, and the simulation configuration contains a setting for the approximation accuracy, affecting those resources which approximate by bounding their maximum error. In particular, the `approximation/trig/default` resource displays the most complicated resource function, comprising trig and exponentials, defined through the Java Math library, approximated by the default secant approximation method. --- examples/streamline-demo/build.gradle | 42 +++++++ .../streamline_demo/ApproximationModel.java | 102 +++++++++++++++++ .../jpl/aerie/streamline_demo/CauseError.java | 73 ++++++++++++ .../ChangeApproximationInput.java | 22 ++++ .../streamline_demo/ChangeDesiredRate.java | 29 +++++ .../aerie/streamline_demo/Configuration.java | 16 +++ .../jpl/aerie/streamline_demo/DataModel.java | 106 ++++++++++++++++++ .../streamline_demo/ErrorTestingModel.java | 39 +++++++ .../jpl/aerie/streamline_demo/Mission.java | 22 ++++ .../aerie/streamline_demo/package-info.java | 17 +++ ...l.aerie.merlin.protocol.model.MerlinPlugin | 1 + ...erie.merlin.protocol.model.SchedulerPlugin | 1 + merlin-framework/build.gradle | 1 + settings.gradle | 1 + 14 files changed, 472 insertions(+) create mode 100644 examples/streamline-demo/build.gradle create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ApproximationModel.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/CauseError.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeApproximationInput.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeDesiredRate.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/DataModel.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java create mode 100644 examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/package-info.java create mode 100644 examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.MerlinPlugin create mode 100644 examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin diff --git a/examples/streamline-demo/build.gradle b/examples/streamline-demo/build.gradle new file mode 100644 index 0000000000..24ed559c1a --- /dev/null +++ b/examples/streamline-demo/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java-library' + id 'jacoco' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(19) + } +} + +jar { + from { + configurations.runtimeClasspath.filter{ it.exists() }.collect{ it.isDirectory() ? it : zipTree(it) } + } { + exclude 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt' + } +} + +test { + useJUnitPlatform() +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + } +} + +dependencies { + annotationProcessor project(':merlin-framework-processor') + + implementation project(':merlin-framework') + implementation project(':contrib') + + testImplementation project(':merlin-framework-junit') + testImplementation 'org.assertj:assertj-core:3.23.1' + + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ApproximationModel.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ApproximationModel.java new file mode 100644 index 0000000000..b7355f0b35 --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ApproximationModel.java @@ -0,0 +1,102 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.*; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.linear.Linear; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; + +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.approximate; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.Approximation.relative; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.DifferentiableResources.asDifferentiable; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.byBoundingError; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.byUniformSampling; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates.errorByOptimization; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates.errorByQuadraticApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.secantApproximation; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.UnstructuredResources.approximateAsLinear; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.UnstructuredResources.approximateAsLinear; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.UnstructuredResources.asUnstructured; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.monads.UnstructuredResourceApplicative.map; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.approximateAsLinear; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.*; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; + +public class ApproximationModel { + private static final double EPSILON = 1e-10; + + public MutableResource polynomial; + public MutableResource divisor; + + public Resource assumedLinear; + public Resource uniformApproximation; + public Resource differentiableApproximation; + public Resource directApproximation; + public Resource defaultApproximation; + + public Resource> rationalFunction; + public Resource uniformApproximation2; + public Resource directApproximation2; + public Resource defaultApproximation2; + + public Resource> trigFunction; + public Resource trigDefaultApproximation; + + public ApproximationModel(final Registrar registrar, final Configuration config) { + final double tolerance = config.approximationTolerance; + + polynomial = polynomialResource(1); + divisor = polynomialResource(1); + + assumedLinear = assumeLinear(polynomial); + defaultApproximation = approximateAsLinear(polynomial, tolerance); + uniformApproximation = approximate( + polynomial, + SecantApproximation.secantApproximation(byUniformSampling(MINUTE))); + differentiableApproximation = approximate( + asDifferentiable(polynomial), + secantApproximation(byBoundingError( + tolerance, + SECOND, + HOUR.times(24), + relative(errorByQuadraticApproximation(), EPSILON)))); + directApproximation = approximate( + polynomial, + secantApproximation(byBoundingError( + tolerance, + SECOND, + HOUR.times(24), + Approximation.relative(errorByOptimization(), EPSILON)))); + + rationalFunction = map( + asUnstructured(polynomial), asUnstructured(divisor), (p, q) -> p / q); + + defaultApproximation2 = approximateAsLinear(rationalFunction, tolerance); + uniformApproximation2 = approximate(rationalFunction, + SecantApproximation.>secantApproximation(byUniformSampling(MINUTE))); + directApproximation2 = approximate(rationalFunction, + secantApproximation(byBoundingError( + tolerance, + SECOND, + HOUR.times(24), + Approximation.>relative(errorByOptimization(), EPSILON)))); + + trigFunction = map(asUnstructured(polynomial), asUnstructured(divisor), + (p, q) -> Math.sin(p * Math.exp(-q / Math.PI))); + trigDefaultApproximation = approximateAsLinear(trigFunction, tolerance); + + registrar.real("approximation/polynomial/assumedLinear", assumedLinear); + registrar.real("approximation/polynomial/default", defaultApproximation); + registrar.real("approximation/polynomial/uniform", uniformApproximation); + registrar.real("approximation/polynomial/differentiable", differentiableApproximation); + registrar.real("approximation/polynomial/direct", directApproximation); + + registrar.real("approximation/rational/default", defaultApproximation2); + registrar.real("approximation/rational/uniform", uniformApproximation2); + registrar.real("approximation/rational/direct", directApproximation2); + + registrar.real("approximation/trig/default", trigDefaultApproximation); + } +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/CauseError.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/CauseError.java new file mode 100644 index 0000000000..8f07dd4945 --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/CauseError.java @@ -0,0 +1,73 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; +import gov.nasa.jpl.aerie.contrib.streamline.core.DynamicsEffect; +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType.EffectModel; +import gov.nasa.jpl.aerie.merlin.framework.annotations.Export.Parameter; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.effect; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Context.contextualized; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Context.inContext; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects.set; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.spawn; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; + +@ActivityType("CauseError") +public class CauseError { + @Parameter + public ResourceSelection selection; + + @Parameter + public String effectName = ""; + + @Parameter + public String activityName = ""; + + @EffectModel + public void run(Mission mission) { + inContext("CauseError" + (activityName.isEmpty() ? "" : " " + activityName), () -> { + delay(HOUR); + switch (selection) { + case Bool -> causeError(mission.errorTestingModel.bool); + case Counter -> causeError(mission.errorTestingModel.counter); + case Continuous -> causeError(mission.errorTestingModel.continuous); + case NonCommuting -> { + spawn(contextualized("Branch 1", () -> { + set(mission.errorTestingModel.counter, 5); + })); + spawn(contextualized("Branch 2", () -> { + set(mission.errorTestingModel.counter, 6); + })); + } + case DoomedClamp -> { + MutableResource.set(mission.errorTestingModel.lowerBound, polynomial(-20, 0.001)); + MutableResource.set(mission.errorTestingModel.upperBound, polynomial(20, -0.001)); + } + } + delay(HOUR); + }); + } + + private > void causeError(MutableResource resource) { + DynamicsEffect effect = effect($ -> { + throw new IllegalStateException("Pretend this is a more informative error message."); + }); + if (effectName.isEmpty()) { + resource.emit(effect); + } else { + resource.emit(effectName, effect); + } + } + + public enum ResourceSelection { + Bool, + Counter, + Continuous, + NonCommuting, + DoomedClamp + } +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeApproximationInput.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeApproximationInput.java new file mode 100644 index 0000000000..da9ae8334d --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeApproximationInput.java @@ -0,0 +1,22 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; +import gov.nasa.jpl.aerie.merlin.framework.annotations.Export; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.set; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; + +@ActivityType("ChangeApproximationInput") +public class ChangeApproximationInput { + @Export.Parameter + public double[] numeratorCoefficients; + + @Export.Parameter + public double[] denominatorCoefficients; + + @ActivityType.EffectModel + public void run(Mission mission) { + set(mission.approximationModel.polynomial, polynomial(numeratorCoefficients)); + set(mission.approximationModel.divisor, polynomial(denominatorCoefficients)); + } +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeDesiredRate.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeDesiredRate.java new file mode 100644 index 0000000000..d9cc417afd --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ChangeDesiredRate.java @@ -0,0 +1,29 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType.EffectModel; +import gov.nasa.jpl.aerie.merlin.framework.annotations.Export.Parameter; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.set; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; + +@ActivityType("ChangeDesiredRate") +public class ChangeDesiredRate { + @Parameter + public Bucket bucket; + + @Parameter + public double rate; + + @EffectModel + public void run(Mission mission) { + var rateToChange = switch (bucket) { + case A -> mission.dataModel.desiredRateA; + case B -> mission.dataModel.desiredRateB; + case C -> mission.dataModel.desiredRateC; + }; + set(rateToChange, polynomial(rate)); + } + + public enum Bucket { A, B, C } +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java new file mode 100644 index 0000000000..fbdf8a0a7a --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java @@ -0,0 +1,16 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.merlin.framework.annotations.Export.Parameter; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +public final class Configuration { + @Parameter + public boolean traceResources = false; + + @Parameter + public double approximationTolerance = 1e-2; + + @Parameter + public Duration profilingDumpTime = Duration.ZERO; + +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/DataModel.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/DataModel.java new file mode 100644 index 0000000000..64bf344c6a --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/DataModel.java @@ -0,0 +1,106 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.Domain; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.eraseExpiry; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.forward; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.choose; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.Comparison.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.LinearExpression.lx; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.*; + +public class DataModel { + public MutableResource desiredRateA = PolynomialResources.polynomialResource(0); + public MutableResource desiredRateB = PolynomialResources.polynomialResource(0); + public MutableResource desiredRateC = PolynomialResources.polynomialResource(0); + public MutableResource upperBoundOnTotalVolume = PolynomialResources.polynomialResource(10); + + public Resource actualRateA, actualRateB, actualRateC; + public MutableResource volumeA, volumeB, volumeC; + public Resource totalVolume; + + public DataModel(final Registrar registrar, final Configuration config) { + // Adding a "derivative" operation makes the constraint problem no longer solvable without backtracking. + // Instead of solving for it directly, we'll solve in two steps, one for rate, one for volume. + + // Set up the rate solver + var rateSolver = new LinearBoundaryConsistencySolver("DataModel Rate Solver"); + var rateA = rateSolver.variable("rateA", Domain::upperBound); + var rateB = rateSolver.variable("rateB", Domain::upperBound); + var rateC = rateSolver.variable("rateC", Domain::upperBound); + this.actualRateA = rateA.resource(); + this.actualRateB = rateB.resource(); + this.actualRateC = rateC.resource(); + + // Use a simple feedback loop on volumes to do the integration and clamping. + this.volumeA = PolynomialResources.polynomialResource(0); + this.volumeB = PolynomialResources.polynomialResource(0); + this.volumeC = PolynomialResources.polynomialResource(0); + var clampedVolumeA = clamp(this.volumeA, constant(0), upperBoundOnTotalVolume); + var volumeB_ub = subtract(upperBoundOnTotalVolume, clampedVolumeA); + var clampedVolumeB = clamp(this.volumeB, constant(0), volumeB_ub); + var volumeC_ub = subtract(volumeB_ub, clampedVolumeB); + var clampedVolumeC = clamp(this.volumeC, constant(0), volumeC_ub); + var correctedVolumeA = map(clampedVolumeA, actualRateA, (v, r) -> r.integral(v.extract())); + var correctedVolumeB = map(clampedVolumeB, actualRateB, (v, r) -> r.integral(v.extract())); + var correctedVolumeC = map(clampedVolumeC, actualRateC, (v, r) -> r.integral(v.extract())); + // Use the corrected integral values to set volumes, but erase expiry information in the process to avoid loops: + forward(eraseExpiry(correctedVolumeA), this.volumeA); + forward(eraseExpiry(correctedVolumeB), this.volumeB); + forward(eraseExpiry(correctedVolumeC), this.volumeC); + + // Integrate the actual rates. + totalVolume = add(this.volumeA, this.volumeB, this.volumeC); + + // Then use the solver to adjust the actual rates + + // When full, we never write more than the upper bound will tolerate, in total + var isFull = greaterThanOrEquals(totalVolume, upperBoundOnTotalVolume); + var totalRate_ub = choose(isFull, differentiate(upperBoundOnTotalVolume), constant(Double.POSITIVE_INFINITY)); + rateSolver.declare(lx(rateA).add(lx(rateB)).add(lx(rateC)), LessThanOrEquals, lx(totalRate_ub)); + + // We only exceed the desired rate when we try to delete from an empty bucket. + var isEmptyA = lessThanOrEquals(this.volumeA, 0); + var isEmptyB = lessThanOrEquals(this.volumeB, 0); + var isEmptyC = lessThanOrEquals(this.volumeC, 0); + var rateA_ub = choose(isEmptyA, max(desiredRateA, constant(0)), desiredRateA); + var rateB_ub = choose(isEmptyB, max(desiredRateB, constant(0)), desiredRateB); + var rateC_ub = choose(isEmptyC, max(desiredRateC, constant(0)), desiredRateC); + rateSolver.declare(lx(rateA), LessThanOrEquals, lx(rateA_ub)); + rateSolver.declare(lx(rateB), LessThanOrEquals, lx(rateB_ub)); + rateSolver.declare(lx(rateC), LessThanOrEquals, lx(rateC_ub)); + + // We cannot delete from an empty bucket + var rateA_lb = choose(isEmptyA, constant(0), constant(Double.NEGATIVE_INFINITY)); + var rateB_lb = choose(isEmptyB, constant(0), constant(Double.NEGATIVE_INFINITY)); + var rateC_lb = choose(isEmptyC, constant(0), constant(Double.NEGATIVE_INFINITY)); + rateSolver.declare(lx(rateA), GreaterThanOrEquals, lx(rateA_lb)); + rateSolver.declare(lx(rateB), GreaterThanOrEquals, lx(rateB_lb)); + rateSolver.declare(lx(rateC), GreaterThanOrEquals, lx(rateC_lb)); + + registerStates(registrar, config); + } + + private void registerStates(Registrar registrar, Configuration config) { + registrar.real("desiredRateA", assumeLinear(desiredRateA)); + registrar.real("desiredRateB", assumeLinear(desiredRateB)); + registrar.real("desiredRateC", assumeLinear(desiredRateC)); + + registrar.real("actualRateA", assumeLinear(actualRateA)); + registrar.real("actualRateB", assumeLinear(actualRateB)); + registrar.real("actualRateC", assumeLinear(actualRateC)); + + registrar.real("volumeA", assumeLinear(volumeA)); + registrar.real("volumeB", assumeLinear(volumeB)); + registrar.real("volumeC", assumeLinear(volumeC)); + registrar.real("totalVolume", assumeLinear(totalVolume)); + registrar.real("maxVolume", assumeLinear(upperBoundOnTotalVolume)); + } +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java new file mode 100644 index 0000000000..ce2693751f --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java @@ -0,0 +1,39 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.contrib.serialization.mappers.BooleanValueMapper; +import gov.nasa.jpl.aerie.contrib.serialization.mappers.IntegerValueMapper; +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources; + +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.discreteResource; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad.map; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.*; + +public class ErrorTestingModel { + public MutableResource> bool = DiscreteResources.discreteResource(true); + public MutableResource> counter = DiscreteResources.discreteResource(5); + public MutableResource continuous = PolynomialResources.polynomialResource(1); + public Resource derived = multiply( + continuous, + asPolynomial(map(counter, c -> (double) c)), + asPolynomial(map(bool, $ -> $ ? 1.0 : -1.0))); + + public MutableResource upperBound = PolynomialResources.polynomialResource(5); + public MutableResource lowerBound = PolynomialResources.polynomialResource(-5); + public Resource clamped = clamp(constant(10), lowerBound, upperBound); + + public ErrorTestingModel(final Registrar registrar, final Configuration config) { + registrar.discrete("errorTesting/bool", bool, new BooleanValueMapper()); + registrar.discrete("errorTesting/counter", counter, new IntegerValueMapper()); + registrar.real("errorTesting/continuous", assumeLinear(continuous)); + registrar.real("errorTesting/derived", assumeLinear(derived)); + registrar.real("errorTesting/lowerBound", assumeLinear(lowerBound)); + registrar.real("errorTesting/upperBound", assumeLinear(upperBound)); + registrar.real("errorTesting/clamped", assumeLinear(clamped)); + } +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java new file mode 100644 index 0000000000..2021ed3c57 --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java @@ -0,0 +1,22 @@ +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar; +import gov.nasa.jpl.aerie.merlin.framework.ModelActions; + +public final class Mission { + public final DataModel dataModel; + public final ErrorTestingModel errorTestingModel; + public final ApproximationModel approximationModel; + + public Mission(final gov.nasa.jpl.aerie.merlin.framework.Registrar registrar$, final Configuration config) { + var registrar = new Registrar(registrar$, Registrar.ErrorBehavior.Log); + if (config.traceResources) registrar.setTrace(); + dataModel = new DataModel(registrar, config); + errorTestingModel = new ErrorTestingModel(registrar, config); + approximationModel = new ApproximationModel(registrar, config); + if (config.profilingDumpTime.isPositive()) { + ModelActions.defer(config.profilingDumpTime, Profiling::dump); + } + } +} diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/package-info.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/package-info.java new file mode 100644 index 0000000000..310237e0bc --- /dev/null +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/package-info.java @@ -0,0 +1,17 @@ +@MissionModel(model = Mission.class) + +@WithConfiguration(Configuration.class) + +@WithMappers(BasicValueMappers.class) + +@WithActivityType(ChangeDesiredRate.class) +@WithActivityType(CauseError.class) +@WithActivityType(ChangeApproximationInput.class) + +package gov.nasa.jpl.aerie.streamline_demo; + +import gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers; +import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel; +import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel.WithActivityType; +import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel.WithConfiguration; +import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel.WithMappers; diff --git a/examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.MerlinPlugin b/examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.MerlinPlugin new file mode 100644 index 0000000000..01d58f055f --- /dev/null +++ b/examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.MerlinPlugin @@ -0,0 +1 @@ +gov.nasa.jpl.aerie.streamline_demo.generated.GeneratedMerlinPlugin diff --git a/examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin b/examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin new file mode 100644 index 0000000000..178d917e84 --- /dev/null +++ b/examples/streamline-demo/src/main/resources/META-INF/services/gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin @@ -0,0 +1 @@ +gov.nasa.jpl.aerie.streamline_demo.generated.GeneratedSchedulerPlugin diff --git a/merlin-framework/build.gradle b/merlin-framework/build.gradle index 5cf2f53462..d025f5b35a 100644 --- a/merlin-framework/build.gradle +++ b/merlin-framework/build.gradle @@ -32,6 +32,7 @@ javadoc.options.addStringOption('Xdoclint:none', '-quiet') dependencies { compileOnlyApi project(':merlin-sdk') implementation 'org.apache.commons:commons-lang3:3.13.0' + implementation 'org.apache.commons:commons-math3:3.6.1' testImplementation project(':merlin-sdk') testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' diff --git a/settings.gradle b/settings.gradle index 03d90f18ed..48b2422579 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,3 +33,4 @@ include 'examples:foo-missionmodel' include 'examples:config-with-defaults' include 'examples:config-without-defaults' include 'examples:minimal-mission-model' +include 'examples:streamline-demo' From 5c858832ae4df77ee2180d9a20f861738e2d7cfe Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 18 Dec 2023 19:01:09 -0800 Subject: [PATCH 030/159] Add unit tests Adds unit tests for a variety of classes in the streamline framework. These unit tests were added as-needed to debug problems, and aren't meant to give comprehensive or systematic coverage. --- .../streamline/core/CellRefV2Test.java | 157 ++++++++ .../contrib/streamline/core/ExpiryTest.java | 153 ++++++++ .../debugging/DependenciesTest.java | 67 ++++ .../black_box/SecantApproximationTest.java | 48 +++ .../discrete/DiscreteEffectsTest.java | 242 ++++++++++++ .../modeling/discrete/PrecomputedTest.java | 140 +++++++ .../modeling/polynomial/ComparisonsTest.java | 365 ++++++++++++++++++ .../LinearBoundaryConsistencySolverTest.java | 235 +++++++++++ .../modeling/polynomial/PrecomputedTest.java | 144 +++++++ .../streamline/unit_aware/DimensionTest.java | 192 +++++++++ 10 files changed, 1743 insertions(+) create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2Test.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/ExpiryTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/DependenciesTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximationTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffectsTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/PrecomputedTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolverTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PrecomputedTest.java create mode 100644 contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/DimensionTest.java diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2Test.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2Test.java new file mode 100644 index 0000000000..c77bb601c8 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2Test.java @@ -0,0 +1,157 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.merlin.framework.Registrar; +import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects; +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.commutingEffects; +import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.noncommutingEffects; +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteDynamicsMonad.effect; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.spawn; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static org.junit.jupiter.api.Assertions.*; + +class MutableResourceTest { + @Nested + @ExtendWith(MerlinExtension.class) + @TestInstance(Lifecycle.PER_CLASS) + class NonCommutingEffects { + public NonCommutingEffects(final Registrar registrar) { + Resources.init(); + } + + private final MutableResource> cell = MutableResource.resource(discrete(42), noncommutingEffects()); + + @Test + void gets_initial_value_if_no_effects_are_emitted() { + assertEquals(42, currentValue(cell)); + } + + @Test + void applies_singleton_effect() { + int initialValue = currentValue(cell); + cell.emit(effect(n -> 3 * n)); + assertEquals(3 * initialValue, currentValue(cell)); + } + + @Test + void applies_sequential_effects_in_order() { + int initialValue = currentValue(cell); + cell.emit(effect(n -> 3 * n)); + cell.emit(effect(n -> n + 1)); + assertEquals(3 * initialValue + 1, currentValue(cell)); + } + + @Test + void throws_exception_when_concurrent_effects_are_applied() { + spawn(() -> cell.emit(effect(n -> 3 * n))); + spawn(() -> cell.emit(effect(n -> 3 * n))); + delay(ZERO); + assertInstanceOf(ErrorCatching.Failure.class, cell.getDynamics()); + } + } + + @Nested + @ExtendWith(MerlinExtension.class) + @TestInstance(Lifecycle.PER_CLASS) + class CommutingEffects { + public CommutingEffects(final Registrar registrar) { + Resources.init(); + } + + private final MutableResource> cell = MutableResource.resource(discrete(42), commutingEffects()); + + @Test + void gets_initial_value_if_no_effects_are_emitted() { + assertEquals(42, currentValue(cell)); + } + + @Test + void applies_singleton_effect() { + int initialValue = currentValue(cell); + cell.emit(effect(n -> 3 * n)); + assertEquals(3 * initialValue, currentValue(cell)); + } + + @Test + void applies_sequential_effects_in_order() { + int initialValue = currentValue(cell); + cell.emit(effect(n -> 3 * n)); + cell.emit(effect(n -> n + 1)); + assertEquals(3 * initialValue + 1, currentValue(cell)); + } + + @Test + void applies_concurrent_effects_in_any_order() { + int initialValue = currentValue(cell); + // These effects do not in fact commute, + // but the point of the commutingEffects is that it *doesn't* check. + spawn(() -> cell.emit(effect(n -> 3 * n))); + spawn(() -> cell.emit(effect(n -> n + 1))); + delay(ZERO); + int result = currentValue(cell); + assertTrue(result == 3*initialValue + 1 || result == 3 * (initialValue + 1)); + } + } + + @Nested + @ExtendWith(MerlinExtension.class) + @TestInstance(Lifecycle.PER_CLASS) + class AutoEffects { + public AutoEffects(final Registrar registrar) { + Resources.init(); + } + + private final MutableResource> cell = MutableResource.resource(discrete(42), autoEffects()); + + @Test + void gets_initial_value_if_no_effects_are_emitted() { + assertEquals(42, currentValue(cell)); + } + + @Test + void applies_singleton_effect() { + int initialValue = currentValue(cell); + cell.emit(effect(n -> 3 * n)); + assertEquals(3 * initialValue, currentValue(cell)); + } + + @Test + void applies_sequential_effects_in_order() { + int initialValue = currentValue(cell); + cell.emit(effect(n -> 3 * n)); + cell.emit(effect(n -> n + 1)); + assertEquals(3 * initialValue + 1, currentValue(cell)); + } + + @Test + void applies_commuting_concurrent_effects() { + int initialValue = currentValue(cell); + // These effects do not in fact commute, + // but the point of the commutingEffects is that it *doesn't* check. + spawn(() -> cell.emit(effect(n -> 3 * n))); + spawn(() -> cell.emit(effect(n -> 4 * n))); + delay(ZERO); + int result = currentValue(cell); + assertEquals(12 * initialValue, result); + } + + @Test + void throws_exception_when_non_commuting_concurrent_effects_are_applied() { + spawn(() -> cell.emit(effect(n -> 3 * n))); + spawn(() -> cell.emit(effect(n -> n + 1))); + delay(ZERO); + assertInstanceOf(ErrorCatching.Failure.class, cell.getDynamics()); + } + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/ExpiryTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/ExpiryTest.java new file mode 100644 index 0000000000..1c91e03da7 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/core/ExpiryTest.java @@ -0,0 +1,153 @@ +package gov.nasa.jpl.aerie.contrib.streamline.core; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.NEVER; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.at; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.expiry; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; +import static org.junit.jupiter.api.Assertions.*; + +class ExpiryTest { + @Nested + class Value { + @Test + void never_expiring_has_empty_value() { + assertEquals(Optional.empty(), NEVER.value()); + } + + @Test + void expiring_at_t_has_value_of_t() { + assertEquals(Optional.of(MINUTE), Expiry.at(MINUTE).value()); + } + } + + @Nested + class Equals { + @Test + void never_equals_never() { + assertEquals(NEVER, NEVER); + assertEquals(NEVER, expiry(Optional.empty())); + assertEquals(expiry(Optional.empty()), NEVER); + assertEquals(expiry(Optional.empty()), expiry(Optional.empty())); + } + + @Test + void at_t_equals_at_t() { + assertEquals(at(MINUTE), at(MINUTE)); + } + + @Test + void at_t_does_not_equal_never() { + assertNotEquals(at(MINUTE), NEVER); + assertNotEquals(NEVER, at(MINUTE)); + } + + @Test + void at_t_does_not_equal_at_s() { + assertNotEquals(at(MINUTE), at(HOUR)); + assertNotEquals(at(HOUR), at(MINUTE)); + } + } + + @Nested + class IsNever { + @Test + void never_is_never() { + assertTrue(NEVER.isNever()); + } + + @Test + void at_t_is_not_never() { + assertFalse(at(MINUTE).isNever()); + } + } + + @Nested + class Or { + @Test + void never_or_never_returns_never() { + assertEquals(NEVER, NEVER.or(NEVER)); + } + + @Test + void never_or_at_t_returns_at_t() { + assertEquals(at(MINUTE), NEVER.or(at(MINUTE))); + } + + @Test + void at_t_or_never_returns_at_t() { + assertEquals(at(MINUTE), at(MINUTE).or(NEVER)); + } + + @Test + void at_t_or_at_greater_than_t_returns_at_t() { + assertEquals(at(MINUTE), at(MINUTE).or(at(HOUR))); + } + + @Test + void at_greater_than_t_or_at_t_returns_at_t() { + assertEquals(at(MINUTE), at(HOUR).or(at(MINUTE))); + } + } + + @Nested + class Minus { + @Test + void never_minus_t_returns_never() { + assertEquals(NEVER, NEVER.minus(MINUTE)); + } + + @Test + void at_t_minus_s_returns_at_difference() { + assertEquals(at(HOUR.minus(MINUTE)), at(HOUR).minus(MINUTE)); + } + } + + @Nested + class Comparisons { + @Test + void never_equals_never() { + assertFalse(NEVER.shorterThan(NEVER)); + assertTrue(NEVER.noShorterThan(NEVER)); + assertFalse(NEVER.longerThan(NEVER)); + assertTrue(NEVER.noLongerThan(NEVER)); + } + + @Test + void at_t_equals_at_t() { + assertFalse(at(MINUTE).shorterThan(at(MINUTE))); + assertTrue(at(MINUTE).noShorterThan(at(MINUTE))); + assertFalse(at(MINUTE).longerThan(at(MINUTE))); + assertTrue(at(MINUTE).noLongerThan(at(MINUTE))); + } + + @Test + void at_t_shorter_than_never() { + assertTrue(at(MINUTE).shorterThan(NEVER)); + assertFalse(at(MINUTE).noShorterThan(NEVER)); + assertFalse(at(MINUTE).longerThan(NEVER)); + assertTrue(at(MINUTE).noLongerThan(NEVER)); + } + + @Test + void never_longer_than_at_t() { + assertFalse(NEVER.shorterThan(at(MINUTE))); + assertTrue(NEVER.noShorterThan(at(MINUTE))); + assertTrue(NEVER.longerThan(at(MINUTE))); + assertFalse(NEVER.noLongerThan(at(MINUTE))); + } + + @Test + void at_t_shorter_than_at_greater_than_t() { + assertTrue(at(MINUTE).shorterThan(at(HOUR))); + assertFalse(at(MINUTE).noShorterThan(at(HOUR))); + assertFalse(at(MINUTE).longerThan(at(HOUR))); + assertTrue(at(MINUTE).noLongerThan(at(HOUR))); + } + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/DependenciesTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/DependenciesTest.java new file mode 100644 index 0000000000..8014216f63 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/DependenciesTest.java @@ -0,0 +1,67 @@ +package gov.nasa.jpl.aerie.contrib.streamline.debugging; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; +import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.constant; +import static org.junit.jupiter.api.Assertions.*; + +@Nested +@ExtendWith(MerlinExtension.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DependenciesTest { + Resource> constantTrue = DiscreteResources.constant(true); + Resource constant1234 = constant(1234); + Resource constant5678 = constant(5678); + Resource polynomialCell = resource(polynomial(1)); + Resource derived = map(constantTrue, constant1234, constant5678, + (b, x, y) -> b.extract() ? x : y); + + @Test + void constants_are_named_by_their_value() { + assertTrue(Naming.getName(constantTrue).get().contains("true")); + assertTrue(Naming.getName(constant1234).get().contains("1234")); + assertTrue(Naming.getName(constant5678).get().contains("5678")); + } + + @Test + void cell_resources_are_not_inherently_named() { + assertTrue(Naming.getName(polynomialCell).isEmpty()); + } + + @Test + void derived_resources_are_not_inherently_named() { + assertTrue(Naming.getName(derived).isEmpty()); + } + + @Test + void constants_have_no_dependencies() { + assertTrue(Dependencies.getDependencies(constantTrue).isEmpty()); + assertTrue(Dependencies.getDependencies(constant1234).isEmpty()); + assertTrue(Dependencies.getDependencies(constant5678).isEmpty()); + } + + @Test + void cell_resources_have_no_dependencies() { + assertTrue(Dependencies.getDependencies(polynomialCell).isEmpty()); + } + + @Test + void derived_resources_have_their_sources_as_dependencies() { + var graphDescription = Dependencies.describeDependencyGraph(derived, true); + assertTrue(graphDescription.contains("true")); + assertTrue(graphDescription.contains("1234")); + assertTrue(graphDescription.contains("5678")); + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximationTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximationTest.java new file mode 100644 index 0000000000..37cb673129 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/SecantApproximationTest.java @@ -0,0 +1,48 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; + +import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.IntervalFunctions.ErrorEstimateInput; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.DifferentiableResources.asDifferentiable; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static org.junit.jupiter.api.Assertions.*; + +class SecantApproximationTest { + @Nested + class ErrorEstimatesTest { + @Test + void quadratic_error_estimate_for_constant_is_zero() { + var dynamics = asDifferentiable(polynomial(5)); + var result = errorByQuadraticApproximation().apply( + new ErrorEstimateInput<>( + dynamics, + 10.0, + 1e-6)); + assertEquals(0.0, result); + } + + @Test + void quadratic_error_estimate_for_linear_is_zero() { + var dynamics = asDifferentiable(polynomial(5, -3)); + var result = errorByQuadraticApproximation().apply( + new ErrorEstimateInput<>( + dynamics, + 10.0, + 1e-6)); + assertEquals(0.0, result); + } + + @Test + void quadratic_error_estimate_for_quadratic_is_exact() { + var dynamics = asDifferentiable(polynomial(1, -1, 0.5)); + var result = errorByQuadraticApproximation().apply( + new ErrorEstimateInput<>( + dynamics, + 2.0, + 1e-6)); + assertEquals(0.5, result); + } + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffectsTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffectsTest.java new file mode 100644 index 0000000000..49d4a953e8 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/DiscreteEffectsTest.java @@ -0,0 +1,242 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.ErrorCatching; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock; +import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware; +import gov.nasa.jpl.aerie.merlin.framework.Registrar; +import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentTime; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.unitAware; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Quantities.add; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Quantities.quantity; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Quantities.subtract; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.StandardUnits.*; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.StandardUnits.BIT; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.StandardUnits.METER; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAwareResources.currentValue; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.spawn; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MerlinExtension.class) +@TestInstance(Lifecycle.PER_CLASS) +class DiscreteEffectsTest { + public DiscreteEffectsTest(final Registrar registrar) { + Resources.init(); + } + + private final MutableResource> settable = resource(discrete(42)); + + @Test + void set_effect_changes_to_new_value() { + set(settable, 123); + assertEquals(123, currentValue(settable)); + } + + @Test + void conflicting_concurrent_set_effects_throw_exception() { + spawn(() -> set(settable, 123)); + spawn(() -> set(settable, 456)); + delay(ZERO); + assertInstanceOf(ErrorCatching.Failure.class, settable.getDynamics()); + } + + @Test + void agreeing_concurrent_set_effects_set_new_value() { + spawn(() -> set(settable, 789)); + spawn(() -> set(settable, 789)); + delay(ZERO); + assertEquals(789, currentValue(settable)); + } + + private final MutableResource> flag = resource(discrete(false)); + + @Test + void flag_set_makes_value_true() { + turnOn(flag); + assertTrue(currentValue(flag)); + } + + @Test + void flag_unset_makes_value_false() { + turnOff(flag); + assertFalse(currentValue(flag)); + } + + @Test + void flag_toggle_changes_value() { + turnOn(flag); + toggle(flag); + assertFalse(currentValue(flag)); + + toggle(flag); + assertTrue(currentValue(flag)); + } + + private final MutableResource> counter = resource(discrete(0)); + + @Test + void increment_increases_value_by_1() { + int initialValue = currentValue(counter); + increment(counter); + assertEquals(initialValue + 1, currentValue(counter)); + } + + @Test + void increment_by_n_increases_value_by_n() { + int initialValue = currentValue(counter); + increment(counter, 3); + assertEquals(initialValue + 3, currentValue(counter)); + } + + @Test + void decrement_decreases_value_by_1() { + int initialValue = currentValue(counter); + decrement(counter); + assertEquals(initialValue - 1, currentValue(counter)); + } + + @Test + void decrement_by_n_decreases_value_by_n() { + int initialValue = currentValue(counter); + decrement(counter, 3); + assertEquals(initialValue - 3, currentValue(counter)); + } + + private final MutableResource> consumable = resource(discrete(10.0)); + + @Test + void consume_decreases_value_by_amount() { + double initialValue = currentValue(consumable); + consume(consumable, 3.14); + assertEquals(initialValue - 3.14, currentValue(consumable)); + } + + @Test + void restore_increases_value_by_amount() { + double initialValue = currentValue(consumable); + restore(consumable, 3.14); + assertEquals(initialValue + 3.14, currentValue(consumable)); + } + + @Test + void consume_and_restore_effects_commute() { + double initialValue = currentValue(consumable); + spawn(() -> consume(consumable, 2.7)); + spawn(() -> restore(consumable, 5.6)); + delay(ZERO); + assertEquals(initialValue - 2.7 + 5.6, currentValue(consumable)); + } + + private final MutableResource> nonconsumable = resource(discrete(10.0)); + + @Test + void using_decreases_value_while_action_is_running() { + double initialValue = currentValue(nonconsumable); + using(nonconsumable, 3.14, () -> { + assertEquals(initialValue - 3.14, currentValue(nonconsumable)); + }); + assertEquals(initialValue, currentValue(nonconsumable)); + } + + MutableResource DEBUG_clock = resource(new Clock(ZERO)); + + @Test + void using_runs_synchronously() { + Duration start = currentTime(); + using(nonconsumable, 3.14, () -> { + assertEquals(start, currentTime()); + delay(MINUTE); + }); + assertEquals(start.plus(MINUTE), currentTime()); + } + + @Test + void tasks_in_parallel_with_using_observe_decreased_value() { + double initialValue = currentValue(nonconsumable); + spawn(() -> using(nonconsumable, 3.14, () -> { + delay(MINUTE); + })); + // Allow one tick for effects to be observable from child task + delay(ZERO); + assertEquals(initialValue - 3.14, currentValue(nonconsumable)); + delay(30, SECONDS); + assertEquals(initialValue - 3.14, currentValue(nonconsumable)); + delay(30, SECONDS); + // Allow one tick for effects to be observable from child task + delay(ZERO); + assertEquals(initialValue, currentValue(nonconsumable)); + } + + UnitAware>> settableDataVolume = unitAware(resource(discrete(10.0)), BIT); + + @Test + void unit_aware_set_converts_to_resource_unit() { + set(settableDataVolume, quantity(2, BYTE)); + assertEquals(quantity(16.0, BIT), currentValue(settableDataVolume)); + } + + @Test + void unit_aware_set_throws_exception_if_wrong_dimension_is_used() { + assertThrows(IllegalArgumentException.class, () -> set(settableDataVolume, quantity(2, METER))); + } + + UnitAware>> consumableDataVolume = unitAware(resource(discrete(10.0)), BIT); + + @Test + void unit_aware_consume_converts_to_resource_unit() { + var initialDataVolume = currentValue(consumableDataVolume); + var oneByte = quantity(1, BYTE); + consume(consumableDataVolume, oneByte); + assertEquals(subtract(initialDataVolume, oneByte), currentValue(consumableDataVolume)); + } + + @Test + void unit_aware_consume_throws_exception_if_wrong_dimension_is_used() { + assertThrows(IllegalArgumentException.class, () -> consume(consumableDataVolume, quantity(1, METER))); + } + + @Test + void unit_aware_restore_converts_to_resource_unit() { + var initialDataVolume = currentValue(consumableDataVolume); + var oneByte = quantity(1, BYTE); + restore(consumableDataVolume, oneByte); + assertEquals(add(initialDataVolume, oneByte), currentValue(consumableDataVolume)); + } + + @Test + void unit_aware_restore_throws_exception_if_wrong_dimension_is_used() { + assertThrows(IllegalArgumentException.class, () -> restore(consumableDataVolume, quantity(1, METER))); + } + + UnitAware>> nonconsumableDataVolume = unitAware(resource(discrete(10.0)), BIT); + + @Test + void unit_aware_using_converts_to_resource_unit() { + var initialDataVolume = currentValue(nonconsumableDataVolume); + var oneByte = quantity(1, BYTE); + using(nonconsumableDataVolume, oneByte, () -> { + assertEquals(subtract(initialDataVolume, oneByte), currentValue(nonconsumableDataVolume)); + }); + assertEquals(initialDataVolume, currentValue(nonconsumableDataVolume)); + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/PrecomputedTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/PrecomputedTest.java new file mode 100644 index 0000000000..3849203bf5 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/discrete/PrecomputedTest.java @@ -0,0 +1,140 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.merlin.framework.Registrar; +import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.Instant; +import java.util.Map; +import java.util.TreeMap; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.precomputed; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link DiscreteResources#precomputed} + */ +@ExtendWith(MerlinExtension.class) +@TestInstance(Lifecycle.PER_CLASS) +public class PrecomputedTest { + public PrecomputedTest(final Registrar registrar) { + Resources.init(); + } + + final Resource> precomputedAsAConstant = + precomputed(4, new TreeMap<>()); + @Test + void precomputed_with_no_transitions_uses_default_value_forever() { + assertEquals(4, currentValue(precomputedAsAConstant)); + delay(HOUR); + assertEquals(4, currentValue(precomputedAsAConstant)); + delay(HOUR); + assertEquals(4, currentValue(precomputedAsAConstant)); + } + + final Resource> precomputedWithOneTransitionInFuture = + precomputed(0, new TreeMap<>(Map.of(MINUTE, 10))); + @Test + void precomputed_with_transition_in_future_changes_at_that_time() { + assertEquals(0, currentValue(precomputedWithOneTransitionInFuture)); + assertTransition(precomputedWithOneTransitionInFuture, MINUTE, 10); + delay(HOUR); + assertEquals(10, currentValue(precomputedWithOneTransitionInFuture)); + delay(HOUR); + assertEquals(10, currentValue(precomputedWithOneTransitionInFuture)); + } + + final Resource> precomputedWithOneTransitionInPast = + precomputed(0, new TreeMap<>(Map.of(duration(-1, MINUTE), 10))); + @Test + void precomputed_with_transition_in_past_uses_that_value_forever() { + assertEquals(10, currentValue(precomputedWithOneTransitionInPast)); + delay(HOUR); + assertEquals(10, currentValue(precomputedWithOneTransitionInPast)); + delay(HOUR); + assertEquals(10, currentValue(precomputedWithOneTransitionInPast)); + } + + final Resource> precomputedWithMultipleTransitionsInFuture = + precomputed(0, new TreeMap<>(Map.of( + duration(2, MINUTE), 5, + duration(5, MINUTE), 10, + duration(6, MINUTE), 15))); + @Test + void precomputed_with_multiple_transitions_in_future_goes_through_each_in_turn() { + assertEquals(0, currentValue(precomputedWithMultipleTransitionsInFuture)); + assertTransition(precomputedWithMultipleTransitionsInFuture, duration(2, MINUTE), 5); + assertTransition(precomputedWithMultipleTransitionsInFuture, duration(3, MINUTE), 10); + assertTransition(precomputedWithMultipleTransitionsInFuture, duration(1, MINUTE), 15); + delay(HOUR); + assertEquals(15, currentValue(precomputedWithMultipleTransitionsInFuture)); + delay(HOUR); + assertEquals(15, currentValue(precomputedWithMultipleTransitionsInFuture)); + } + + final Resource> precomputedWithMultipleTransitionsInPast = + precomputed(0, new TreeMap<>(Map.of( + duration(-2, MINUTE), 5, + duration(-5, MINUTE), 10, + duration(-6, MINUTE), 15))); + @Test + void precomputed_with_multiple_transition_in_past_uses_last_value_forever() { + assertEquals(5, currentValue(precomputedWithMultipleTransitionsInPast)); + delay(HOUR); + assertEquals(5, currentValue(precomputedWithMultipleTransitionsInPast)); + delay(HOUR); + assertEquals(5, currentValue(precomputedWithMultipleTransitionsInPast)); + } + + final Resource> precomputedWithTransitionsInPastAndFuture = + precomputed(0, new TreeMap<>(Map.of( + duration(-5, MINUTE), 25, + duration(-2, MINUTE), 5, + duration(5, MINUTE), 10, + duration(6, MINUTE), 15))); + @Test + void precomputed_with_transitions_in_past_and_future_chooses_starting_value_and_changes_later() { + assertEquals(5, currentValue(precomputedWithTransitionsInPastAndFuture)); + assertTransition(precomputedWithTransitionsInPastAndFuture, duration(5, MINUTE), 10); + assertTransition(precomputedWithTransitionsInPastAndFuture, duration(1, MINUTE), 15); + delay(HOUR); + assertEquals(15, currentValue(precomputedWithTransitionsInPastAndFuture)); + delay(HOUR); + assertEquals(15, currentValue(precomputedWithTransitionsInPastAndFuture)); + } + + final Resource> precomputedWithInstantKeys = + precomputed(0, new TreeMap<>(Map.of( + Instant.parse("2023-10-17T23:55:00Z"), 25, + Instant.parse("2023-10-17T23:58:00Z"), 5, + Instant.parse("2023-10-18T00:05:00Z"), 10, + Instant.parse("2023-10-18T00:06:00Z"), 15)), + Instant.parse("2023-10-18T00:00:00Z")); + @Test + void precomputed_with_instant_keys_behaves_identically_to_equivalent_duration_offsets() { + assertEquals(5, currentValue(precomputedWithInstantKeys)); + assertTransition(precomputedWithInstantKeys, duration(5, MINUTE), 10); + assertTransition(precomputedWithInstantKeys, duration(1, MINUTE), 15); + delay(HOUR); + assertEquals(15, currentValue(precomputedWithInstantKeys)); + delay(HOUR); + assertEquals(15, currentValue(precomputedWithInstantKeys)); + } + + private
void assertTransition(Resource> resource, Duration transitionDelay, A expectedValue) { + A startValue = currentValue(resource); + delay(transitionDelay.minus(EPSILON)); + assertEquals(startValue, currentValue(resource)); + delay(EPSILON); + assertEquals(expectedValue, currentValue(resource)); + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java new file mode 100644 index 0000000000..f707482e72 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java @@ -0,0 +1,365 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial; + +import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Expiry; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; +import gov.nasa.jpl.aerie.merlin.framework.Registrar; +import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.set; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentData; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.*; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.EPSILON; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MerlinExtension.class) +@TestInstance(Lifecycle.PER_CLASS) +public class ComparisonsTest { + public ComparisonsTest(final Registrar registrar) { + Resources.init(); + } + + private final MutableResource p = resource(polynomial(0)); + private final MutableResource q = resource(polynomial(0)); + + private final Resource> p_lt_q = lessThan(p, q); + private final Resource> p_lte_q = lessThanOrEquals(p, q); + private final Resource> p_gt_q = greaterThan(p, q); + private final Resource> p_gte_q = greaterThanOrEquals(p, q); + + private final Resource min_p_q = min(p, q); + private final Resource min_q_p = min(q, p); + private final Resource max_p_q = max(p, q); + private final Resource max_q_p = max(q, p); + + @Test + void comparing_distinct_constants() { + setup(() -> { + set(p, polynomial(0)); + set(q, polynomial(1)); + }); + + check_comparison(p_lt_q, true, false); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + check_comparison(p_gte_q, false, false); + } + + @Test + void comparing_equal_constants() { + setup(() -> { + set(p, polynomial(1)); + set(q, polynomial(1)); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + check_comparison(p_gte_q, true, false); + } + + @Test + void comparing_diverging_linear_terms() { + setup(() -> { + set(p, polynomial(0, 1)); + set(q, polynomial(1, 2)); + }); + + check_comparison(p_lt_q, true, false); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + check_comparison(p_gte_q, false, false); + } + + @Test + void comparing_converging_linear_terms() { + setup(() -> { + set(p, polynomial(0, 1)); + set(q, polynomial(2, -1)); + }); + check_comparison(p_lt_q, true, true); + check_comparison(p_lte_q, true, true); + check_comparison(p_gt_q, false, true); + check_comparison(p_gte_q, false, true); + } + + @Test + void comparing_equal_linear_terms() { + setup(() -> { + set(p, polynomial(0, 1)); + set(q, polynomial(0, 1)); + }); + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + check_comparison(p_gte_q, true, false); + } + + @Test + void comparing_equal_then_diverging_linear_terms() { + setup(() -> { + set(p, polynomial(0, 1)); + set(q, polynomial(0, 2)); + }); + // Notice that LT is initially false, but will immediately cross over + check_comparison(p_lt_q, false, true); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + // Notice that GTE is initially true, but will immediately cross over + check_comparison(p_gte_q, true, true); + } + + @Test + void comparing_diverging_nonlinear_terms() { + setup(() -> { + set(p, polynomial(0, 1, 1)); + set(q, polynomial(1, 2, 2)); + }); + check_comparison(p_lt_q, true, false); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + check_comparison(p_gte_q, false, false); + } + + @Test + void comparing_converging_nonlinear_terms() { + setup(() -> { + set(p, polynomial(0, 1, 1)); + set(q, polynomial(1, 2, -1)); + }); + check_comparison(p_lt_q, true, true); + check_comparison(p_lte_q, true, true); + check_comparison(p_gt_q, false, true); + check_comparison(p_gte_q, false, true); + } + + @Test + void comparing_equal_nonlinear_terms() { + setup(() -> { + set(p, polynomial(1, 2, -1)); + set(q, polynomial(1, 2, -1)); + }); + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + check_comparison(p_gte_q, true, false); + } + + @Test + void comparing_equal_then_diverging_nonlinear_terms() { + setup(() -> { + set(p, polynomial(1, 2, -1)); + set(q, polynomial(1, 2, 1)); + }); + // Notice that LT is initially false, but will immediately cross over + check_comparison(p_lt_q, false, true); + check_comparison(p_lte_q, true, false); + check_comparison(p_gt_q, false, false); + // Notice that GTE is initially true, but will immediately cross over + check_comparison(p_gte_q, true, true); + } + + @Test + void extrema_of_equal_resources() { + setup(() -> { + set(p, polynomial(1, 2, -1)); + set(q, polynomial(1, 2, -1)); + }); + + check_extrema(false, false); + } + + @Test + void extrema_of_diverging_resources() { + setup(() -> { + set(p, polynomial(0, 1, -1)); + set(q, polynomial(1, 2, 1)); + }); + + check_extrema(false, false); + } + + @Test + void extrema_of_converging_resources() { + setup(() -> { + set(p, polynomial(0, 1, 1)); + set(q, polynomial(1, 2, -1)); + }); + + check_extrema(false, true); + } + + @Test + void extrema_of_equal_then_diverging_resources() { + setup(() -> { + set(p, polynomial(1, 1, -1)); + set(q, polynomial(1, 2, 1)); + }); + + check_extrema(false, false); + } + + @Test + void extrema_of_first_order_equal_then_diverging_resources() { + setup(() -> { + set(p, polynomial(1, 2, -1)); + set(q, polynomial(1, 2, 1)); + }); + + check_extrema(false, false); + } + + @Test + void extrema_of_tangent_resources() { + setup(() -> { + set(p, polynomial(0, 2, -1)); + set(q, polynomial(2, -2, 1)); + }); + + // No crossover because curves are tangent at t = 1, but q still dominates p + check_extrema(false, false); + // Explicitly check answer at t = 1, just to be sure: + reset(); + delay(SECOND); + check_extrema(false, false); + } + + // "Fine precision": + // Due to floating-point precision, it can take more than 1 microsecond + // to actually change the value of a polynomial if the rates are sufficiently small. + // Implementations of the comparisons and extrema must account for this + // for simulations to be fast and stable. + + @Test + void comparing_equal_then_diverging_linear_terms_with_fine_precision() { + setup(() -> { + set(p, polynomial(1000)); + set(q, polynomial(1000, -1e-20)); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, true, true); + check_comparison(p_gt_q, false, true); + check_comparison(p_gte_q, true, false); + check_extrema(true, false); + } + + @Test + void comparing_converging_linear_terms_with_fine_precision() { + setup(() -> { + set(p, polynomial(1000 - 1e-6, 1e-14)); + set(q, polynomial(1000, -1e-12)); + }); + + check_comparison(p_lt_q, true, true); + check_comparison(p_lte_q, true, true); + check_comparison(p_gt_q, false, true); + check_comparison(p_gte_q, false, true); + check_extrema(false, true); + } + + @Test + void comparing_equal_then_diverging_nonlinear_terms_with_fine_precision() { + setup(() -> { + set(p, polynomial(1000, -1e-20, 2e-22)); + set(q, polynomial(1000, -1e-20, 1e-22)); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, true, true); + check_comparison(p_gt_q, false, true); + check_comparison(p_gte_q, true, false); + check_extrema(true, false); + } + + @Test + void comparing_converging_nonlinear_terms_with_fine_precision() { + setup(() -> { + set(p, polynomial(1000 - 1e-6, 1e-14, 1e20)); + set(q, polynomial(1000, -1e-12, -1e20)); + }); + + check_comparison(p_lt_q, true, true); + check_comparison(p_lte_q, true, true); + check_comparison(p_gt_q, false, true); + check_comparison(p_gte_q, false, true); + check_extrema(false, true); + } + + private void check_comparison(Resource> result, boolean expectedValue, boolean expectCrossover) { + reset(); + var resultDynamics = result.getDynamics().getOrThrow(); + assertEquals(expectedValue, resultDynamics.data().extract()); + assertEquals(expectCrossover, !resultDynamics.expiry().isNever()); + if (expectCrossover) { + Duration crossover = resultDynamics.expiry().value().get(); + delay(crossover.minus(EPSILON)); + assertEquals(expectedValue, currentValue(result)); + delay(EPSILON); + assertEquals(!expectedValue, currentValue(result)); + } + } + + private void check_extrema(boolean expect_p_dominates_q, boolean expectCrossover) { + reset(); + + var minPQDynamics = min_p_q.getDynamics(); + var minQPDynamics = min_q_p.getDynamics(); + var maxPQDynamics = max_p_q.getDynamics(); + var maxQPDynamics = max_q_p.getDynamics(); + + // min and max are exactly symmetric + assertEquals(minPQDynamics, minQPDynamics); + assertEquals(maxPQDynamics, maxQPDynamics); + + // Expiry for min and max are exactly the same + Expiry expiry = minPQDynamics.getOrThrow().expiry(); + assertEquals(expiry, maxPQDynamics.getOrThrow().expiry()); + // Expiry is finite iff we expect a crossover + assertEquals(expectCrossover, !expiry.isNever()); + + var expectedMax = expect_p_dominates_q ? p : q; + var expectedMin = expect_p_dominates_q ? q : p; + + // Data for min and max match their corresponding arguments + assertEquals(currentData(expectedMin), currentData(min_p_q)); + assertEquals(currentData(expectedMax), currentData(max_p_q)); + + if (expectCrossover) { + // Just before crossover, min and max still match their originally stated arguments + delay(expiry.value().get().minus(EPSILON)); + assertEquals(currentData(expectedMin), currentData(min_p_q)); + assertEquals(currentData(expectedMax), currentData(max_p_q)); + // At crossover, min and max swap + delay(EPSILON); + assertEquals(currentData(expectedMax), currentData(min_p_q)); + assertEquals(currentData(expectedMin), currentData(max_p_q)); + } + } + + // Helper utilities to reset the simulation during a test. + // This is helpful to group similar test cases within a single method, + // even though the simulation can advance while running assertions. + private Runnable setupFunction = () -> {}; + private void setup(Runnable setupFunction) { + this.setupFunction = setupFunction; + reset(); + } + private void reset() { + setupFunction.run(); + delay(ZERO); + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolverTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolverTest.java new file mode 100644 index 0000000000..eceec5b70e --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolverTest.java @@ -0,0 +1,235 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial; + +import gov.nasa.jpl.aerie.contrib.streamline.core.*; +import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.Domain; +import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.*; +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentData; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.Comparison.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.LinearBoundaryConsistencySolver.LinearExpression.*; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.*; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static org.junit.jupiter.api.Assertions.*; + +class LinearBoundaryConsistencySolverTest { + @Nested + @ExtendWith(MerlinExtension.class) + @TestInstance(Lifecycle.PER_CLASS) + class SingleVariableSingleConstraint { + MutableResource driver = resource(polynomial(10)); + Resource result; + + public SingleVariableSingleConstraint() { + Resources.init(); + + var solver = new LinearBoundaryConsistencySolver("SingleVariableSingleConstraint"); + var v = solver.variable("v", Domain::upperBound); + result = v.resource(); + solver.declare(lx(v), LessThanOrEquals, lx(driver)); + } + + @Test + void initial_results_are_ready_after_settling() { + settle(); + assertEquals(polynomial(10), currentData(result)); + } + + @Test + void solver_reacts_to_driving_resource() { + set(driver, polynomial(20, -1, 3)); + settle(); + assertEquals(polynomial(20, -1, 3), currentData(result)); + } + + @Test + void results_evolve_with_time() { + set(driver, polynomial(20, -1, 3)); + settle(); + assertEquals(polynomial(20, -1, 3), currentData(result)); + delay(10, SECONDS); + // new dynamics = 20 - 1 (x + 10) + 3 (x + 10)^2 = 310 + 59x + 3x^2 + assertEquals(polynomial(310, 59, 3), currentData(result)); + } + } + + @Nested + @ExtendWith(MerlinExtension.class) + @TestInstance(Lifecycle.PER_CLASS) + class SingleVariableMultipleConstraint { + MutableResource lowerBound1 = resource(polynomial(10)); + MutableResource lowerBound2 = resource(polynomial(20)); + MutableResource upperBound = resource(polynomial(30)); + Resource result; + + public SingleVariableMultipleConstraint() { + Resources.init(); + + var solver = new LinearBoundaryConsistencySolver("SingleVariableMultipleConstraint"); + var v = solver.variable("v", Domain::lowerBound); + result = v.resource(); + solver.declare(lx(v), GreaterThanOrEquals, lx(lowerBound1)); + solver.declare(lx(v), GreaterThanOrEquals, lx(lowerBound2)); + solver.declare(lx(v), LessThanOrEquals, lx(upperBound)); + } + + @Test + void initial_results_use_selection_policy() { + settle(); + assertEquals(polynomial(20), currentData(result)); + } + + @Test + void fully_determined_bounds_are_allowed() { + set(lowerBound1, polynomial(10, 5)); + set(lowerBound2, polynomial(12, 3)); + set(upperBound, polynomial(12, 3)); + settle(); + assertEquals(polynomial(12, 3), currentData(result)); + } + + @Test + void tangent_bounds_use_dominant_behavior() { + // Although lb1 == lb2 now, lb2 has a greater slope, so it dominates + set(lowerBound1, polynomial(12, 3, 5)); + set(lowerBound2, polynomial(12, 4, -1)); + set(upperBound, polynomial(12, 5)); + settle(); + assertEquals(polynomial(12, 4, -1), currentData(result)); + } + + @Test + void infeasible_bounds_result_in_failure() { + set(lowerBound1, polynomial(12, 3, 5)); + set(lowerBound2, polynomial(12, 4, -1)); + set(upperBound, polynomial(11, 7)); + settle(); + assertInstanceOf(ErrorCatching.Failure.class, result.getDynamics()); + } + + /** + * Clearing failures when upstream conditions improve mirrors + * the logic of derived resources, where downstream resources fail + * only when upstream resources fail; downstream resources clear + * when upstream resources clear and derivation succeeds. + */ + @Test + void failures_are_cleared_if_problem_becomes_feasible_again() { + set(lowerBound1, polynomial(12, 3, 5)); + set(lowerBound2, polynomial(12, 4, -1)); + set(upperBound, polynomial(11, 7)); + settle(); + assertInstanceOf(ErrorCatching.Failure.class, result.getDynamics()); + + set(lowerBound1, polynomial(10, 5)); + set(lowerBound2, polynomial(12, 3)); + set(upperBound, polynomial(12, 3)); + settle(); + assertEquals(polynomial(12, 3), currentData(result)); + } + } + + @Nested + @ExtendWith(MerlinExtension.class) + @TestInstance(Lifecycle.PER_CLASS) + class ScalingConstraint { + MutableResource driver = resource(polynomial(10)); + Resource result; + + public ScalingConstraint() { + Resources.init(); + + var solver = new LinearBoundaryConsistencySolver("ScalingConstraint"); + var v = solver.variable("v", Domain::upperBound); + result = v.resource(); + solver.declare(lx(v).multiply(4), LessThanOrEquals, lx(driver)); + } + + @Test + void scaling_can_be_inverted_when_solving() { + settle(); + assertEquals(polynomial(2.5), currentData(result)); + } + + @Test + void scaling_is_respected_for_later_solutions() { + set(driver, polynomial(20, 4, -8)); + settle(); + assertEquals(polynomial(5, 1, -2), currentData(result)); + } + } + + @Nested + @ExtendWith(MerlinExtension.class) + @TestInstance(Lifecycle.PER_CLASS) + class MultipleVariables { + MutableResource upperBound = resource(polynomial(10)); + MutableResource upperBoundOnC = resource(polynomial(5)); + Resource a, b, c; + + public MultipleVariables() { + Resources.init(); + + var solver = new LinearBoundaryConsistencySolver("MultipleVariablesSingleConstraint"); + var a = solver.variable("a", Domain::upperBound); + var b = solver.variable("b", Domain::upperBound); + var c = solver.variable("c", Domain::upperBound); + this.a = a.resource(); + this.b = b.resource(); + this.c = c.resource(); + solver.declare(lx(a).add(lx(b).multiply(2)).subtract(lx(c)), LessThanOrEquals, lx(upperBound)); + solver.declare(lx(c), LessThanOrEquals, lx(upperBoundOnC)); + solver.declare(lx(a), GreaterThanOrEquals, lx(0)); + solver.declare(lx(b), GreaterThanOrEquals, lx(0)); + solver.declare(lx(c), GreaterThanOrEquals, lx(0)); + } + + @Test + void when_problem_is_underconstrained_variables_are_resolved_in_declaration_order() { + settle(); + // Since a is resolved first, it chooses the greatest value it can + assertEquals(polynomial(15), currentData(a)); + // b and c are determined as a result of this. Notice b is constrained all the way down to 0. + assertEquals(polynomial(0), currentData(b)); + assertEquals(polynomial(5), currentData(c)); + } + + @Test + void when_problem_is_fully_determined_the_solution_is_reached() { + set(upperBoundOnC, polynomial(0)); + set(upperBound, polynomial(0)); + // Forces a = b = c = 0 + settle(); + assertEquals(polynomial(0), currentData(a)); + assertEquals(polynomial(0), currentData(b)); + assertEquals(polynomial(0), currentData(c)); + } + + @Test + void solving_works_on_higher_coefficients_too() { + set(upperBoundOnC, polynomial(5, -1)); + set(upperBound, polynomial(10, -2)); + settle(); + assertEquals(polynomial(15, -3), currentData(a)); + assertEquals(polynomial(0), currentData(b)); + assertEquals(polynomial(5, -1), currentData(c)); + // Since the problem will be infeasible at t = 5s, that should be the expiry on everything: + assertEquals(Expiry.at(Duration.of(5, SECONDS)), a.getDynamics().getOrThrow().expiry()); + assertEquals(Expiry.at(Duration.of(5, SECONDS)), b.getDynamics().getOrThrow().expiry()); + assertEquals(Expiry.at(Duration.of(5, SECONDS)), c.getDynamics().getOrThrow().expiry()); + } + } + + static void settle() { + delay(ZERO); + delay(ZERO); + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PrecomputedTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PrecomputedTest.java new file mode 100644 index 0000000000..47427baf17 --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PrecomputedTest.java @@ -0,0 +1,144 @@ +package gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial; + +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resources; +import gov.nasa.jpl.aerie.merlin.framework.Registrar; +import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; +import java.util.TreeMap; + +import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; +import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.precomputed; +import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link PolynomialResources#precomputed} + */ +@ExtendWith(MerlinExtension.class) +@TestInstance(Lifecycle.PER_CLASS) +public class PrecomputedTest { + public PrecomputedTest(final Registrar registrar) { + Resources.init(); + } + + final Resource precomputedAsConstantInPast = + precomputed(new TreeMap<>(Map.of(duration(-1, MINUTE), 4.0))); + @Test + void precomputed_with_single_point_in_past_extrapolates_that_value_forever() { + assertValueEquals(4.0, precomputedAsConstantInPast); + delay(HOUR); + assertValueEquals(4.0, precomputedAsConstantInPast); + delay(HOUR); + assertValueEquals(4.0, precomputedAsConstantInPast); + } + + final Resource precomputedAsConstantInFuture = + precomputed(new TreeMap<>(Map.of(duration(2, HOUR), 4.0))); + @Test + void precomputed_with_single_point_in_future_extrapolates_that_value_forever() { + assertValueEquals(4.0, precomputedAsConstantInFuture); + delay(HOUR); + assertValueEquals(4.0, precomputedAsConstantInFuture); + delay(HOUR); + assertValueEquals(4.0, precomputedAsConstantInFuture); + delay(HOUR); + assertValueEquals(4.0, precomputedAsConstantInFuture); + delay(HOUR); + assertValueEquals(4.0, precomputedAsConstantInFuture); + } + + final Resource precomputedWithSingleInteriorSegmentInPast = + precomputed(new TreeMap<>(Map.of( + duration(-100, SECOND), 0.0, + duration(-50, SECOND), 5.0))); + @Test + void precomputed_with_single_interior_segment_in_past_extrapolates_final_value() { + assertValueEquals(5.0, precomputedWithSingleInteriorSegmentInPast); + delay(HOUR); + assertValueEquals(5.0, precomputedWithSingleInteriorSegmentInPast); + delay(HOUR); + assertValueEquals(5.0, precomputedWithSingleInteriorSegmentInPast); + } + + final Resource precomputedWithSingleInteriorSegmentInFuture = + precomputed(new TreeMap<>(Map.of( + duration(50, SECOND), 0.0, + duration(100, SECOND), 5.0))); + @Test + void precomputed_with_single_interior_segment_in_future_interpolates_that_segment() { + assertValueEquals(0.0, precomputedWithSingleInteriorSegmentInFuture); + delay(50, SECOND); + assertValueEquals(0.0, precomputedWithSingleInteriorSegmentInFuture); + delay(10, SECOND); + assertValueEquals(1.0, precomputedWithSingleInteriorSegmentInFuture); + delay(10, SECOND); + assertValueEquals(2.0, precomputedWithSingleInteriorSegmentInFuture); + delay(10, SECOND); + assertValueEquals(3.0, precomputedWithSingleInteriorSegmentInFuture); + delay(10, SECOND); + assertValueEquals(4.0, precomputedWithSingleInteriorSegmentInFuture); + delay(10, SECOND); + assertValueEquals(5.0, precomputedWithSingleInteriorSegmentInFuture); + delay(HOUR); + assertValueEquals(5.0, precomputedWithSingleInteriorSegmentInFuture); + } + + final Resource precomputedStartingInInterior = + precomputed(new TreeMap<>(Map.of( + duration(-50, SECOND), 0.0, + duration(50, SECOND), 10.0))); + void precomputed_starting_in_interior_interpolates_over_full_segment() { + assertValueEquals(5.0, precomputedStartingInInterior); + delay(10, SECOND); + assertValueEquals(6.0, precomputedStartingInInterior); + delay(10, SECOND); + assertValueEquals(7.0, precomputedStartingInInterior); + delay(10, SECOND); + assertValueEquals(8.0, precomputedStartingInInterior); + delay(10, SECOND); + assertValueEquals(9.0, precomputedStartingInInterior); + delay(10, SECOND); + assertValueEquals(10.0, precomputedStartingInInterior); + delay(HOUR); + assertValueEquals(10.0, precomputedStartingInInterior); + } + + final Resource precomputedWithMultipleSegments = + precomputed(new TreeMap<>(Map.of( + duration(-50, SECOND), 0.0, + duration(50, SECOND), 10.0, + duration(60, SECOND), 30.0, + duration(90, SECOND), -30.0))); + @Test + void precomputed_with_multiple_segments_interpolates_each_segment_independently() { + assertValueEquals(5.0, precomputedWithMultipleSegments); + delay(25, SECOND); + assertValueEquals(7.5, precomputedWithMultipleSegments); + delay(25, SECOND); + assertValueEquals(10.0, precomputedWithMultipleSegments); + delay(5, SECOND); + assertValueEquals(20.0, precomputedWithMultipleSegments); + delay(5, SECOND); + assertValueEquals(30.0, precomputedWithMultipleSegments); + delay(10, SECOND); + assertValueEquals(10.0, precomputedWithMultipleSegments); + delay(10, SECOND); + assertValueEquals(-10.0, precomputedWithMultipleSegments); + delay(10, SECOND); + assertValueEquals(-30.0, precomputedWithMultipleSegments); + } + + private static final double TOLERANCE = 1e-13; + private static final double EPSILON = 1e-10; + private void assertValueEquals(double expected, Resource resource) { + assertTrue(Math.abs(expected - currentValue(resource)) / (expected + EPSILON) < TOLERANCE, + "Resource value equals " + expected); + } +} diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/DimensionTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/DimensionTest.java new file mode 100644 index 0000000000..8376b89eef --- /dev/null +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/unit_aware/DimensionTest.java @@ -0,0 +1,192 @@ +package gov.nasa.jpl.aerie.contrib.streamline.unit_aware; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Dimension.SCALAR; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.Rational.*; +import static gov.nasa.jpl.aerie.contrib.streamline.unit_aware.StandardDimensions.*; +import static org.junit.jupiter.api.Assertions.*; + +class DimensionTest { + @Nested + class BaseDimensions { + @Test + void equal_themselves() { + assertEquals(LENGTH, LENGTH); + } + + @Test + void are_distinct() { + assertNotEquals(LENGTH, TIME); + } + + @Test + void satisfy_is_base_check() { + assertTrue(LENGTH.isBase()); + } + } + + @Nested + class DimensionProducts { + @Test + void are_distinct_from_both_factors() { + Dimension MASS_LENGTH = MASS.multiply(LENGTH); + assertNotEquals(MASS, MASS_LENGTH); + assertNotEquals(LENGTH, MASS_LENGTH); + } + + @Test + void are_equal_to_identically_derived_dimensions() { + Dimension MASS_LENGTH_1 = MASS.multiply(LENGTH); + Dimension MASS_LENGTH_2 = MASS.multiply(LENGTH); + assertEquals(MASS_LENGTH_1, MASS_LENGTH_2); + } + + @Test + void are_not_equal_to_different_products() { + Dimension MASS_LENGTH = MASS.multiply(LENGTH); + Dimension MASS_TIME = MASS.multiply(TIME); + assertNotEquals(MASS_LENGTH, MASS_TIME); + } + + @Test + void fail_is_base_check() { + Dimension MASS_LENGTH = MASS.multiply(LENGTH); + assertFalse(MASS_LENGTH.isBase()); + } + + @Test + void commute() { + Dimension MASS_LENGTH = MASS.multiply(LENGTH); + Dimension LENGTH_MASS = LENGTH.multiply(MASS); + assertEquals(MASS_LENGTH, LENGTH_MASS); + } + + @Test + void associate() { + Dimension MASS_LENGTH_TIME = MASS.multiply(LENGTH).multiply(TIME); + Dimension MASS_LENGTH_TIME_2 = MASS.multiply(LENGTH.multiply(TIME)); + assertEquals(MASS_LENGTH_TIME, MASS_LENGTH_TIME_2); + } + + @Test + void have_scalar_as_left_identity() { + Dimension MASS_2 = SCALAR.multiply(MASS); + assertEquals(MASS, MASS_2); + } + + @Test + void have_scalar_as_right_identity() { + Dimension MASS_2 = MASS.multiply(SCALAR); + assertEquals(MASS, MASS_2); + } + } + + @Nested + class DimensionQuotients { + @Test + void are_distinct_from_both_factors() { + assertNotEquals(LENGTH, LENGTH.divide(TIME)); + assertNotEquals(TIME, LENGTH.divide(TIME)); + } + + @Test + void are_equal_to_identically_derived_dimensions() { + assertEquals(LENGTH.divide(TIME), LENGTH.divide(TIME)); + } + + @Test + void are_not_equal_to_different_quotients() { + assertNotEquals(LENGTH.divide(TIME), MASS.divide(TIME)); + } + + @Test + void fail_is_base_check() { + assertFalse(LENGTH.divide(TIME).isBase()); + } + + @Test + void do_not_commute() { + assertNotEquals(LENGTH.divide(TIME), TIME.divide(LENGTH)); + } + + @Test + void do_not_have_scalar_as_left_identity() { + assertNotEquals(MASS, SCALAR.divide(MASS)); + } + + @Test + void have_scalar_as_right_identity() { + assertEquals(MASS, MASS.divide(SCALAR)); + } + + @Test + void invert_dimension_products() { + assertEquals(MASS, MASS.multiply(LENGTH).divide(LENGTH)); + } + + @Test + void are_inverted_by_dimension_products() { + assertEquals(MASS, MASS.divide(LENGTH).multiply(LENGTH)); + } + } + + @Nested + class DimensionPowers { + @Test + void have_one_as_right_identity() { + assertEquals(MASS, MASS.power(ONE)); + } + + @Test + void have_zero_as_right_annihilator() { + assertEquals(SCALAR, MASS.power(ZERO)); + } + + @Test + void have_scalar_as_left_annihilator() { + assertEquals(SCALAR, SCALAR.power(rational(2))); + assertEquals(SCALAR, SCALAR.power(rational(-2))); + assertEquals(SCALAR, SCALAR.power(rational(0))); + } + + @Test + void are_equal_to_repeated_multiplication_for_positive_integer_powers() { + assertEquals(MASS.multiply(MASS), MASS.power(rational(2))); + assertEquals(MASS.multiply(MASS).multiply(MASS), MASS.power(rational(3))); + } + + @Test + void are_equal_to_repeated_division_for_negative_integer_powers() { + assertEquals(SCALAR.divide(MASS), MASS.power(rational(-1))); + assertEquals(SCALAR.divide(MASS).divide(MASS), MASS.power(rational(-2))); + } + + @Test + void distribute_over_products() { + var p = rational(2, 3); + assertEquals(MASS.power(p).multiply(LENGTH.power(p)), MASS.multiply(LENGTH).power(p)); + } + + @Test + void distribute_over_quotients() { + var p = rational(2, 3); + assertEquals(MASS.power(p).divide(LENGTH.power(p)), MASS.divide(LENGTH).power(p)); + } + + @Test + void add_exactly_when_multiplying_common_bases() { + var p = rational(2, 3); + var q = rational(1, 3); + assertEquals(MASS, MASS.power(p).multiply(MASS.power(q))); + } + + @Test + void subtract_exactly_when_multiplying_common_bases() { + var p = rational(4, 3); + var q = rational(1, 3); + assertEquals(MASS, MASS.power(p).divide(MASS.power(q))); + } + } +} From 8097ffff4bae573a265b580a5f90dabd9bb42dc9 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 29 Jan 2024 10:19:16 -0800 Subject: [PATCH 031/159] Rename applicator to cellType in allocate --- .../java/gov/nasa/jpl/aerie/merlin/framework/CellRef.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/CellRef.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/CellRef.java index aff20238e7..0a6bf5180b 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/CellRef.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/CellRef.java @@ -37,8 +37,8 @@ CellRef allocate( } public static - CellRef allocate(final State initialState, final CellType applicator) { - return allocate(initialState, applicator, $ -> $); + CellRef allocate(final State initialState, final CellType cellType) { + return allocate(initialState, cellType, $ -> $); } public State get() { From 5ab58acdccddc6c725e0edbcd84959189262ff81 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 29 Jan 2024 10:31:55 -0800 Subject: [PATCH 032/159] Rename constraintsLeft to remainingConstraints --- .../polynomial/LinearBoundaryConsistencySolver.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java index 9f7f258854..69cbdb5c1d 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java @@ -113,13 +113,13 @@ private void buildNeighboringConstraints() { private void solve() { final var domains = variables.stream().collect(toMap(identity(), Domain::new)); - final Queue constraintsLeft = new LinkedList<>(constraints); + final Queue remainingConstraints = new LinkedList<>(constraints); DirectionalConstraint constraint; try { // While we either have constraints to apply or domains to solve... - while (!constraintsLeft.isEmpty() || domains.values().stream().anyMatch(Domain::isUnsolved)) { + while (!remainingConstraints.isEmpty() || domains.values().stream().anyMatch(Domain::isUnsolved)) { // Apply all constraints through simple arc consistency - while ((constraint = constraintsLeft.poll()) != null) { + while ((constraint = remainingConstraints.poll()) != null) { var V = constraint.constrainedVariable; var D = domains.get(V); var newBound = constraint.bound.apply(domains).getDynamics().getOrThrow(); @@ -134,7 +134,7 @@ private void solve() { getName(this).orElseThrow(), D.variable, D.lowerBound, D.upperBound)); } // TODO: Make this more efficient by not adding constraints that are already in the queue. - constraintsLeft.addAll(neighboringConstraints.get(D.variable)); + remainingConstraints.addAll(neighboringConstraints.get(D.variable)); } } // If that didn't fully solve all variables, choose the first unsolved variable @@ -146,7 +146,7 @@ private void solve() { .findFirst() .ifPresent(D -> { D.lowerBound = D.upperBound = D.variable.selectionPolicy.apply(D); - constraintsLeft.addAll(neighboringConstraints.get(D.variable)); + remainingConstraints.addAll(neighboringConstraints.get(D.variable)); }); } // All domains are solved and non-empty, emit solution From d0a1b2c1839bbd7857110af751769babc41cdb75 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 29 Jan 2024 10:32:30 -0800 Subject: [PATCH 033/159] Return thing from name --- .../nasa/jpl/aerie/contrib/streamline/debugging/Naming.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java index 8929b57a5a..c161cccbb8 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java @@ -33,7 +33,7 @@ private Naming() {} * Register a name for thing, as a function of args' names. * If any of the args are anonymous, so is this thing. */ - public static void name(Object thing, String nameFormat, Object... args) { + public static T name(T thing, String nameFormat, Object... args) { // Only capture weak references to arguments, so we don't leak memory var args$ = Arrays.stream(args).map(WeakReference::new).toArray(WeakReference[]::new); NAMES.put(thing, () -> { @@ -49,6 +49,7 @@ public static void name(Object thing, String nameFormat, Object... args) { } return Optional.of(nameFormat.formatted(argNames)); }); + return thing; } /** From a23b503ae2150dbc70700100351a72cc9b6a3a1e Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 29 Jan 2024 10:33:16 -0800 Subject: [PATCH 034/159] Fix typo in Tracing --- .../nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java index ebe68b3ff0..5a954aeaf1 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Tracing.java @@ -44,7 +44,7 @@ public static > MutableResource trace(String name, M public static > MutableResource trace(Supplier name, MutableResource resource) { return new MutableResource<>() { - private final Resource tracedResoure = trace(name, (Resource) resource); + private final Resource tracedResource = trace(name, (Resource) resource); @Override public void emit(final DynamicsEffect effect) { @@ -57,7 +57,7 @@ public void emit(final DynamicsEffect effect) { @Override public ErrorCatching> getDynamics() { - return tracedResoure.getDynamics(); + return tracedResource.getDynamics(); } }; } From 6bbb77fe74b4954b5cc5a0712d0e7d6eacd37a33 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 29 Jan 2024 10:34:14 -0800 Subject: [PATCH 035/159] Shift check interval sign --- .../contrib/streamline/core/Resources.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java index 8d5de971a8..3ed1821ec3 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java @@ -16,9 +16,7 @@ import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.neverExpiring; import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.NEVER; -import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever; import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.wheneverDynamicsChange; -import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.map; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock.clock; @@ -44,6 +42,7 @@ public static void init() { currentTime(); } + // TODO if Aerie provides either a `getElapsedTime` method or dynamic allocation of Cells, we can avoid this mutable static variable private static Resource CLOCK = resource(clock(ZERO)); public static Duration currentTime() { try { @@ -109,13 +108,13 @@ public static > Condition dynamicsChange(Resource re // Use semantic comparison for exceptions, since derivation can generate the exception each invocation. currentException -> !equivalentExceptions(startException, currentException))); - return positive == haveChanged - ? Optional.of(atEarliest) - : positive - ? currentDynamics.match( + if (positive == haveChanged) return Optional.of(atEarliest); + if (!positive) return Optional.empty(); // Dynamics have changed, so they will never be unchanged + + // Dynamics haven't changed, but they may expire before atLatest. Treat expiry as a change + return currentDynamics.match( expiring -> expiring.expiry().value().filter(atLatest::noShorterThan), - exception -> Optional.empty()) - : Optional.empty(); + exception -> Optional.empty()); }; name(result, "Dynamics Change (%s)", resource); return result; @@ -187,7 +186,6 @@ public static > void forward(Resource source, Mutabl addDependency(destination, source); } - // TODO: Should this be moved somewhere else? /** * Tests if two exceptions are equivalent from the point of view of resource values. * Two exceptions are equivalent if they have the same type and message. @@ -216,7 +214,7 @@ public static boolean equivalentExceptions(Throwable startException, Throwable c *

*/ public static > Resource cache(Resource resource) { - var cell = resource(resource.getDynamics()); + final var cell = resource(resource.getDynamics()); forward(resource, cell); name(cell, "Cache (%s)", resource); return cell; @@ -266,6 +264,12 @@ public static > Resource signalling(Resource reso } public static > Resource shift(Resource resource, Duration interval, D initialDynamics) { + if (interval.shorterThan(ZERO)) { + throw new IllegalArgumentException("Cannot shift resource by negative interval: " + interval); + } + if (interval.isEqualTo(ZERO)) { + return resource; + } var cell = resource(initialDynamics); delayedSet(cell, resource.getDynamics(), interval); wheneverDynamicsChange(resource, newDynamics -> From fb15cdaa3ce8dcf8ad46ecf772ac061a813a135a Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 29 Jan 2024 10:35:06 -0800 Subject: [PATCH 036/159] Add javadocs and remove unused imports --- .../jpl/aerie/contrib/streamline/core/CellRefV2.java | 7 ++++++- .../jpl/aerie/contrib/streamline/core/Dynamics.java | 2 ++ .../jpl/aerie/contrib/streamline/core/Reactions.java | 1 - .../aerie/contrib/streamline/modeling/Registrar.java | 10 ++++++++-- .../modeling/black_box/UnstructuredResources.java | 3 +-- .../streamline/modeling/polynomial/Polynomial.java | 11 +++++++++++ .../jpl/aerie/streamline_demo/ErrorTestingModel.java | 4 ++-- 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java index 064f575411..c747d0a689 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/CellRefV2.java @@ -1,7 +1,6 @@ package gov.nasa.jpl.aerie.contrib.streamline.core; import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ErrorCatchingMonad; -import gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming; import gov.nasa.jpl.aerie.merlin.framework.CellRef; import gov.nasa.jpl.aerie.merlin.protocol.model.CellType; import gov.nasa.jpl.aerie.merlin.protocol.model.EffectTrait; @@ -15,6 +14,9 @@ import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +/** + * Utility class for a simplified allocate method. + */ public final class CellRefV2 { private CellRefV2() {} @@ -89,6 +91,9 @@ public static > EffectTrait> autoEffe * correctly comparing expiry and error information in the process. */ public static Predicate>>> testing(Predicate> test) { + // If both expiring, compare expiry and data + // If both error, compare error contents + // If one is expiring and the other is error, return false return input -> input.leftResult.match( leftExpiring -> input.rightResult.match( rightExpiring -> leftExpiring.expiry().equals(rightExpiring.expiry()) && test.test(new CommutativityTestInput<>( diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java index c70509f47b..1d4fd78376 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Dynamics.java @@ -14,6 +14,8 @@ public interface Dynamics> { /** * Evolve for the given time. + * + * @apiNote This method should always return the same value when called on the same object with the same duration */ D step(Duration t); } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java index 5ae4597240..17125f3460 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Reactions.java @@ -3,7 +3,6 @@ import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; import gov.nasa.jpl.aerie.merlin.framework.Condition; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import org.apache.commons.lang3.mutable.MutableObject; import java.util.function.Consumer; import java.util.function.Supplier; diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java index a173fe683d..124eb56d5c 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/Registrar.java @@ -70,6 +70,9 @@ public Registrar(final gov.nasa.jpl.aerie.merlin.framework.Registrar baseRegistr this.errorBehavior = errorBehavior; errors = resource(Discrete.discrete(Map.of())); var errorString = map(errors, errors$ -> errors$.entrySet().stream().map(entry -> formatError(entry.getKey(), entry.getValue())).collect(joining("\n\n"))); + + // Register the errors and number of errors resources for output + // TODO consider using serializable events, rather than resources, to log errors discrete("errors", errorString, new StringValueMapper()); discrete("numberOfErrors", map(errors, Map::size), new IntegerValueMapper()); } @@ -99,7 +102,7 @@ public void setProfile() { } public void clearProfile() { - profile = true; + profile = false; } public void discrete(final String name, final Resource> resource, final ValueMapper mapper) { @@ -145,7 +148,7 @@ private > void logErrors(String name, Resource resou }); } - // TODO: Consider pulling in a Guava MultiMap instead of doing this by hand below + // TODO: Consider using a MultiMap instead of doing this by hand below private Unit logError(String resourceName, Throwable e) { errors.emit(effect(s -> { var s$ = new HashMap<>(s); @@ -163,6 +166,9 @@ private Unit logError(String resourceName, Throwable e) { return Unit.UNIT; } + /** + * Include the resource name in the error to give context + */ private static gov.nasa.jpl.aerie.merlin.framework.Resource wrapErrors(String resourceName, gov.nasa.jpl.aerie.merlin.framework.Resource resource) { return () -> { try { diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java index faba22ce1f..8b90f68fad 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/UnstructuredResources.java @@ -1,7 +1,6 @@ package gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box; import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics; -import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.SecantApproximation.ErrorEstimates; import gov.nasa.jpl.aerie.contrib.streamline.modeling.black_box.monads.UnstructuredResourceApplicative; @@ -32,7 +31,7 @@ public static
Resource> timeBased(Function f) { // Put this in a cell so it'll be stepped up appropriately return resource(Unstructured.timeBased(f)); } - + public static > Resource> asUnstructured(Resource resource) { return map(resource, Unstructured::unstructured); } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java index ab8d3cb541..7e00dd83a9 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java @@ -25,9 +25,20 @@ import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; import static org.apache.commons.math3.analysis.polynomials.PolynomialsUtils.shift; +/** + * An implementation of Polynomial Dynamics + * @param coefficients an array of polynomial coefficients, where an entry's index in the array corresponds to the degree of that coefficient + * + * @apiNote The units of `t` are seconds + */ public record Polynomial(double[] coefficients) implements Dynamics { // TODO: Add Duration parameter for unit of formal parameter? + /** + * + * @param coefficients the polynomial coefficients, from least to most significant + * @return a Polynomial with the given coefficients + */ public static Polynomial polynomial(double... coefficients) { int n = coefficients.length; if (n == 0) { diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java index ce2693751f..8c4de6e23a 100644 --- a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/ErrorTestingModel.java @@ -10,7 +10,7 @@ import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial; import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources; -import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.discreteResource; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.name; import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteResourceMonad.map; import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.PolynomialResources.*; @@ -23,7 +23,7 @@ public class ErrorTestingModel { asPolynomial(map(counter, c -> (double) c)), asPolynomial(map(bool, $ -> $ ? 1.0 : -1.0))); - public MutableResource upperBound = PolynomialResources.polynomialResource(5); + public MutableResource upperBound = name(PolynomialResources.polynomialResource(5), "upperBound"); public MutableResource lowerBound = PolynomialResources.polynomialResource(-5); public Resource clamped = clamp(constant(10), lowerBound, upperBound); From 882d1315d6b74a4232da77e3c222b1e1f51b723a Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 22 Jan 2024 13:12:32 -0800 Subject: [PATCH 037/159] Add Updated_At column to Merge Requests This will enable us to see when the status of a merge request last changed. --- .../AerieMerlin/36_metadata_updated_at/down.sql | 5 +++++ .../AerieMerlin/36_metadata_updated_at/up.sql | 17 +++++++++++++++++ merlin-server/sql/merlin/applied_migrations.sql | 1 + .../sql/merlin/tables/merge_request.sql | 14 ++++++++++++++ 4 files changed, 37 insertions(+) create mode 100644 deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/down.sql create mode 100644 deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/up.sql diff --git a/deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/down.sql b/deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/down.sql new file mode 100644 index 0000000000..c316c5c7f0 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/down.sql @@ -0,0 +1,5 @@ +drop trigger set_timestamp on merge_request; +drop function merge_request_set_updated_at(); +alter table merge_request drop column updated_at; + +call migrations.mark_migration_rolled_back('36'); diff --git a/deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/up.sql b/deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/up.sql new file mode 100644 index 0000000000..841731f870 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/36_metadata_updated_at/up.sql @@ -0,0 +1,17 @@ +alter table merge_request add column updated_at timestamptz not null default now(); + +create function merge_request_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp + before update or insert on merge_request + for each row +execute function merge_request_set_updated_at(); + +call migrations.mark_migration_applied('36'); + diff --git a/merlin-server/sql/merlin/applied_migrations.sql b/merlin-server/sql/merlin/applied_migrations.sql index 0d4d1d7177..d3096db0e8 100644 --- a/merlin-server/sql/merlin/applied_migrations.sql +++ b/merlin-server/sql/merlin/applied_migrations.sql @@ -38,3 +38,4 @@ call migrations.mark_migration_applied('32'); call migrations.mark_migration_applied('33'); call migrations.mark_migration_applied('34'); call migrations.mark_migration_applied('35'); +call migrations.mark_migration_applied('36'); diff --git a/merlin-server/sql/merlin/tables/merge_request.sql b/merlin-server/sql/merlin/tables/merge_request.sql index b92758b0e1..6049c2d5dd 100644 --- a/merlin-server/sql/merlin/tables/merge_request.sql +++ b/merlin-server/sql/merlin/tables/merge_request.sql @@ -7,6 +7,7 @@ create table merge_request( status merge_request_status default 'pending', requester_username text, reviewer_username text, + updated_at timestamptz not null default now(), constraint merge_request_requester_exists foreign key (requester_username) references metadata.users @@ -39,3 +40,16 @@ comment on column merge_request.requester_username is e'' 'The user who created this merge request.'; comment on column merge_request.reviewer_username is e'' 'The user who reviews this merge request. Is empty until the request enters review.'; + +create function merge_request_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp + before update or insert on merge_request + for each row +execute function merge_request_set_updated_at(); From 59c5303a43f87e775c823b02f5bccaeae041df30 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Wed, 31 Jan 2024 06:40:45 -1000 Subject: [PATCH 038/159] Fixed an issue where constraint runs would fail against activities with unitized variants --- .../nasa/jpl/aerie/e2e/ConstraintsTests.java | 48 +++++++++++++++++++ .../activities/PeelBananaActivity.java | 2 + .../driver/json/ValueSchemaJsonParser.java | 1 + 3 files changed, 51 insertions(+) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java index fd91e68acc..bc443b9630 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java @@ -132,6 +132,54 @@ void constraintsSucceedOneViolation() throws IOException { assertTrue(constraintResult.gaps().isEmpty()); } + @Test + void constraintsSucceedAgainstVariantWithUnits() throws IOException { + hasura.deleteActivity(planId, activityId); + hasura.deleteConstraint(constraintId); + var fruitConstraintName = "fruit_equals_3"; + var fruitConstraintId = hasura.insertPlanConstraint( + fruitConstraintName, + planId, + "export default (): Constraint => Real.Resource(\"/fruit\").equal(3)", + ""); + hasura.insertActivity( + planId, + "PeelBanana", + "1h", + Json.createObjectBuilder().add("peelDirection", "fromStem").build()); + + hasura.awaitSimulation(planId); + final var constraintsResponses = hasura.checkConstraints(planId); + assertEquals(1, constraintsResponses.size()); + + // Check the Response + final var constraintResponse = constraintsResponses.get(0); + assertTrue(constraintResponse.success()); + assertEquals(fruitConstraintId, constraintResponse.constraintId()); + assertEquals(fruitConstraintName, constraintResponse.constraintName()); + assertEquals("plan", constraintResponse.type()); + // Check the Result + assertTrue(constraintResponse.result().isPresent()); + final var constraintResult = constraintResponse.result().get(); + // Resources + final var resources = constraintResult.resourceIds(); + assertEquals(1, resources.size()); + assertTrue(resources.contains("/fruit")); + + // Violation + assertEquals(1, constraintResult.violations().size()); + final var violation = constraintResult.violations().get(0); + assertEquals(1, violation.windows().size()); + + final long activityOffset = 60 * 60 * 1000000L; // 1h in micros + final long planDuration = 1212 * 60 * 60 * 1000000L; // 1212h in micros + + assertEquals(activityOffset, violation.windows().get(0).start()); + assertEquals(planDuration, violation.windows().get(0).end()); + // Gaps + assertTrue(constraintResult.gaps().isEmpty()); + } + @Test void constraintsSucceedNoViolations() throws IOException { // Delete activity to avoid violation diff --git a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java index be6006d8c2..4436e29bb0 100644 --- a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java +++ b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.banananation.activities; import gov.nasa.jpl.aerie.banananation.Mission; +import gov.nasa.jpl.aerie.contrib.metadata.Unit; import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType.EffectModel; import gov.nasa.jpl.aerie.merlin.framework.annotations.Export.Parameter; @@ -25,6 +26,7 @@ public enum PeelDirectionEnum { } @Parameter + @Unit("direction") public PeelDirectionEnum peelDirection = PeelDirectionEnum.fromStem; @EffectModel diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/json/ValueSchemaJsonParser.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/json/ValueSchemaJsonParser.java index 810e20ea63..b369d676d7 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/json/ValueSchemaJsonParser.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/json/ValueSchemaJsonParser.java @@ -94,6 +94,7 @@ private JsonParseResult parseVariant(final JsonObject obj) { productP .field("type", literalP("variant")) .field("variants", listP(variantP)) + .rest() .map( untuple((type, variants) -> ValueSchema.ofVariant(variants)), $ -> tuple(Unit.UNIT, $.asVariant().get())); From f49294b66f8d7f3fed25f48eeb94c77eeb40a91c Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Wed, 31 Jan 2024 07:03:43 -1000 Subject: [PATCH 039/159] Fixed the new constraint test --- .../test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java index bc443b9630..505843644b 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java @@ -172,10 +172,9 @@ void constraintsSucceedAgainstVariantWithUnits() throws IOException { assertEquals(1, violation.windows().size()); final long activityOffset = 60 * 60 * 1000000L; // 1h in micros - final long planDuration = 1212 * 60 * 60 * 1000000L; // 1212h in micros - assertEquals(activityOffset, violation.windows().get(0).start()); - assertEquals(planDuration, violation.windows().get(0).end()); + assertEquals(0, violation.windows().get(0).start()); + assertEquals(activityOffset, violation.windows().get(0).end()); // Gaps assertTrue(constraintResult.gaps().isEmpty()); } From 795b547c97172666daf566103a4bed30cd6754a9 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Wed, 31 Jan 2024 07:30:55 -1000 Subject: [PATCH 040/159] Added the new metadata to fix our activity upload test --- .../java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java index 99fb616742..7947734f12 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/MissionModelTests.java @@ -311,9 +311,11 @@ private ArrayList expectedActivityTypesBanananation() { activityTypes.add(new ActivityType( "PeelBanana", Map.of("peelDirection", - new Parameter(0, new ValueSchemaVariant(List.of( - new Variant("fromStem", "fromStem"), - new Variant("fromTip", "fromTip"))))))); + new Parameter(0, + new ValueSchemaMeta(Map.of("unit", Json.createObjectBuilder(Map.of("value", "direction")).build()), + new ValueSchemaVariant(List.of( + new Variant("fromStem", "fromStem"), + new Variant("fromTip", "fromTip")))))))); activityTypes.add(new ActivityType("PickBanana", Map.of("quantity", new Parameter(0, VALUE_SCHEMA_INT)))); activityTypes.add(new ActivityType("RipenBanana", Map.of())); activityTypes.add(new ActivityType("ThrowBanana", Map.of("speed", new Parameter(0, VALUE_SCHEMA_REAL)))); From f44e2c1798dbefb5a545289787af78d3741821c9 Mon Sep 17 00:00:00 2001 From: joswig Date: Fri, 2 Feb 2024 21:38:09 +0000 Subject: [PATCH 041/159] Release v2.3.0 --- gradle.properties | 2 +- sequencing-server/package-lock.json | 4 ++-- sequencing-server/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8a2c3f161b..67af9236e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ publishing.version= # Override for releases # Change the version number here -version.number=2.2.0 +version.number=2.3.0 # If you are publishing a release *manually* (i.e. not through github actions), # override this on the command line with `./gradlew publish -Pversion.isRelease=true`. diff --git a/sequencing-server/package-lock.json b/sequencing-server/package-lock.json index 57d08ded87..dcbd67729f 100644 --- a/sequencing-server/package-lock.json +++ b/sequencing-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "sequencing-server", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sequencing-server", - "version": "2.2.0", + "version": "2.3.0", "license": "MIT", "dependencies": { "@js-temporal/polyfill": "~0.4.3", diff --git a/sequencing-server/package.json b/sequencing-server/package.json index e3f89024d4..9eb4106c44 100644 --- a/sequencing-server/package.json +++ b/sequencing-server/package.json @@ -1,6 +1,6 @@ { "name": "sequencing-server", - "version": "2.2.0", + "version": "2.3.0", "description": "Aerie sequencing server", "type": "module", "license": "MIT", From 990edf5106165543e9d984191ff17c789f31cc79 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 23 Oct 2023 12:43:04 -0700 Subject: [PATCH 042/159] Use skip-list multi-map to speed up job queue --- .../merlin/driver/engine/JobSchedule.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java index d4ffdd731c..28a740fb13 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java @@ -1,15 +1,13 @@ package gov.nasa.jpl.aerie.merlin.driver.engine; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import org.apache.commons.lang3.tuple.Pair; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.PriorityQueue; import java.util.Set; +import java.util.concurrent.ConcurrentSkipListMap; public final class JobSchedule { /** The scheduled time for each upcoming job. */ @@ -17,43 +15,45 @@ public final class JobSchedule { /** A time-ordered queue of all tasks whose resumption time is concretely known. */ @DerivedFrom("scheduledJobs") - private final PriorityQueue> queue = new PriorityQueue<>(Comparator.comparing(Pair::getLeft)); + private final ConcurrentSkipListMap> queue = new ConcurrentSkipListMap<>(); public void schedule(final JobRef job, final TimeRef time) { final var oldTime = this.scheduledJobs.put(job, time); - if (oldTime != null) this.queue.remove(Pair.of(oldTime, job)); - this.queue.add(Pair.of(time, job)); + if (oldTime != null) removeJobFromQueue(oldTime, job); + + this.queue.compute(time, (t, jobsAtNewTime) -> { + if (jobsAtNewTime == null) jobsAtNewTime = new HashSet<>(); + jobsAtNewTime.add(job); + return jobsAtNewTime; + }); } public void unschedule(final JobRef job) { final var oldTime = this.scheduledJobs.remove(job); + if (oldTime != null) removeJobFromQueue(oldTime, job); + } - if (oldTime != null) this.queue.remove(Pair.of(oldTime, job)); + private void removeJobFromQueue(TimeRef time, JobRef job) { + var jobsAtOldTime = this.queue.get(time); + jobsAtOldTime.remove(job); + if (jobsAtOldTime.isEmpty()) { + this.queue.remove(time); + } } public Batch extractNextJobs(final Duration maximumTime) { if (this.queue.isEmpty()) return new Batch<>(maximumTime, Collections.emptySet()); - final var time = this.queue.peek().getKey(); + final var time = this.queue.firstKey(); if (time.project().longerThan(maximumTime)) { return new Batch<>(maximumTime, Collections.emptySet()); } // Ready all tasks at the soonest task time. - final var readyJobs = new HashSet(); - while (true) { - final var entry = this.queue.peek(); - if (entry == null) break; - if (entry.getLeft().compareTo(time) > 0) break; - - this.scheduledJobs.remove(entry.getRight()); - this.queue.remove(); - - readyJobs.add(entry.getRight()); - } - - return new Batch<>(time.project(), readyJobs); + final var entry = this.queue.pollFirstEntry(); + entry.getValue().forEach(this.scheduledJobs::remove); + return new Batch<>(entry.getKey().project(), entry.getValue()); } public void clear() { From 7a3b8ff082e354e0df481bc75fe02e171c63180a Mon Sep 17 00:00:00 2001 From: DavidLegg Date: Tue, 14 Nov 2023 11:56:41 -0800 Subject: [PATCH 043/159] Clarify "add job" expression Co-authored-by: Matt Dailis --- .../nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java index 28a740fb13..4f7f7bda02 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java @@ -22,11 +22,7 @@ public void schedule(final JobRef job, final TimeRef time) { if (oldTime != null) removeJobFromQueue(oldTime, job); - this.queue.compute(time, (t, jobsAtNewTime) -> { - if (jobsAtNewTime == null) jobsAtNewTime = new HashSet<>(); - jobsAtNewTime.add(job); - return jobsAtNewTime; - }); + this.queue.computeIfAbsent(time, $ -> new HashSet<>()).add(job); } public void unschedule(final JobRef job) { From 6fb2e12f91af32d8d67c3d4857fdc742ff0380ec Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 13 Feb 2024 11:05:32 -0800 Subject: [PATCH 044/159] Fix filters in Upsert Preset in Commit Merge - Was missing snapshot id and merge request filters --- .../37_commit_merge_filter_presets/down.sql | 169 +++++++++++++++++ .../37_commit_merge_filter_presets/up.sql | 171 ++++++++++++++++++ .../sql/merlin/applied_migrations.sql | 1 + .../merlin/functions/public/commit_merge.sql | 10 +- 4 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/down.sql create mode 100644 deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/up.sql diff --git a/deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/down.sql b/deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/down.sql new file mode 100644 index 0000000000..c090662a90 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/down.sql @@ -0,0 +1,169 @@ +create or replace procedure commit_merge(_request_id integer) + language plpgsql as $$ + declare + validate_noConflicts integer; + plan_id_R integer; + snapshot_id_S integer; +begin + if(select id from merge_request where id = _request_id) is null then + raise exception 'Invalid merge request id %.', _request_id; + end if; + + -- Stop if this merge is not 'in-progress' + if (select status from merge_request where id = _request_id) != 'in-progress' then + raise exception 'Cannot commit a merge request that is not in-progress.'; + end if; + + -- Stop if any conflicts have not been resolved + select * from conflicting_activities + where merge_request_id = _request_id and resolution = 'none' + limit 1 + into validate_noConflicts; + + if(validate_noConflicts is not null) then + raise exception 'There are unresolved conflicts in merge request %. Cannot commit merge.', _request_id; + end if; + + select plan_id_receiving_changes from merge_request mr where mr.id = _request_id into plan_id_R; + select snapshot_id_supplying_changes from merge_request mr where mr.id = _request_id into snapshot_id_S; + + insert into merge_staging_area( + merge_request_id, activity_id, name, tags, source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, type, arguments, metadata, anchor_id, anchored_to_start, change_type) + -- gather delete data from the opposite tables + select _request_id, activity_id, name, metadata.tag_ids_activity_directive(ca.activity_id, ad.plan_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'delete'::activity_change_type + from conflicting_activities ca + join activity_directive ad + on ca.activity_id = ad.id + where ca.resolution = 'supplying' + and ca.merge_request_id = _request_id + and plan_id = plan_id_R + and ca.change_type_supplying = 'delete' + union + select _request_id, activity_id, name, metadata.tag_ids_activity_snapshot(ca.activity_id, psa.snapshot_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'delete'::activity_change_type + from conflicting_activities ca + join plan_snapshot_activities psa + on ca.activity_id = psa.id + where ca.resolution = 'receiving' + and ca.merge_request_id = _request_id + and snapshot_id = snapshot_id_S + and ca.change_type_receiving = 'delete' + union + select _request_id, activity_id, name, metadata.tag_ids_activity_directive(ca.activity_id, ad.plan_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'none'::activity_change_type + from conflicting_activities ca + join activity_directive ad + on ca.activity_id = ad.id + where ca.resolution = 'receiving' + and ca.merge_request_id = _request_id + and plan_id = plan_id_R + and ca.change_type_receiving = 'modify' + union + select _request_id, activity_id, name, metadata.tag_ids_activity_snapshot(ca.activity_id, psa.snapshot_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'modify'::activity_change_type + from conflicting_activities ca + join plan_snapshot_activities psa + on ca.activity_id = psa.id + where ca.resolution = 'supplying' + and ca.merge_request_id = _request_id + and snapshot_id = snapshot_id_S + and ca.change_type_supplying = 'modify'; + + -- Unlock so that updates can be written + update plan + set is_locked = false + where id = plan_id_R; + + -- Update the plan's activities to match merge-staging-area's activities + -- Add + insert into activity_directive( + id, plan_id, name, source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, type, arguments, metadata, anchor_id, anchored_to_start ) + select activity_id, plan_id_R, name, source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, type, arguments, metadata, anchor_id, anchored_to_start + from merge_staging_area + where merge_staging_area.merge_request_id = _request_id + and change_type = 'add'; + + -- Modify + insert into activity_directive( + id, plan_id, "name", source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, "type", arguments, metadata, anchor_id, anchored_to_start ) + select activity_id, plan_id_R, "name", source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, "type", arguments, metadata, anchor_id, anchored_to_start + from merge_staging_area + where merge_staging_area.merge_request_id = _request_id + and change_type = 'modify' + on conflict (id, plan_id) + do update + set name = excluded.name, + source_scheduling_goal_id = excluded.source_scheduling_goal_id, + created_at = excluded.created_at, + created_by = excluded.created_by, + last_modified_by = excluded.last_modified_by, + start_offset = excluded.start_offset, + type = excluded.type, + arguments = excluded.arguments, + metadata = excluded.metadata, + anchor_id = excluded.anchor_id, + anchored_to_start = excluded.anchored_to_start; + + -- Tags + delete from metadata.activity_directive_tags adt + using merge_staging_area msa + where adt.directive_id = msa.activity_id + and adt.plan_id = plan_id_R + and msa.merge_request_id = _request_id + and msa.change_type = 'modify'; + + insert into metadata.activity_directive_tags(plan_id, directive_id, tag_id) + select plan_id_R, activity_id, t.id + from merge_staging_area msa + inner join metadata.tags t -- Inner join because it's specifically inserting into a tags-association table, so if there are no valid tags we do not want a null value for t.id + on t.id = any(msa.tags) + where msa.merge_request_id = _request_id + and (change_type = 'modify' + or change_type = 'add') + on conflict (directive_id, plan_id, tag_id) do nothing; + -- Presets + insert into preset_to_directive(preset_id, activity_id, plan_id) + select pts.preset_id, pts.activity_id, plan_id_R + from merge_staging_area msa, preset_to_snapshot_directive pts + where msa.activity_id = pts.activity_id + and msa.change_type = 'add' + or msa.change_type = 'modify' + on conflict (activity_id, plan_id) + do update + set preset_id = excluded.preset_id; + + -- Delete + delete from activity_directive ad + using merge_staging_area msa + where ad.id = msa.activity_id + and ad.plan_id = plan_id_R + and msa.merge_request_id = _request_id + and msa.change_type = 'delete'; + + -- Clean up + delete from conflicting_activities where merge_request_id = _request_id; + delete from merge_staging_area where merge_staging_area.merge_request_id = _request_id; + + update merge_request + set status = 'accepted' + where id = _request_id; + + -- Attach snapshot history + insert into plan_latest_snapshot(plan_id, snapshot_id) + select plan_id_receiving_changes, snapshot_id_supplying_changes + from merge_request + where id = _request_id; +end +$$; + +call migrations.mark_migration_rolled_back('37'); diff --git a/deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/up.sql b/deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/up.sql new file mode 100644 index 0000000000..fbbe110877 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/37_commit_merge_filter_presets/up.sql @@ -0,0 +1,171 @@ +create or replace procedure commit_merge(_request_id integer) + language plpgsql as $$ + declare + validate_noConflicts integer; + plan_id_R integer; + snapshot_id_S integer; +begin + if(select id from merge_request where id = _request_id) is null then + raise exception 'Invalid merge request id %.', _request_id; + end if; + + -- Stop if this merge is not 'in-progress' + if (select status from merge_request where id = _request_id) != 'in-progress' then + raise exception 'Cannot commit a merge request that is not in-progress.'; + end if; + + -- Stop if any conflicts have not been resolved + select * from conflicting_activities + where merge_request_id = _request_id and resolution = 'none' + limit 1 + into validate_noConflicts; + + if(validate_noConflicts is not null) then + raise exception 'There are unresolved conflicts in merge request %. Cannot commit merge.', _request_id; + end if; + + select plan_id_receiving_changes from merge_request mr where mr.id = _request_id into plan_id_R; + select snapshot_id_supplying_changes from merge_request mr where mr.id = _request_id into snapshot_id_S; + + insert into merge_staging_area( + merge_request_id, activity_id, name, tags, source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, type, arguments, metadata, anchor_id, anchored_to_start, change_type) + -- gather delete data from the opposite tables + select _request_id, activity_id, name, metadata.tag_ids_activity_directive(ca.activity_id, ad.plan_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'delete'::activity_change_type + from conflicting_activities ca + join activity_directive ad + on ca.activity_id = ad.id + where ca.resolution = 'supplying' + and ca.merge_request_id = _request_id + and plan_id = plan_id_R + and ca.change_type_supplying = 'delete' + union + select _request_id, activity_id, name, metadata.tag_ids_activity_snapshot(ca.activity_id, psa.snapshot_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'delete'::activity_change_type + from conflicting_activities ca + join plan_snapshot_activities psa + on ca.activity_id = psa.id + where ca.resolution = 'receiving' + and ca.merge_request_id = _request_id + and snapshot_id = snapshot_id_S + and ca.change_type_receiving = 'delete' + union + select _request_id, activity_id, name, metadata.tag_ids_activity_directive(ca.activity_id, ad.plan_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'none'::activity_change_type + from conflicting_activities ca + join activity_directive ad + on ca.activity_id = ad.id + where ca.resolution = 'receiving' + and ca.merge_request_id = _request_id + and plan_id = plan_id_R + and ca.change_type_receiving = 'modify' + union + select _request_id, activity_id, name, metadata.tag_ids_activity_snapshot(ca.activity_id, psa.snapshot_id), + source_scheduling_goal_id, created_at, created_by, last_modified_by, start_offset, type, arguments, metadata, anchor_id, anchored_to_start, + 'modify'::activity_change_type + from conflicting_activities ca + join plan_snapshot_activities psa + on ca.activity_id = psa.id + where ca.resolution = 'supplying' + and ca.merge_request_id = _request_id + and snapshot_id = snapshot_id_S + and ca.change_type_supplying = 'modify'; + + -- Unlock so that updates can be written + update plan + set is_locked = false + where id = plan_id_R; + + -- Update the plan's activities to match merge-staging-area's activities + -- Add + insert into activity_directive( + id, plan_id, name, source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, type, arguments, metadata, anchor_id, anchored_to_start ) + select activity_id, plan_id_R, name, source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, type, arguments, metadata, anchor_id, anchored_to_start + from merge_staging_area + where merge_staging_area.merge_request_id = _request_id + and change_type = 'add'; + + -- Modify + insert into activity_directive( + id, plan_id, "name", source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, "type", arguments, metadata, anchor_id, anchored_to_start ) + select activity_id, plan_id_R, "name", source_scheduling_goal_id, created_at, created_by, last_modified_by, + start_offset, "type", arguments, metadata, anchor_id, anchored_to_start + from merge_staging_area + where merge_staging_area.merge_request_id = _request_id + and change_type = 'modify' + on conflict (id, plan_id) + do update + set name = excluded.name, + source_scheduling_goal_id = excluded.source_scheduling_goal_id, + created_at = excluded.created_at, + created_by = excluded.created_by, + last_modified_by = excluded.last_modified_by, + start_offset = excluded.start_offset, + type = excluded.type, + arguments = excluded.arguments, + metadata = excluded.metadata, + anchor_id = excluded.anchor_id, + anchored_to_start = excluded.anchored_to_start; + + -- Tags + delete from metadata.activity_directive_tags adt + using merge_staging_area msa + where adt.directive_id = msa.activity_id + and adt.plan_id = plan_id_R + and msa.merge_request_id = _request_id + and msa.change_type = 'modify'; + + insert into metadata.activity_directive_tags(plan_id, directive_id, tag_id) + select plan_id_R, activity_id, t.id + from merge_staging_area msa + inner join metadata.tags t -- Inner join because it's specifically inserting into a tags-association table, so if there are no valid tags we do not want a null value for t.id + on t.id = any(msa.tags) + where msa.merge_request_id = _request_id + and (change_type = 'modify' + or change_type = 'add') + on conflict (directive_id, plan_id, tag_id) do nothing; + -- Presets + insert into preset_to_directive(preset_id, activity_id, plan_id) + select pts.preset_id, pts.activity_id, plan_id_R + from merge_staging_area msa + inner join preset_to_snapshot_directive pts using (activity_id) + where pts.snapshot_id = snapshot_id_S + and msa.merge_request_id = _request_id + and (msa.change_type = 'add' + or msa.change_type = 'modify') + on conflict (activity_id, plan_id) + do update + set preset_id = excluded.preset_id; + + -- Delete + delete from activity_directive ad + using merge_staging_area msa + where ad.id = msa.activity_id + and ad.plan_id = plan_id_R + and msa.merge_request_id = _request_id + and msa.change_type = 'delete'; + + -- Clean up + delete from conflicting_activities where merge_request_id = _request_id; + delete from merge_staging_area where merge_staging_area.merge_request_id = _request_id; + + update merge_request + set status = 'accepted' + where id = _request_id; + + -- Attach snapshot history + insert into plan_latest_snapshot(plan_id, snapshot_id) + select plan_id_receiving_changes, snapshot_id_supplying_changes + from merge_request + where id = _request_id; +end +$$; + +call migrations.mark_migration_applied('37'); diff --git a/merlin-server/sql/merlin/applied_migrations.sql b/merlin-server/sql/merlin/applied_migrations.sql index d3096db0e8..fecaa4df73 100644 --- a/merlin-server/sql/merlin/applied_migrations.sql +++ b/merlin-server/sql/merlin/applied_migrations.sql @@ -39,3 +39,4 @@ call migrations.mark_migration_applied('33'); call migrations.mark_migration_applied('34'); call migrations.mark_migration_applied('35'); call migrations.mark_migration_applied('36'); +call migrations.mark_migration_applied('37'); diff --git a/merlin-server/sql/merlin/functions/public/commit_merge.sql b/merlin-server/sql/merlin/functions/public/commit_merge.sql index d6c99fddf3..b2a73042ef 100644 --- a/merlin-server/sql/merlin/functions/public/commit_merge.sql +++ b/merlin-server/sql/merlin/functions/public/commit_merge.sql @@ -138,10 +138,12 @@ begin -- Presets insert into preset_to_directive(preset_id, activity_id, plan_id) select pts.preset_id, pts.activity_id, plan_id_R - from merge_staging_area msa, preset_to_snapshot_directive pts - where msa.activity_id = pts.activity_id - and msa.change_type = 'add' - or msa.change_type = 'modify' + from merge_staging_area msa + inner join preset_to_snapshot_directive pts using (activity_id) + where pts.snapshot_id = snapshot_id_S + and msa.merge_request_id = _request_id + and (msa.change_type = 'add' + or msa.change_type = 'modify') on conflict (activity_id, plan_id) do update set preset_id = excluded.preset_id; From 514f089a6dc8f6fb7d30a5d2a2452c0305a2724a Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 13 Feb 2024 11:05:50 -0800 Subject: [PATCH 045/159] Update DB Tests --- .../database/MerlinDatabaseTestHelper.java | 11 ++++ .../database/PlanCollaborationTests.java | 59 ++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java index 9635082e44..507f506284 100644 --- a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java +++ b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java @@ -210,6 +210,17 @@ void assignPreset(int presetId, int activityId, int planId, String userSession) } } + void unassignPreset(int presetId, int activityId, int planId) throws SQLException { + try(final var statement = connection.createStatement()){ + statement.execute( + //language=sql + """ + delete from preset_to_directive + where (preset_id, activity_id, plan_id) = (%d, %d, %d); + """.formatted(presetId, activityId, planId)); + } + } + int insertConstraintPlan(int plan_id, String name, String definition, User user) throws SQLException { try(final var statement = connection.createStatement()) { diff --git a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/PlanCollaborationTests.java b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/PlanCollaborationTests.java index 531b2b3e73..4fb9c33754 100644 --- a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/PlanCollaborationTests.java +++ b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/PlanCollaborationTests.java @@ -2983,11 +2983,11 @@ void deleteActivityReanchorDoesNotImpactRelatedPlans() throws SQLException{ @Nested class PresetTests{ private final gov.nasa.jpl.aerie.database.PresetTests presetTests = new gov.nasa.jpl.aerie.database.PresetTests(); + { presetTests.setConnection(helper);} // Activities added in branches keep their preset information when merged @Test void presetPersistsWithAdd() throws SQLException{ - presetTests.setConnection(helper); merlinHelper.insertActivityType(missionModelId, "test-activity"); final int planId = merlinHelper.insertPlan(missionModelId); final int branchId = duplicatePlan(planId, "Add Preset Branch"); @@ -3010,7 +3010,6 @@ void presetPersistsWithAdd() throws SQLException{ // The preset set in the supplying activity persists @Test void presetPersistsWithModify() throws SQLException{ - presetTests.setConnection(helper); merlinHelper.insertActivityType(missionModelId, "test-activity"); final int planId = merlinHelper.insertPlan(missionModelId); final int activityId = merlinHelper.insertActivity(planId); @@ -3033,7 +3032,6 @@ void presetPersistsWithModify() throws SQLException{ // If the preset used in a snapshot is deleted during the merge, the activity does not have a preset after the merge. @Test void postMergeNoPresetIfPresetDeleted() throws SQLException{ - presetTests.setConnection(helper); merlinHelper.insertActivityType(missionModelId, "test-activity"); final int planId = merlinHelper.insertPlan(missionModelId); final int branchId = duplicatePlan(planId, "Delete Preset Branch"); @@ -3053,6 +3051,61 @@ void postMergeNoPresetIfPresetDeleted() throws SQLException{ assertNull(presetTests.getPresetAssignedToActivity(activityId, planId)); assertNull(presetTests.getPresetAssignedToActivity(activityId, branchId)); } + + // Presets set in prior snapshots don't affect the merge + @Test + void presetOnlyPullsFromSourceSnapshot() throws SQLException { + merlinHelper.insertActivityType(missionModelId, "test-activity"); + final int presetId = merlinHelper.insertPreset(missionModelId, "Demo Preset", "test-activity"); + + // Create a manual snapshot with a preset + final int planId = merlinHelper.insertPlan(missionModelId); + final int activityId = merlinHelper.insertActivity(planId); + merlinHelper.assignPreset(presetId, activityId, planId, merlinHelper.admin.session()); + createSnapshot(planId); + + // Remove preset from plan before branching + merlinHelper.unassignPreset(presetId, activityId, planId); + final int branchId = duplicatePlan(planId, "Delete Preset Branch"); + updateActivityName("new name", activityId, branchId); + + // Merge + final int mergeRQId = createMergeRequest(planId, branchId); + beginMerge(mergeRQId); + commitMerge(mergeRQId); + + // Assertions + assertNull(presetTests.getPresetAssignedToActivity(activityId, planId)); + assertNull(presetTests.getPresetAssignedToActivity(activityId, branchId)); + } + + @Test + void presetUnaffectedByUnrelatedSnapshot() throws SQLException { + merlinHelper.insertActivityType(missionModelId, "test-activity"); + final int presetId = merlinHelper.insertPreset(missionModelId, "Demo Preset", "test-activity"); + + // Create a snapshot of an unrelated plan with a preset set + final int unrelatedPlanId = merlinHelper.insertPlan(missionModelId); + final int unrelatedActivityId = merlinHelper.insertActivity(unrelatedPlanId); + merlinHelper.assignPreset(presetId, unrelatedActivityId, unrelatedPlanId, merlinHelper.admin.session()); + createSnapshot(unrelatedPlanId); + + // Setup working plan + final int planId = merlinHelper.insertPlan(missionModelId); + final int activityId = merlinHelper.insertActivity(planId); + final int branchId = duplicatePlan(planId, "Delete Preset Branch"); + updateActivityName("new name", activityId, branchId); + + // Merge + final int mergeRQId = createMergeRequest(planId, branchId); + beginMerge(mergeRQId); + commitMerge(mergeRQId); + + // Assertions + assertNull(presetTests.getPresetAssignedToActivity(activityId, planId)); + assertNull(presetTests.getPresetAssignedToActivity(activityId, branchId)); + assertEquals(presetId, presetTests.getPresetAssignedToActivity(unrelatedActivityId, unrelatedPlanId).id()); + } } @Nested From d48a0d7b3b15fa0cc74742f910c248508200bb7e Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Thu, 30 Nov 2023 08:55:21 -0800 Subject: [PATCH 046/159] update dev docs w/ new JDK version --- docs/DEVELOPER.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index 097ecb905a..ae61ea143d 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -33,7 +33,7 @@ Before you can run Aerie you must install and configure the following products o Make sure you update your `JAVA_HOME` environment variable. For example with [Zsh](https://www.zsh.org/) you can set your `.zshrc` to: ```sh - export JAVA_HOME="/Library/Java/JavaVirtualMachines/temurin-19.jdk/Contents/Home" + export JAVA_HOME="/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home" ``` - [PostgreSQL](https://www.postgresql.org) which is used for testing the database. You do not need this normally since Aerie runs Postgres in a Docker container for development, and you only need it for the [psql](https://www.postgresql.org/docs/current/app-psql.html) command-line tool. **Do not run the Postgres service locally** or it will clash with the Aerie Postgres Docker container. If you're on OSX you can use brew: From 6256a45638f3b044e36e3d2a295ddf39a796d018 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Mon, 12 Feb 2024 14:48:51 -0800 Subject: [PATCH 047/159] update to gradle 8.6 --- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 18 ++++++++++++++---- gradlew.bat | 15 +++++++++------ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 36987 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 z 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 VBfu2+*qg&~f-ihLS3eum{s(|${^$Sz diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cb702f0..509c4a29b4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787337..65dcd68d65 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,6 +209,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32c4..93e3f59f13 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 0b6a3c7fd075a0346719412f304ca3c45ee2ca1a Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Tue, 7 Nov 2023 15:00:38 -0800 Subject: [PATCH 048/159] update to jdk 21 --- constraints/build.gradle | 2 +- contrib/build.gradle | 2 +- db-tests/build.gradle | 2 +- e2e-tests/build.gradle | 2 +- examples/banananation/build.gradle | 2 +- examples/config-with-defaults/build.gradle | 2 +- examples/config-without-defaults/build.gradle | 2 +- examples/foo-missionmodel/build.gradle | 2 +- examples/minimal-mission-model/build.gradle | 2 +- examples/streamline-demo/build.gradle | 2 +- merlin-driver/build.gradle | 2 +- merlin-framework-junit/build.gradle | 2 +- merlin-framework-processor/build.gradle | 2 +- merlin-framework/build.gradle | 2 +- merlin-sdk/build.gradle | 2 +- merlin-server/Dockerfile | 2 +- merlin-server/build.gradle | 2 +- merlin-worker/Dockerfile | 2 +- merlin-worker/build.gradle | 2 +- parsing-utilities/build.gradle | 2 +- permissions/build.gradle | 2 +- scheduler-driver/build.gradle | 2 +- scheduler-server/Dockerfile | 2 +- scheduler-server/build.gradle | 2 +- scheduler-worker/Dockerfile | 2 +- scheduler-worker/build.gradle | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/constraints/build.gradle b/constraints/build.gradle index f197a5ca9d..8a219a80a4 100644 --- a/constraints/build.gradle +++ b/constraints/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/contrib/build.gradle b/contrib/build.gradle index 9b895af315..ebb6146fb6 100644 --- a/contrib/build.gradle +++ b/contrib/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } withJavadocJar() diff --git a/db-tests/build.gradle b/db-tests/build.gradle index dfcad31365..a04992a271 100644 --- a/db-tests/build.gradle +++ b/db-tests/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 2541e13618..9d1eb39b29 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/examples/banananation/build.gradle b/examples/banananation/build.gradle index 8fd2f09ec8..402c81ca9e 100644 --- a/examples/banananation/build.gradle +++ b/examples/banananation/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/examples/config-with-defaults/build.gradle b/examples/config-with-defaults/build.gradle index 7425e3309d..2fa0e7e215 100644 --- a/examples/config-with-defaults/build.gradle +++ b/examples/config-with-defaults/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/examples/config-without-defaults/build.gradle b/examples/config-without-defaults/build.gradle index 7425e3309d..2fa0e7e215 100644 --- a/examples/config-without-defaults/build.gradle +++ b/examples/config-without-defaults/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/examples/foo-missionmodel/build.gradle b/examples/foo-missionmodel/build.gradle index 7425e3309d..2fa0e7e215 100644 --- a/examples/foo-missionmodel/build.gradle +++ b/examples/foo-missionmodel/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/examples/minimal-mission-model/build.gradle b/examples/minimal-mission-model/build.gradle index 7425e3309d..2fa0e7e215 100644 --- a/examples/minimal-mission-model/build.gradle +++ b/examples/minimal-mission-model/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/examples/streamline-demo/build.gradle b/examples/streamline-demo/build.gradle index 24ed559c1a..b0679446c5 100644 --- a/examples/streamline-demo/build.gradle +++ b/examples/streamline-demo/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/merlin-driver/build.gradle b/merlin-driver/build.gradle index 4d80d8ee87..76cbca76bb 100644 --- a/merlin-driver/build.gradle +++ b/merlin-driver/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } withJavadocJar() diff --git a/merlin-framework-junit/build.gradle b/merlin-framework-junit/build.gradle index 2a8a142c25..52819c1b97 100644 --- a/merlin-framework-junit/build.gradle +++ b/merlin-framework-junit/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } withJavadocJar() diff --git a/merlin-framework-processor/build.gradle b/merlin-framework-processor/build.gradle index 4413f39136..2ccadfde82 100644 --- a/merlin-framework-processor/build.gradle +++ b/merlin-framework-processor/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/merlin-framework/build.gradle b/merlin-framework/build.gradle index d025f5b35a..a52dde0379 100644 --- a/merlin-framework/build.gradle +++ b/merlin-framework/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } withJavadocJar() diff --git a/merlin-sdk/build.gradle b/merlin-sdk/build.gradle index f1433b7dad..908b26fad7 100644 --- a/merlin-sdk/build.gradle +++ b/merlin-sdk/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } withJavadocJar() diff --git a/merlin-server/Dockerfile b/merlin-server/Dockerfile index cb0aa2652b..f578fe1aef 100644 --- a/merlin-server/Dockerfile +++ b/merlin-server/Dockerfile @@ -4,7 +4,7 @@ COPY build/distributions/*.tar /usr/src/app/server.tar RUN mkdir /usr/src/app/extracted RUN cd /usr/src/app && tar --strip-components 1 -xf server.tar -C extracted -FROM eclipse-temurin:19-jre-jammy +FROM eclipse-temurin:21-jre-jammy ENV NODE_VERSION=18.13.0 ENV NVM_DIR=/usr/src/.nvm diff --git a/merlin-server/build.gradle b/merlin-server/build.gradle index be7eb521c3..5158d10ba2 100644 --- a/merlin-server/build.gradle +++ b/merlin-server/build.gradle @@ -7,7 +7,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/merlin-worker/Dockerfile b/merlin-worker/Dockerfile index b6232f1305..a8b1d7072b 100644 --- a/merlin-worker/Dockerfile +++ b/merlin-worker/Dockerfile @@ -4,7 +4,7 @@ COPY build/distributions/*.tar /usr/src/app/server.tar RUN mkdir /usr/src/app/extracted RUN cd /usr/src/app && tar --strip-components 1 -xf server.tar -C extracted -FROM eclipse-temurin:19-jre-jammy +FROM eclipse-temurin:21-jre-jammy COPY --from=extractor /usr/src/app/extracted /usr/src/app diff --git a/merlin-worker/build.gradle b/merlin-worker/build.gradle index bf65678a2e..0e8824914d 100644 --- a/merlin-worker/build.gradle +++ b/merlin-worker/build.gradle @@ -5,7 +5,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } withJavadocJar() diff --git a/parsing-utilities/build.gradle b/parsing-utilities/build.gradle index 8050e8e8fc..3eaa311b8b 100644 --- a/parsing-utilities/build.gradle +++ b/parsing-utilities/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/permissions/build.gradle b/permissions/build.gradle index 019f5248bb..bc7cb46a1c 100644 --- a/permissions/build.gradle +++ b/permissions/build.gradle @@ -4,7 +4,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/scheduler-driver/build.gradle b/scheduler-driver/build.gradle index c5d5ad7fd1..fef5cca0f9 100644 --- a/scheduler-driver/build.gradle +++ b/scheduler-driver/build.gradle @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/scheduler-server/Dockerfile b/scheduler-server/Dockerfile index 57cfb8d17f..855fac1d8c 100644 --- a/scheduler-server/Dockerfile +++ b/scheduler-server/Dockerfile @@ -4,7 +4,7 @@ COPY build/distributions/*.tar /usr/src/app/server.tar RUN mkdir /usr/src/app/extracted RUN cd /usr/src/app && tar --strip-components 1 -xf server.tar -C extracted -FROM eclipse-temurin:19-jre-jammy +FROM eclipse-temurin:21-jre-jammy COPY --from=extractor /usr/src/app/extracted /usr/src/app diff --git a/scheduler-server/build.gradle b/scheduler-server/build.gradle index 8c2381b95f..d36f28b0cf 100644 --- a/scheduler-server/build.gradle +++ b/scheduler-server/build.gradle @@ -9,7 +9,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/scheduler-worker/Dockerfile b/scheduler-worker/Dockerfile index 2c08ecf773..3ba21b38d6 100644 --- a/scheduler-worker/Dockerfile +++ b/scheduler-worker/Dockerfile @@ -4,7 +4,7 @@ COPY build/distributions/*.tar /usr/src/app/server.tar RUN mkdir /usr/src/app/extracted RUN cd /usr/src/app && tar --strip-components 1 -xf server.tar -C extracted -FROM eclipse-temurin:19-jre-jammy +FROM eclipse-temurin:21-jre-jammy ENV NODE_VERSION=18.13.0 ENV NVM_DIR=/usr/src/.nvm diff --git a/scheduler-worker/build.gradle b/scheduler-worker/build.gradle index 882a6ec2f7..ef32b3ce53 100644 --- a/scheduler-worker/build.gradle +++ b/scheduler-worker/build.gradle @@ -8,7 +8,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(21) } withJavadocJar() From 95080d076aca42a8ace9cd4df57d8b2bf8ffc94e Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Mon, 12 Feb 2024 15:22:07 -0800 Subject: [PATCH 049/159] Remove thread lifecycle tests in lieu of loom --- .../driver/engine/SimulationEngine.java | 24 +------------ .../simulation/ResumableSimulationTest.java | 36 ------------------- 2 files changed, 1 insertion(+), 59 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index 6f77878ec7..efbddb54f2 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -73,29 +73,7 @@ public final class SimulationEngine implements AutoCloseable { private final Map> taskChildren = new HashMap<>(); /** A thread pool that modeled tasks can use to keep track of their state between steps. */ - private final ExecutorService executor = getLoomOrFallback(); - - private static ExecutorService getLoomOrFallback() { - // Try to use Loom's lightweight virtual threads, if possible. Otherwise, just use a thread pool. - // This approach is inspired by that of Javalin 5. - // https://github.com/javalin/javalin/blob/97e9e23ebe8f57aa353bc7a45feb560ad61e50a0/javalin/src/main/java/io/javalin/util/ConcurrencyUtil.kt#L48-L51 - try { - // Use reflection to avoid needing `--enable-preview` at compile-time. - // If the runtime JVM is run with `--enable-preview`, this should succeed. - return (ExecutorService) Executors.class.getMethod("newVirtualThreadPerTaskExecutor").invoke(null); - } catch (final ReflectiveOperationException ex) { - return Executors.newCachedThreadPool($ -> { - final var t = new Thread($); - // TODO: Make threads non-daemons. - // We're marking these as daemons right now solely to ensure that the JVM shuts down cleanly in lieu of - // proper model lifecycle management. - // In fact, daemon threads can mask bad memory leaks: a hanging thread is almost indistinguishable - // from a dead thread. - t.setDaemon(true); - return t; - }); - } - } + private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); /** Schedule a new task to be performed at the given time. */ public TaskId scheduleTask(final Duration startTime, final TaskFactory state) { diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java index 0f2bd23508..4f40990a76 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java @@ -2,7 +2,6 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; import gov.nasa.jpl.aerie.scheduler.SimulationUtility; @@ -12,7 +11,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Map; -import java.util.concurrent.ThreadPoolExecutor; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -93,40 +91,6 @@ public void testStopsAtEndOfPlanningHorizon() throws SchedulingInterruptedExcept assert(resumableSimulationDriver.getSimulationResults(Instant.now()).unfinishedActivities.size() == 1); } - @Test - public void testThreadsReleased() throws SchedulingInterruptedException { - final var activity = new TestSimulatedActivity( - Duration.of(0, SECONDS), - new SerializedActivity("BasicActivity", Map.of()), - new ActivityDirectiveId(1)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - resumableSimulationDriver = new ResumableSimulationDriver<>(fooMissionModel, tenHours, ()-> false); - try (final var executor = unsafeGetExecutor(resumableSimulationDriver)) { - for (var i = 0; i < 20000; i++) { - resumableSimulationDriver.initSimulation(); - resumableSimulationDriver.clearActivitiesInserted(); - resumableSimulationDriver.simulateActivity(activity.start, activity.activity, null, true, activity.id); - assertTrue( - executor.getActiveCount() < 100, - "Threads are not being cleaned up properly - this test shouldn't need more than 2 threads, but it used at least 100"); - } - } - } - - private static ThreadPoolExecutor unsafeGetExecutor(final ResumableSimulationDriver driver) { - try { - final var engineField = ResumableSimulationDriver.class.getDeclaredField("engine"); - engineField.setAccessible(true); - - final var executorField = SimulationEngine.class.getDeclaredField("executor"); - executorField.setAccessible(true); - - return (ThreadPoolExecutor) executorField.get(engineField.get(driver)); - } catch (final ReflectiveOperationException ex) { - throw new RuntimeException(ex); - } - } - private ArrayList getActivities(){ final var acts = new ArrayList(); var act1 = new TestSimulatedActivity( From 11bd508f9615821c2d5407ce488189ed92f64710 Mon Sep 17 00:00:00 2001 From: joswig Date: Sat, 17 Feb 2024 00:57:07 +0000 Subject: [PATCH 050/159] Release v2.4.0 --- gradle.properties | 2 +- sequencing-server/package-lock.json | 4 ++-- sequencing-server/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 67af9236e3..b182f7909b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ publishing.version= # Override for releases # Change the version number here -version.number=2.3.0 +version.number=2.4.0 # If you are publishing a release *manually* (i.e. not through github actions), # override this on the command line with `./gradlew publish -Pversion.isRelease=true`. diff --git a/sequencing-server/package-lock.json b/sequencing-server/package-lock.json index dcbd67729f..2ecde0deb8 100644 --- a/sequencing-server/package-lock.json +++ b/sequencing-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "sequencing-server", - "version": "2.3.0", + "version": "2.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sequencing-server", - "version": "2.3.0", + "version": "2.4.0", "license": "MIT", "dependencies": { "@js-temporal/polyfill": "~0.4.3", diff --git a/sequencing-server/package.json b/sequencing-server/package.json index 9eb4106c44..75ff7d7534 100644 --- a/sequencing-server/package.json +++ b/sequencing-server/package.json @@ -1,6 +1,6 @@ { "name": "sequencing-server", - "version": "2.3.0", + "version": "2.4.0", "description": "Aerie sequencing server", "type": "module", "license": "MIT", From 161443b7e8bc960bf5d340bf7473df2e44329206 Mon Sep 17 00:00:00 2001 From: Cody Hansen Date: Tue, 20 Feb 2024 10:01:47 -1000 Subject: [PATCH 051/159] Fixed an issue where extraneous expansions were being saved in an expanded sequence --- .../src/routes/command-expansion.ts | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/sequencing-server/src/routes/command-expansion.ts b/sequencing-server/src/routes/command-expansion.ts index 14ebef9b4d..f5073ac0d8 100644 --- a/sequencing-server/src/routes/command-expansion.ts +++ b/sequencing-server/src/routes/command-expansion.ts @@ -315,7 +315,7 @@ commandExpansionRouter.post('/expand-all-activity-instances', async (req, res, n // Get all the sequence IDs that are assigned to simulated activities. const seqToSimulatedActivity = await db.query( ` - select seq_id + select seq_id, simulated_activity_id from sequence_to_simulated_activity where sequence_to_simulated_activity.simulated_activity_id in (${pgFormat( '%L', @@ -340,6 +340,17 @@ commandExpansionRouter.post('/expand-all-activity-instances', async (req, res, n [simulationDatasetId], ); + // Map seqIds to simulated activity ids so we only save expanded seqs for selected activites. + const seqIdToSimActivityId: Record> = {}; + + for (const row of seqToSimulatedActivity.rows) { + if (seqIdToSimActivityId[row.seq_id] === undefined) { + seqIdToSimActivityId[row.seq_id] = new Set(); + } + + seqIdToSimActivityId[row.seq_id]!.add(row.simulated_activity_id); + } + // If the user has created a sequence, we can try to save the expanded sequences when an expansion runs. for (const seqRow of seqRows.rows) { const seqId = seqRow.seq_id; @@ -360,10 +371,12 @@ commandExpansionRouter.post('/expand-all-activity-instances', async (req, res, n return next(); } - const sortedActivityInstances = ( + let sortedActivityInstances = ( simulatedActivities as Exclude<(typeof simulatedActivities)[number], Error>[] ).sort((a, b) => Temporal.Duration.compare(a.startOffset, b.startOffset)); + sortedActivityInstances = sortedActivityInstances.filter(ai => seqIdToSimActivityId[seqId]?.has(ai.id)); + const sortedSimulatedActivitiesWithCommands = sortedActivityInstances.map(ai => { const row = expandedActivityInstances.find(row => row.id === ai.id); @@ -380,18 +393,19 @@ commandExpansionRouter.post('/expand-all-activity-instances', async (req, res, n return { ...ai, - commands: row.commands?.map(c => { - switch (c.type) { - case 'command': - return CommandStem.fromSeqJson(c); - case 'load': - return LoadStep.fromSeqJson(c); - case 'activate': - return ActivateStep.fromSeqJson(c); - default: - throw new Error(`Unknown command type: ${c.type}`); - } - }) ?? null, + commands: + row.commands?.map(c => { + switch (c.type) { + case 'command': + return CommandStem.fromSeqJson(c); + case 'load': + return LoadStep.fromSeqJson(c); + case 'activate': + return ActivateStep.fromSeqJson(c); + default: + throw new Error(`Unknown command type: ${c.type}`); + } + }) ?? null, errors: errors as { message: string; stack: string; location: { line: number; column: number } }[], }; }); From 3dfccf53855933e2330874280d4bca738c5e9358 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Tue, 20 Feb 2024 07:35:41 -0800 Subject: [PATCH 052/159] update java version to 21 in CI --- .github/workflows/create_jnispice.yml | 2 +- .github/workflows/deploy-to-gh-pages.yml | 2 +- .github/workflows/load-test.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/security-scan.yml | 2 +- .github/workflows/test.yml | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/create_jnispice.yml b/.github/workflows/create_jnispice.yml index fd77d86be8..1913836653 100644 --- a/.github/workflows/create_jnispice.yml +++ b/.github/workflows/create_jnispice.yml @@ -77,7 +77,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: "temurin" - java-version: "19" + java-version: "21" - name: Download and Unpack JNISpice TAR run: | curl https://naif.jpl.nasa.gov/pub/naif/misc/JNISpice/PC_Linux_GCC_Java1.8_64bit/packages/JNISpice.tar.Z -o JNISpice.tar.Z diff --git a/.github/workflows/deploy-to-gh-pages.yml b/.github/workflows/deploy-to-gh-pages.yml index 0ed6e9b49a..9bc3eee449 100644 --- a/.github/workflows/deploy-to-gh-pages.yml +++ b/.github/workflows/deploy-to-gh-pages.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: 19 + java-version: "21" - uses: gradle/gradle-build-action@v2 - name: Create Pages Build Directories run: mkdir -p build/javadoc/examples diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index e38a6987f7..16ee8ea6af 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: "temurin" - java-version: "19" + java-version: "21" - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - name: Setup Gradle diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1377a3e711..5de7c1588a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -68,7 +68,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: "temurin" - java-version: "19" + java-version: "21" - name: Init Gradle cache uses: gradle/gradle-build-action@v2 diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index f9f4f66441..4d565ff351 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -39,7 +39,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: "temurin" - java-version: "19" + java-version: "21" - name: Build run: | ./gradlew testClasses diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc0b0e9efd..6ed29c790b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: "temurin" - java-version: "19" + java-version: "21" - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 - name: Setup Gradle @@ -62,7 +62,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: "temurin" - java-version: "19" + java-version: "21" - name: Setup Postgres Client (psql) run: | sudo apt-get update From 4d12705cd6a29ab7208fd9b4ef61bb9cadb0da19 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 21 Feb 2024 10:13:24 -0800 Subject: [PATCH 053/159] update pgjdbc to fix SQL injection vuln see: https://github.com/advisories/GHSA-xfg6-62px-cxc2 --- db-tests/build.gradle | 2 +- merlin-server/build.gradle | 2 +- merlin-worker/build.gradle | 2 +- scheduler-server/build.gradle | 2 +- scheduler-worker/build.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/db-tests/build.gradle b/db-tests/build.gradle index a04992a271..24d3960f58 100644 --- a/db-tests/build.gradle +++ b/db-tests/build.gradle @@ -21,7 +21,7 @@ task e2eTest(type: Test) { } dependencies { - testImplementation 'org.postgresql:postgresql:42.6.0' + testImplementation 'org.postgresql:postgresql:42.6.1' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' testImplementation 'com.zaxxer:HikariCP:5.0.1' diff --git a/merlin-server/build.gradle b/merlin-server/build.gradle index 5158d10ba2..7e4557f5b6 100644 --- a/merlin-server/build.gradle +++ b/merlin-server/build.gradle @@ -84,7 +84,7 @@ dependencies { implementation 'org.slf4j:slf4j-simple:2.0.7' implementation 'org.glassfish:javax.json:1.1.4' - implementation 'org.postgresql:postgresql:42.6.0' + implementation 'org.postgresql:postgresql:42.6.1' implementation 'com.zaxxer:HikariCP:5.0.1' testImplementation project(':examples:foo-missionmodel') diff --git a/merlin-worker/build.gradle b/merlin-worker/build.gradle index 0e8824914d..eed0fb9b0f 100644 --- a/merlin-worker/build.gradle +++ b/merlin-worker/build.gradle @@ -30,6 +30,6 @@ dependencies { implementation 'io.javalin:javalin:5.6.3' implementation 'org.slf4j:slf4j-simple:2.0.7' - implementation 'org.postgresql:postgresql:42.6.0' + implementation 'org.postgresql:postgresql:42.6.1' implementation 'com.zaxxer:HikariCP:5.0.1' } diff --git a/scheduler-server/build.gradle b/scheduler-server/build.gradle index d36f28b0cf..4fa0895105 100644 --- a/scheduler-server/build.gradle +++ b/scheduler-server/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation 'io.javalin:javalin:5.6.3' implementation 'org.eclipse:yasson:3.0.3' - implementation 'org.postgresql:postgresql:42.6.0' + implementation 'org.postgresql:postgresql:42.6.1' implementation 'com.zaxxer:HikariCP:5.0.1' testImplementation project(':examples:foo-missionmodel') diff --git a/scheduler-worker/build.gradle b/scheduler-worker/build.gradle index ef32b3ce53..241c01bd2b 100644 --- a/scheduler-worker/build.gradle +++ b/scheduler-worker/build.gradle @@ -116,7 +116,7 @@ dependencies { implementation 'io.javalin:javalin:5.6.3' implementation 'org.slf4j:slf4j-simple:2.0.7' implementation 'org.eclipse:yasson:3.0.3' - implementation 'org.postgresql:postgresql:42.6.0' + implementation 'org.postgresql:postgresql:42.6.1' implementation 'com.zaxxer:HikariCP:5.0.1' testImplementation project(':examples:foo-missionmodel') From b642b93ee1af859ade2ff1d89f84b1c081d339fa Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Fri, 16 Feb 2024 16:50:09 -0800 Subject: [PATCH 054/159] Update GH actions to use Node20 versions --- .github/workflows/cloc.yml | 2 +- .github/workflows/create_jnispice.yml | 10 ++--- .github/workflows/deploy-to-gh-pages.yml | 10 ++--- .github/workflows/load-test.yml | 10 ++--- .github/workflows/pgcmp.yml | 52 ++++++++++++------------ .github/workflows/publish.yml | 30 +++++++------- .github/workflows/security-scan.yml | 10 ++--- .github/workflows/test.yml | 26 ++++++------ 8 files changed, 75 insertions(+), 75 deletions(-) diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml index cb2e7e04e2..8f7a7596ab 100644 --- a/.github/workflows/cloc.yml +++ b/.github/workflows/cloc.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install cloc run: | sudo apt-get update diff --git a/.github/workflows/create_jnispice.yml b/.github/workflows/create_jnispice.yml index 1913836653..b9dc9ab3c6 100644 --- a/.github/workflows/create_jnispice.yml +++ b/.github/workflows/create_jnispice.yml @@ -20,7 +20,7 @@ jobs: 7z x JNISpice.zip echo "JNISpice unpacked" - name: Upload DLL - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Windows Spice path: JNISpice/lib/JNISpice.dll @@ -39,7 +39,7 @@ jobs: run: mv libJNISpice.so libJNISpice_Intel.so working-directory: JNISpice/lib - name: Upload .so - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: x86 Linux Spice path: JNISpice/lib/libJNISpice_Intel.so @@ -64,7 +64,7 @@ jobs: working-directory: JNISpice/src/JNISpice shell: csh {0} - name: Upload Intel Mac .jnilib - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: x86 Mac Spice path: JNISpice/lib/libJNISpice_Intel.jnilib @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" @@ -104,7 +104,7 @@ jobs: working-directory: JNISpice/src/JNISpice - name: Upload JAR if: success() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: JNISpice Jar path: JNISpice/src/JNISpice/JNISpice-*.jar diff --git a/.github/workflows/deploy-to-gh-pages.yml b/.github/workflows/deploy-to-gh-pages.yml index 9bc3eee449..5af4fbd129 100644 --- a/.github/workflows/deploy-to-gh-pages.yml +++ b/.github/workflows/deploy-to-gh-pages.yml @@ -15,12 +15,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: "21" - - uses: gradle/gradle-build-action@v2 + - uses: gradle/actions/setup-gradle@v3 - name: Create Pages Build Directories run: mkdir -p build/javadoc/examples - name: Build EDSL API Docs @@ -51,7 +51,7 @@ jobs: cp -a ./scheduler-server/build/docs/javadoc ./build/javadoc/scheduler-server cp -a ./scheduler-worker/build/docs/javadoc ./build/javadoc/scheduler-worker - name: Upload Artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: build/ @@ -64,4 +64,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index 16ee8ea6af..d6ac773bcc 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -17,16 +17,16 @@ jobs: environment: load-test steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 - name: Assemble run: ./gradlew assemble --parallel - name: Start Services @@ -43,7 +43,7 @@ jobs: ./load-test.sh - name: Upload Load Test Results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Load Test Results path: "**/load-tests/load-report.*" diff --git a/.github/workflows/pgcmp.yml b/.github/workflows/pgcmp.yml index 731b8009b0..1d2cf88740 100644 --- a/.github/workflows/pgcmp.yml +++ b/.github/workflows/pgcmp.yml @@ -28,16 +28,16 @@ jobs: environment: e2e-test steps: - name: Checkout v1.0.1 - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: "v1.0.1" - name: Clone PGCMP - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: cbbrowne/pgcmp path: pgcmp - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Setup Postgres Client (psql) @@ -47,9 +47,9 @@ jobs: - name: Setup Hasura CLI run: sudo curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 - name: Distribute SQL and Assemble Java - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: arguments: distributeSQL - name: Start Postgres @@ -79,13 +79,13 @@ jobs: PGURI=postgres://"${AERIE_USERNAME}":"${AERIE_PASSWORD}"@localhost:5432/aerie_ui PGCMPOUTPUT=./pgdumpv1_0_1/AerieUIV1_0_1 PGCLABEL=AerieUIV1_0_1 PGBINDIR=/usr/bin ./pgcmp/pgcmp-dump shell: bash - name: Share Database Dump - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: v1_0_1-db-dump path: pgdumpv1_0_1 retention-days: 1 - name: Checkout Latest - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Restart Hasura run: | docker compose down @@ -116,7 +116,7 @@ jobs: AERIE_USERNAME: "${{secrets.AERIE_USERNAME}}" AERIE_PASSWORD: "${{secrets.AERIE_PASSWORD}}" - name: Clone PGCMP - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: cbbrowne/pgcmp path: pgcmp @@ -132,7 +132,7 @@ jobs: PGURI=postgres://"${AERIE_USERNAME}":"${AERIE_PASSWORD}"@localhost:5432/aerie_ui PGCMPOUTPUT=./pgdumpmigrated/AerieUIMigrated PGCLABEL=AerieUIMigrated PGBINDIR=/usr/bin ./pgcmp/pgcmp-dump shell: bash - name: Share Database Dump - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migrated-db-dump path: pgdumpmigrated @@ -153,19 +153,19 @@ jobs: environment: e2e-test steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Postgres Client (psql) run: | sudo apt-get update sudo apt-get install --yes postgresql-client - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 - name: Distribute SQL - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: arguments: distributeSQL - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Setup Hasura CLI @@ -186,7 +186,7 @@ jobs: run: sleep 60s shell: bash - name: Clone PGCMP - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: cbbrowne/pgcmp path: pgcmp @@ -202,7 +202,7 @@ jobs: PGURI=postgres://"${AERIE_USERNAME}":"${AERIE_PASSWORD}"@localhost:5432/aerie_ui PGCMPOUTPUT=./pgdumpraw/AerieUIRaw PGCLABEL=AerieUIRaw PGBINDIR=/usr/bin ./pgcmp/pgcmp-dump shell: bash - name: Share Database Dump - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: raw-sql-db-dump path: pgdumpraw @@ -232,7 +232,7 @@ jobs: PGURI=postgres://"${AERIE_USERNAME}":"${AERIE_PASSWORD}"@localhost:5432/aerie_ui PGCMPOUTPUT=./pgdumpmigrateddown/AerieUIMigratedDown PGCLABEL=AerieUIMigratedDown PGBINDIR=/usr/bin ./pgcmp/pgcmp-dump shell: bash - name: Share Database Dump - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migrated-down-db-dump path: pgdumpmigrateddown @@ -252,9 +252,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Clone PGCMP - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: cbbrowne/pgcmp path: pgcmp @@ -268,11 +268,11 @@ jobs: run: sleep 5s shell: bash - name: Download Shared Dumps - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: migrated-db-dump path: pgcmp/pgdumpmigrated - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: raw-sql-db-dump path: pgcmp/pgdumpraw @@ -285,7 +285,7 @@ jobs: shell: bash - name: Upload Invalid if: ${{ failure() && steps.dbcmp.conclusion == 'failure' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pgcmpresultsup path: "**/results/" @@ -305,9 +305,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Clone PGCMP - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: cbbrowne/pgcmp path: pgcmp @@ -321,11 +321,11 @@ jobs: run: sleep 5s shell: bash - name: Download Shared Dumps - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: v1_0_1-db-dump path: pgcmp/pgdumpv1_0_1 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: migrated-down-db-dump path: pgcmp/pgdumpmigrateddown @@ -340,7 +340,7 @@ jobs: shell: bash - name: Upload Invalid if: ${{ failure() && steps.dbcmp.conclusion == 'failure' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pgcmpresultsdown path: "**/results/" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5de7c1588a..1bd3d008db 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,11 +20,11 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/wrapper-validation-action@v2 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/actions/setup-gradle@v3 with: generate-job-summary: false @@ -63,28 +63,28 @@ jobs: file: docker/Dockerfile.postgres name: ${{ matrix.components.image }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" - name: Init Gradle cache - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: generate-job-summary: false - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: platforms: linux/amd64,linux/arm64 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to ${{ env.REGISTRY }} - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -92,7 +92,7 @@ jobs: - name: Extract metadata (tags, labels) for ${{ matrix.components.image }} id: metadata-step - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ matrix.components.image }} @@ -109,7 +109,7 @@ jobs: fi - name: Build and push ${{ matrix.components.image }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: ${{ matrix.components.context }} file: ${{ matrix.components.file }} @@ -148,7 +148,7 @@ jobs: - name: Upload ${{ matrix.image }} scan results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Vuln Scan Results path: "${{ matrix.image }}-results.html" @@ -161,10 +161,10 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Gradle Cache - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: generate-job-summary: false @@ -177,7 +177,7 @@ jobs: run: ./gradlew archiveDeployment - name: Publish deployment - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Deployment path: deployment.tar diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 4d565ff351..5bc1b9fa8f 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -28,15 +28,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: +security-extended tools: latest - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" @@ -44,7 +44,7 @@ jobs: run: | ./gradlew testClasses - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 - name: Gradle Dependency Submission uses: mikepenz/gradle-dependency-submission@v0.9.0 with: @@ -89,7 +89,7 @@ jobs: echo "RESULTS_DIR=$results_dir" >> $GITHUB_ENV - name: Upload Security Scan Results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Security Scan Results path: ${{ env.RESULTS_DIR }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ed29c790b..54bd4e170d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,29 +25,29 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 - name: Assemble run: ./gradlew assemble --parallel - name: Run Unit Tests run: ./gradlew test --parallel - name: Upload Test Results as XML if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Test Results path: "**/build/test-results/test" - name: Upload Test Results as HTML if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Test Results path: "**/build/reports/tests/test" @@ -57,9 +57,9 @@ jobs: environment: e2e-test steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" @@ -68,9 +68,9 @@ jobs: sudo apt-get update sudo apt-get install --yes postgresql-client - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 - name: Assemble run: ./gradlew assemble --parallel - name: Start Services @@ -85,19 +85,19 @@ jobs: run: ./gradlew e2eTest - name: Upload E2E Test Results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Test Results path: "**/e2e-tests/build/reports/tests/e2eTest" - name: Upload DB Test Results as HTML if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Test Results path: "**/db-tests/build/reports/tests/e2eTest" - name: Upload Sequencing Server Test Results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Test Results path: "**/sequencing-server/test-report.*" From 55a35b03326efef7bb3f6ad7d44cb1a89452b134 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 20 Feb 2024 08:47:25 -0800 Subject: [PATCH 055/159] Resolve upload-artifact breaking changes --- .github/workflows/publish.yml | 2 +- .github/workflows/security-scan.yml | 2 +- .github/workflows/test.yml | 33 +++++++++-------------------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1bd3d008db..9451b1e68c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -150,7 +150,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: Vuln Scan Results + name: Vuln Scan Results - ${{ matrix.image }} path: "${{ matrix.image }}-results.html" publish: diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 5bc1b9fa8f..f5e9f43e66 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -91,5 +91,5 @@ jobs: - name: Upload Security Scan Results uses: actions/upload-artifact@v4 with: - name: Security Scan Results + name: Security Scan Results - ${{ matrix.language }} path: ${{ env.RESULTS_DIR }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54bd4e170d..277f96079b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,18 +39,14 @@ jobs: run: ./gradlew assemble --parallel - name: Run Unit Tests run: ./gradlew test --parallel - - name: Upload Test Results as XML + - name: Upload Test Results as XML and HTML if: always() uses: actions/upload-artifact@v4 with: - name: Test Results - path: "**/build/test-results/test" - - name: Upload Test Results as HTML - if: always() - uses: actions/upload-artifact@v4 - with: - name: Test Results - path: "**/build/reports/tests/test" + name: Unit Test Results + path: | + **/build/test-results/test + **/build/reports/tests/test e2e-test: runs-on: ubuntu-latest @@ -87,20 +83,11 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: Test Results - path: "**/e2e-tests/build/reports/tests/e2eTest" - - name: Upload DB Test Results as HTML - if: always() - uses: actions/upload-artifact@v4 - with: - name: Test Results - path: "**/db-tests/build/reports/tests/e2eTest" - - name: Upload Sequencing Server Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: Test Results - path: "**/sequencing-server/test-report.*" + name: E2E Test Results + path: | + **/e2e-tests/build/reports/tests/e2eTest + **/db-tests/build/reports/tests/e2eTest + **/sequencing-server/test-report.* - name: Print Logs for Services if: always() run: docker compose -f ./e2e-tests/docker-compose-test.yml logs -t From 676238999d3c0b4435255a0a6d2dc36cf6c728cb Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 21 Feb 2024 11:14:44 -0800 Subject: [PATCH 056/159] Replace `Gradle Dependency Submission` with `Setup Gradle` mikepenz/gradle-dependency-submission has been archived in favor of gradle/actions/setup-gradle --- .github/workflows/security-scan.yml | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index f5e9f43e66..f2034d59d1 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -45,33 +45,8 @@ jobs: ./gradlew testClasses - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 - - name: Gradle Dependency Submission - uses: mikepenz/gradle-dependency-submission@v0.9.0 - with: - gradle-build-module: |- - :merlin-sdk - :merlin-driver - :merlin-framework - :merlin-framework-junit - :merlin-framework-processor - :contrib - :parsing-utilities - :permissions - :merlin-server - :merlin-worker - :scheduler-server - :scheduler-worker - :sequencing-server - :constraints - :scheduler-driver - :db-tests - :e2e-tests - :examples:banananation - :examples:foo-missionmodel - :examples:config-with-defaults - :examples:config-without-defaults - :examples:minimal-mission-model - sub-module-mode: "COMBINED" + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: NASA Scrub run: | python3 -m pip install nasa-scrub From a83b1efc530d9add2b684e35ea6265623085f01b Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 21 Feb 2024 12:25:18 -0800 Subject: [PATCH 057/159] Tweak db-lockup-test numbers for GH Workflow --- load-tests/src/db-lockup-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/load-tests/src/db-lockup-test.ts b/load-tests/src/db-lockup-test.ts index 9e05197040..7a59c152d1 100644 --- a/load-tests/src/db-lockup-test.ts +++ b/load-tests/src/db-lockup-test.ts @@ -6,7 +6,7 @@ import {ActivityInsertInput} from "./types/activity"; export const options = { // A number specifying the number of VUs to run concurrently. - vus: 50, + vus: 15, // set to at least 50 when running locally // A string specifying the total duration of the test run. duration: '10s', From aa18df34b2b547d708ff0b87ec95f8bf98e000e4 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Tue, 6 Feb 2024 13:45:22 -1000 Subject: [PATCH 058/159] Expose Work Options for the Sequencing Server * The user can specify the number of workers and how much heap size each worker can have. --- sequencing-server/src/env.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sequencing-server/src/env.ts b/sequencing-server/src/env.ts index 2701912377..ee8564799f 100644 --- a/sequencing-server/src/env.ts +++ b/sequencing-server/src/env.ts @@ -10,6 +10,8 @@ export type Env = { POSTGRES_PORT: string; POSTGRES_USER: string; STORAGE: string; + SEQUENCING_WORKER_NUM: string; + SEQUENCING_MAX_WORKER_HEAP_MB: string; }; export const defaultEnv: Env = { @@ -24,6 +26,8 @@ export const defaultEnv: Env = { POSTGRES_PORT: '5432', POSTGRES_USER: '', STORAGE: 'sequencing_file_store', + SEQUENCING_WORKER_NUM: '8', + SEQUENCING_MAX_WORKER_HEAP_MB: '1000', }; export function getEnv(): Env { @@ -40,6 +44,9 @@ export function getEnv(): Env { const POSTGRES_PORT = env['SEQUENCING_DB_PORT'] ?? defaultEnv.POSTGRES_PORT; const POSTGRES_USER = env['SEQUENCING_DB_USER'] ?? defaultEnv.POSTGRES_USER; const STORAGE = env['SEQUENCING_LOCAL_STORE'] ?? defaultEnv.STORAGE; + const SEQUENCING_WORKER_NUM = env['SEQUENCING_WORKER_NUM'] ?? defaultEnv.SEQUENCING_WORKER_NUM; + const SEQUENCING_MAX_WORKER_HEAP_MB = + env['SEQUENCING_MAX_WORKER_HEAP_MB'] ?? defaultEnv.SEQUENCING_MAX_WORKER_HEAP_MB; return { HASURA_GRAPHQL_ADMIN_SECRET, LOG_FILE, @@ -52,5 +59,7 @@ export function getEnv(): Env { POSTGRES_PORT, POSTGRES_USER, STORAGE, + SEQUENCING_WORKER_NUM, + SEQUENCING_MAX_WORKER_HEAP_MB, }; } From aaf22522c2b38ac6cad2ff95f58f8161631e5446 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Tue, 6 Feb 2024 13:46:02 -1000 Subject: [PATCH 059/159] Added the Worker Options to the docker-compose.yml --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index b0124d0762..aa46392887 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -93,6 +93,8 @@ services: SEQUENCING_DB_SERVER: postgres SEQUENCING_DB_USER: "${AERIE_USERNAME}" SEQUENCING_LOCAL_STORE: /usr/src/app/sequencing_file_store + SEQUENCING_WORKER_NUM: 8 + SEQUENCING_MAX_WORKER_HEAP_MB: 1000 image: aerie_sequencing ports: ["27184:27184"] restart: always From 3f9609c59bd1a972dbaa46c332a66a4923180123 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Tue, 6 Feb 2024 13:49:01 -1000 Subject: [PATCH 060/159] Enhance worker management with Piscina worker options Implement worker options to tailor worker behavior to an application's needs, optimizing resource utilization. --- sequencing-server/src/app.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sequencing-server/src/app.ts b/sequencing-server/src/app.ts index 3c1f422a58..dd1370f406 100644 --- a/sequencing-server/src/app.ts +++ b/sequencing-server/src/app.ts @@ -24,6 +24,9 @@ import getLogger from './utils/logger.js'; import { commandExpansionRouter } from './routes/command-expansion.js'; import { seqjsonRouter } from './routes/seqjson.js'; import { getHasuraSession, canUserPerformAction, ENDPOINTS_WHITELIST } from './utils/hasura.js'; +import type { Result } from '@nasa-jpl/aerie-ts-user-code-runner/build/utils/monads'; +import type { CacheItem, UserCodeError } from '@nasa-jpl/aerie-ts-user-code-runner'; +import { PromiseThrottler } from './utils/PromiseThrottler.js'; const logger = getLogger('app'); @@ -37,7 +40,14 @@ app.use(bodyParser.json({ limit: '100mb' })); DbExpansion.init(); export const db = DbExpansion.getDb(); -export const piscina = new Piscina({ filename: new URL('worker.js', import.meta.url).pathname }); +export const piscina = new Piscina({ + filename: new URL('worker.js', import.meta.url).pathname, + minThreads: parseInt(getEnv().SEQUENCING_WORKER_NUM), + resourceLimits: { maxOldGenerationSizeMb: parseInt(getEnv().SEQUENCING_MAX_WORKER_HEAP_MB) }, +}); +export const promiseThrottler = new PromiseThrottler(parseInt(getEnv().SEQUENCING_WORKER_NUM) - 2); +export const typeCheckingCache = new Map[]>>>(); + const temporalPolyfillTypes = fs.readFileSync(new URL('TemporalPolyfillTypes.ts', import.meta.url).pathname, 'utf-8'); export type Context = { @@ -234,4 +244,7 @@ app.use((err: any, _: Request, res: Response, next: NextFunction) => { app.listen(PORT, () => { logger.info(`connected to port ${PORT}`); + logger.info(`Worker pool initialized: + Total workers started: ${piscina.threads.length}, + Heap Size per Worker: ${getEnv().SEQUENCING_MAX_WORKER_HEAP_MB} MB`); }); From f281d43c0709dc514e8aec3a923017590fc7fff7 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Wed, 14 Feb 2024 12:19:04 -1000 Subject: [PATCH 061/159] Added Dylan's PromisThrottler. Works better than the original chunking I implemented --- .../src/utils/PromiseThrottler.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 sequencing-server/src/utils/PromiseThrottler.ts diff --git a/sequencing-server/src/utils/PromiseThrottler.ts b/sequencing-server/src/utils/PromiseThrottler.ts new file mode 100644 index 0000000000..917004c0fa --- /dev/null +++ b/sequencing-server/src/utils/PromiseThrottler.ts @@ -0,0 +1,22 @@ +export class PromiseThrottler { + private runningPromises: Promise[] = []; + private promiseLimit: number; + + public constructor(promiseLimit: number) { + this.promiseLimit = promiseLimit; + } + + public async run(promiseFactory: () => Promise): Promise { + while (this.runningPromises.length >= this.promiseLimit) { + await Promise.race(this.runningPromises); + } + const promise = promiseFactory(); + this.runningPromises.push(promise); + return promise.finally(() => { + const index = this.runningPromises.indexOf(promise); + if (index !== -1) { + this.runningPromises.splice(index, 1); + } + }); + } +} From b991ca434e3e5dd83cafcf41059773f7fb653cb7 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Tue, 6 Feb 2024 13:53:15 -1000 Subject: [PATCH 062/159] Optimize Performance: Cache TS transpiling, enhance worker management Introduce a TypeScript transpiling cache to significantly reduce build times for both expansion sets and sequence expansion. This will be an upfront cost. Implement chunking to prevent overloading the worker queue with excessive jobs. This effectively manages worker resources and prevents potential the consumption of resources and heap size. --- .../src/routes/command-expansion.ts | 143 ++++++++++++------ 1 file changed, 94 insertions(+), 49 deletions(-) diff --git a/sequencing-server/src/routes/command-expansion.ts b/sequencing-server/src/routes/command-expansion.ts index f5073ac0d8..e53e2eb39b 100644 --- a/sequencing-server/src/routes/command-expansion.ts +++ b/sequencing-server/src/routes/command-expansion.ts @@ -1,6 +1,6 @@ -import type { CacheItem, UserCodeError } from '@nasa-jpl/aerie-ts-user-code-runner'; +import type { UserCodeError } from '@nasa-jpl/aerie-ts-user-code-runner'; import pgFormat from 'pg-format'; -import { Context, db, piscina } from './../app.js'; +import { Context, db, piscina, promiseThrottler, typeCheckingCache } from './../app.js'; import { Result } from '@nasa-jpl/aerie-ts-user-code-runner/build/utils/monads.js'; import express from 'express'; import { serializeWithTemporal } from './../utils/temporalSerializers.js'; @@ -13,6 +13,7 @@ import { unwrapPromiseSettledResults } from '../lib/batchLoaders/index.js'; import { defaultSeqBuilder } from '../defaultSeqBuilder.js'; import { ActivateStep, CommandStem, LoadStep } from './../lib/codegen/CommandEDSLPreface.js'; import { getUsername } from '../utils/hasura.js'; +import * as crypto from 'crypto'; const logger = getLogger('app'); @@ -54,17 +55,18 @@ commandExpansionRouter.post('/put-expansion', async (req, res, next) => { activityTypeName, }); const activityTypescript = generateTypescriptForGraphQLActivitySchema(activitySchema); - - const result = Result.fromJSON( - await (piscina.run( - { - expansionLogic, - commandTypes: commandTypes, - activityTypes: activityTypescript, - }, - { name: 'typecheckExpansion' }, - ) as ReturnType), - ); + const result = await promiseThrottler.run(() => { + return ( + piscina.run( + { + commandTypes: commandTypes, + activityTypes: activityTypescript, + activityTypeName: activityTypeName, + }, + { name: 'typecheckExpansion' }, + ) as ReturnType + ).then(Result.fromJSON); + }); res.status(200).json({ id, errors: result.isErr() ? result.unwrapErr() : [] }); return next(); @@ -90,32 +92,65 @@ commandExpansionRouter.post('/put-expansion-set', async (req, res, next) => { if (expansion instanceof Error) { throw new InheritedError(`Expansion with id: ${expansionIds[index]} could not be loaded`, expansion); } + + const hash = crypto + .createHash('sha256') + .update( + JSON.stringify({ + commandDictionaryId, + missionModelId, + id: expansion.id, + expansionLogic: expansion.expansionLogic, + activityType: expansion.activityType, + }), + ) + .digest('hex'); + + if (typeCheckingCache.has(hash)) { + console.log(`Using cached typechecked data for ${expansion.activityType}`); + return typeCheckingCache.get(hash); + } + const activitySchema = await context.activitySchemaDataLoader.load({ missionModelId, activityTypeName: expansion.activityType, }); const activityTypescript = generateTypescriptForGraphQLActivitySchema(activitySchema); - const result = Result.fromJSON( - await (piscina.run( - { - expansionLogic: expansion.expansionLogic, - commandTypes: commandTypes, - activityTypes: activityTypescript, - }, - { name: 'typecheckExpansion' }, - ) as ReturnType), - ); + const typeCheckResult = promiseThrottler.run(() => { + return ( + piscina.run( + { + expansionLogic: expansion.expansionLogic, + commandTypes: commandTypes, + activityTypes: activityTypescript, + activityTypeName: expansion.activityType, + }, + { name: 'typecheckExpansion' }, + ) as ReturnType + ).then(Result.fromJSON); + }); - return result; + typeCheckingCache.set(hash, typeCheckResult); + return typeCheckResult; }), ); const errors = unwrapPromiseSettledResults(typecheckErrorPromises).reduce((accum, item) => { - if (item instanceof Error) { - accum.push(item); - } else if (item.isErr()) { - accum.push(...item.unwrapErr()); + if (item && (item instanceof Error || item.isErr)) { + // Check for item's existence before accessing properties + if (item instanceof Error) { + accum.push(item); + } else if (item.isErr()) { + try { + accum.push(...item.unwrapErr()); // Handle potential errors within unwrapErr + } catch (error) { + accum.push(new Error('Failed to unwrap error: ' + error)); // Log unwrapErr errors + } + } + } else { + accum.push(new Error('Unexpected result in resolved promises')); // Handle unexpected non-error values } + return accum; }, [] as (Error | ReturnType)[]); @@ -168,15 +203,10 @@ commandExpansionRouter.post('/expand-all-activity-instances', async (req, res, n context.expansionSetDataLoader.load({ expansionSetId }), context.simulatedActivitiesDataLoader.load({ simulationDatasetId }), ]); + const commandDictionaryId = expansionSet.commandDictionary.id; + const missionModelId = expansionSet.missionModel.id; const commandTypes = expansionSet.commandDictionary.commandTypesTypeScript; - // Note: We are keeping the Promise in the cache so that we don't have to wait for resolution to insert into - // the cache and consequently end up doing the compilation multiple times because of a cache miss. - const expansionBuildArtifactsCache = new Map< - number, - Promise[]>> - >(); - const settledExpansionResults = await Promise.allSettled( simulatedActivities.map(async simulatedActivity => { // The simulatedActivity's duration and endTime will be null if the effect model reaches across the plan end boundaries. @@ -205,21 +235,36 @@ commandExpansionRouter.post('/expand-all-activity-instances', async (req, res, n } const activityTypes = generateTypescriptForGraphQLActivitySchema(activitySchema); - if (!expansionBuildArtifactsCache.has(expansion.id)) { - const typecheckResult = ( - piscina.run( - { - expansionLogic: expansion.expansionLogic, - commandTypes: commandTypes, - activityTypes, - }, - { name: 'typecheckExpansion' }, - ) as ReturnType - ).then(Result.fromJSON); - expansionBuildArtifactsCache.set(expansion.id, typecheckResult); - } + const hash = crypto + .createHash('sha256') + .update( + JSON.stringify({ + commandDictionaryId, + missionModelId, + id: expansion.id, + expansionLogic: expansion.expansionLogic, + activityType: expansion.activityType, + }), + ) + .digest('hex'); + if (!typeCheckingCache.has(hash)) { + const typeCheckResult = promiseThrottler.run(() => { + return ( + piscina.run( + { + expansionLogic: expansion.expansionLogic, + commandTypes: commandTypes, + activityTypes: activityTypes, + activityTypeName: expansion.activityType, + }, + { name: 'typecheckExpansion' }, + ) as ReturnType + ).then(Result.fromJSON); + }); - const expansionBuildArtifacts = await expansionBuildArtifactsCache.get(expansion.id)!; + typeCheckingCache.set(hash, typeCheckResult); + } + const expansionBuildArtifacts = await typeCheckingCache.get(hash)!; if (expansionBuildArtifacts.isErr()) { return { From 3b39cd42461fa1fef93770060bc1a4733020e519 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Tue, 6 Feb 2024 13:53:47 -1000 Subject: [PATCH 063/159] Added helpful logging messages to the workers for troubleshooting. --- sequencing-server/src/worker.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sequencing-server/src/worker.ts b/sequencing-server/src/worker.ts index 44e040b3ea..37c9bbc0a0 100644 --- a/sequencing-server/src/worker.ts +++ b/sequencing-server/src/worker.ts @@ -26,7 +26,13 @@ export async function typecheckExpansion(opts: { expansionLogic: string; commandTypes: string; activityTypes: string; + activityTypeName?: string; }): Promise[]>> { + const startTime = Date.now(); + console.log( + `[ Worker ] started transpiling authoring logic ${opts.activityTypeName ? `- ${opts.activityTypeName}` : ''}`, + ); + const result = await codeRunner.preProcess( opts.expansionLogic, 'ExpansionReturn', @@ -37,6 +43,14 @@ export async function typecheckExpansion(opts: { ts.createSourceFile('TemporalPolyfillTypes.ts', temporalPolyfillTypes, compilerTarget), ], ); + + const endTime = Date.now(); + console.log( + `[ Worker ] finished transpiling ${opts.activityTypeName ? `- ${opts.activityTypeName}` : ''}, (${ + (endTime - startTime) / 1000 + } s)`, + ); + if (result.isOk()) { return Result.Ok(result.unwrap()).toJSON(); } else { From 4be8a55da2c35a37cecfdbb5190ddd4d2747d14f Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 4 Jan 2024 14:28:34 -0800 Subject: [PATCH 064/159] Reorganize Triggers in Plan.sql --- merlin-server/sql/merlin/tables/plan.sql | 83 +++++++++++++----------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/merlin-server/sql/merlin/tables/plan.sql b/merlin-server/sql/merlin/tables/plan.sql index 67f84cff07..b2b7344510 100644 --- a/merlin-server/sql/merlin/tables/plan.sql +++ b/merlin-server/sql/merlin/tables/plan.sql @@ -76,24 +76,37 @@ comment on column plan.updated_by is e'' comment on column plan.description is e'' 'A human-readable description for this plan and its contents.'; +-- Insert Triggers -create function increment_revision_on_update_plan() +create function create_simulation_row_for_new_plan() returns trigger security definer language plpgsql as $$begin - update plan - set revision = revision + 1 - where id = new.id - or id = old.id; + insert into simulation (revision, simulation_template_id, plan_id, arguments, simulation_start_time, simulation_end_time) + values (0, null, new.id, '{}', new.start_time, new.start_time+new.duration); + return new; +end +$$; + +create trigger simulation_row_for_new_plan_trigger +after insert on plan +for each row +execute function create_simulation_row_for_new_plan(); + +-- Insert or Update Triggers +create function plan_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); return new; end$$; -create trigger increment_revision_on_update_plan_trigger -after update on plan -for each row -when (pg_trigger_depth() < 1) -execute function increment_revision_on_update_plan(); +create trigger set_timestamp + before update or insert on plan + for each row +execute function plan_set_updated_at(); create function raise_duration_is_negative() returns trigger @@ -108,6 +121,28 @@ for each row when (new.duration < '0') execute function raise_duration_is_negative(); +-- Update Triggers + +create function increment_revision_on_update_plan() +returns trigger +security definer +language plpgsql as $$begin + update plan + set revision = revision + 1 + where id = new.id + or id = old.id; + + return new; +end$$; + +create trigger increment_revision_on_update_plan_trigger +after update on plan +for each row +when (pg_trigger_depth() < 1) +execute function increment_revision_on_update_plan(); + +-- Delete Triggers + create function cleanup_on_delete() returns trigger language plpgsql as $$ @@ -136,31 +171,3 @@ create trigger cleanup_on_delete_trigger before delete on plan for each row execute function cleanup_on_delete(); - -create function create_simulation_row_for_new_plan() -returns trigger -security definer -language plpgsql as $$begin - insert into simulation (revision, simulation_template_id, plan_id, arguments, simulation_start_time, simulation_end_time) - values (0, null, new.id, '{}', new.start_time, new.start_time+new.duration); - return new; -end -$$; - -create trigger simulation_row_for_new_plan_trigger -after insert on plan -for each row -execute function create_simulation_row_for_new_plan(); - -create function plan_set_updated_at() -returns trigger -security definer -language plpgsql as $$begin - new.updated_at = now(); - return new; -end$$; - -create trigger set_timestamp - before update or insert on plan - for each row -execute function plan_set_updated_at(); From b89ae8f2011d05ba5e6f7a6332a6e078a3ce3d21 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 4 Jan 2024 16:13:14 -0800 Subject: [PATCH 065/159] Split constraints table into definition and metadata --- merlin-server/sql/merlin/init.sql | 9 +- .../sql/merlin/tables/constraint.sql | 108 ------------------ .../merlin/tables/constraint_definition.sql | 56 +++++++++ .../sql/merlin/tables/constraint_metadata.sql | 63 ++++++++++ .../sql/merlin/tables/constraint_run.sql | 17 ++- .../metadata/constraint_definition_tags.sql | 14 +++ .../tables/metadata/constraint_tags.sql | 2 +- 7 files changed, 148 insertions(+), 121 deletions(-) delete mode 100644 merlin-server/sql/merlin/tables/constraint.sql create mode 100644 merlin-server/sql/merlin/tables/constraint_definition.sql create mode 100644 merlin-server/sql/merlin/tables/constraint_metadata.sql create mode 100644 merlin-server/sql/merlin/tables/metadata/constraint_definition_tags.sql diff --git a/merlin-server/sql/merlin/init.sql b/merlin-server/sql/merlin/init.sql index be3c06a929..cc6b18bb77 100644 --- a/merlin-server/sql/merlin/init.sql +++ b/merlin-server/sql/merlin/init.sql @@ -50,12 +50,16 @@ begin; \ir tables/event.sql -- Analysis intents - \ir tables/constraint.sql - \ir tables/mission_model_parameters.sql \ir tables/simulation_dataset.sql \ir tables/simulation_extent.sql \ir tables/plan_dataset.sql + + -- Constraints + \ir tables/constraint_metadata.sql + \ir tables/constraint_definition.sql + \ir tables/constraint_specification.sql + \ir tables/constraint_model_specification.sql \ir tables/constraint_run.sql -- Plan Collaboration @@ -83,6 +87,7 @@ begin; -- Table-specific Metadata \ir tables/metadata/activity_directive_tags.sql \ir tables/metadata/constraint_tags.sql + \ir tables/metadata/constraint_definition_tags.sql \ir tables/metadata/plan_snapshot_tags.sql \ir tables/metadata/plan_tags.sql \ir tables/metadata/snapshot_activity_tags.sql diff --git a/merlin-server/sql/merlin/tables/constraint.sql b/merlin-server/sql/merlin/tables/constraint.sql deleted file mode 100644 index dca303a20e..0000000000 --- a/merlin-server/sql/merlin/tables/constraint.sql +++ /dev/null @@ -1,108 +0,0 @@ -create table "constraint" ( - id integer generated always as identity, - - name text not null, - description text not null default '', - definition text not null, - - plan_id integer null, - model_id integer null, - - created_at timestamptz not null default now(), - updated_at timestamptz not null default now(), - - owner text, - updated_by text, - - constraint constraint_synthetic_key - primary key (id), - constraint constraint_owner_exists - foreign key (owner) - references metadata.users - on update cascade - on delete set null, - constraint constraint_updated_by_exists - foreign key (updated_by) - references metadata.users - on update cascade - on delete set null, - constraint constraint_scoped_to_plan - foreign key (plan_id) - references plan - on update cascade - on delete cascade, - constraint constraint_scoped_to_model - foreign key (model_id) - references mission_model - on update cascade - on delete cascade, - constraint constraint_has_one_scope - check ( - -- Model-scoped - (plan_id is null and model_id is not null) or - -- Plan-scoped - (plan_id is not null and model_id is null) - ) -); - -comment on table "constraint" is e'' - 'A constraint associated with an individual plan.'; - -comment on column "constraint".id is e'' - 'The synthetic identifier for this constraint.'; -comment on column "constraint".name is e'' - 'A human-meaningful name.'; -comment on column "constraint".description is e'' - 'A detailed description suitable for long-form documentation.'; -comment on column "constraint".definition is e'' - 'An executable expression in the Merlin constraint language.'; -comment on column "constraint".plan_id is e'' - 'The ID of the plan owning this constraint, if plan-scoped.'; -comment on column "constraint".model_id is e'' - 'The ID of the mission model owning this constraint, if model-scoped.'; -comment on column "constraint".owner is e'' - 'The user responsible for this constraint.'; -comment on column "constraint".updated_by is e'' - 'The user who last modified this constraint.'; -comment on column "constraint".created_at is e'' - 'The time at which this constraint was created.'; -comment on column "constraint".updated_at is e'' - 'The time at which this constraint was last modified.'; - - -create or replace function constraint_set_updated_at() -returns trigger -security definer -language plpgsql as $$begin - new.updated_at = now(); - return new; -end$$; - -create trigger set_timestamp -before update on "constraint" -for each row -execute function constraint_set_updated_at(); - -create function constraint_check_constraint_run() - returns trigger - security definer - language plpgsql as $$ -begin - update constraint_run - set definition_outdated = true - where constraint_id = new.id - and constraint_definition != new.definition - and definition_outdated = false; - update constraint_run - set definition_outdated = false - where constraint_id = new.id - and constraint_definition = new.definition - and definition_outdated = true; - return new; -end$$; - -create trigger constraint_check_constraint_run_trigger - after update on "constraint" - for each row - when (new.definition != old.definition) -execute function constraint_check_constraint_run(); diff --git a/merlin-server/sql/merlin/tables/constraint_definition.sql b/merlin-server/sql/merlin/tables/constraint_definition.sql new file mode 100644 index 0000000000..00c79bff89 --- /dev/null +++ b/merlin-server/sql/merlin/tables/constraint_definition.sql @@ -0,0 +1,56 @@ +create table constraint_definition( + constraint_id integer not null, + revision integer not null default 0, + definition text not null, + author text, + created_at timestamptz not null default now(), + + constraint constraint_definition_pkey + primary key (constraint_id, revision), + constraint constraint_definition_constraint_exists + foreign key (constraint_id) + references constraint_metadata + on update cascade + on delete cascade, + constraint constraint_definition_author_exists + foreign key (author) + references metadata.users + on update cascade + on delete set null +); + +comment on table constraint_definition is e'' + 'The specific revisions of a constraint''s definition'; +comment on column constraint_definition.revision is e'' + 'An identifier of this definition.'; +comment on column constraint_definition.definition is e'' + 'An executable expression in the Merlin constraint language.'; +comment on column constraint_definition.author is e'' + 'The user who authored this revision.'; +comment on column constraint_definition.created_at is e'' + 'When this revision was created.'; + +create function constraint_definition_set_revision() +returns trigger +volatile +language plpgsql as $$ +declare + max_revision integer; +begin + -- Grab the current max value of revision, or -1, if this is the first revision + select coalesce((select revision + from constraint_definition + where constraint_id = new.constraint_id + order by revision desc + limit 1), -1) + into max_revision; + + new.revision = max_revision + 1; + return new; +end +$$; + +create trigger constraint_definition_set_revision + before insert on constraint_definition + for each row + execute function constraint_definition_set_revision(); diff --git a/merlin-server/sql/merlin/tables/constraint_metadata.sql b/merlin-server/sql/merlin/tables/constraint_metadata.sql new file mode 100644 index 0000000000..d3d6bd5950 --- /dev/null +++ b/merlin-server/sql/merlin/tables/constraint_metadata.sql @@ -0,0 +1,63 @@ +create table constraint_metadata( + id integer generated always as identity, + + name text not null, + description text not null default '', + public boolean not null default false, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + owner text, + updated_by text, + + constraint constraint_metadata_pkey + primary key (id), + constraint constraint_owner_exists + foreign key (owner) + references metadata.users + on update cascade + on delete set null, + constraint constraint_updated_by_exists + foreign key (updated_by) + references metadata.users + on update cascade + on delete set null +); + +-- A partial index is used to enforce name uniqueness only on constraints visible to other users +create unique index name_unique_if_published on constraint_metadata (name) where public; + +comment on table constraint_metadata is e'' + 'The metadata for a constraint'; +comment on column constraint_metadata.id is e'' + 'The unique identifier of the constraint'; +comment on column constraint_metadata.name is e'' + 'A human-meaningful name.'; +comment on column constraint_metadata.description is e'' + 'A detailed description suitable for long-form documentation.'; +comment on column constraint_metadata.public is e'' + 'Whether this constraint is visible to all users.'; +comment on column constraint_metadata.owner is e'' + 'The user responsible for this constraint.'; +comment on column constraint_metadata.updated_by is e'' + 'The user who last modified this constraint''s metadata.'; +comment on column constraint_metadata.created_at is e'' + 'The time at which this constraint was created.'; +comment on column constraint_metadata.updated_at is e'' + 'The time at which this constraint''s metadata was last modified.'; + +create function constraint_metadata_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp +before update on constraint_metadata +for each row +execute function constraint_metadata_set_updated_at(); + + diff --git a/merlin-server/sql/merlin/tables/constraint_run.sql b/merlin-server/sql/merlin/tables/constraint_run.sql index c0fa9d7957..d57c50db71 100644 --- a/merlin-server/sql/merlin/tables/constraint_run.sql +++ b/merlin-server/sql/merlin/tables/constraint_run.sql @@ -1,9 +1,8 @@ create table constraint_run ( constraint_id integer not null, - constraint_definition text not null, + constraint_revision integer not null, simulation_dataset_id integer not null, - definition_outdated boolean default false not null, results jsonb not null default '{}', -- Additional Metadata @@ -11,10 +10,10 @@ create table constraint_run ( requested_at timestamptz not null default now(), constraint constraint_run_key - primary key (constraint_id, constraint_definition, simulation_dataset_id), - constraint constraint_run_to_constraint - foreign key (constraint_id) - references "constraint" + primary key (constraint_id, constraint_revision, simulation_dataset_id), + constraint constraint_run_to_constraint_definition + foreign key (constraint_id, constraint_revision) + references constraint_definition on delete cascade, constraint constraint_run_to_simulation_dataset foreign key (simulation_dataset_id) @@ -35,12 +34,10 @@ comment on table constraint_run is e'' comment on column constraint_run.constraint_id is e'' 'The constraint that we are evaluating during the run.'; -comment on column constraint_run.constraint_definition is e'' - 'The definition of the constraint when it was checked, used to determine staleness.'; +comment on column constraint_run.constraint_revision is e'' + 'The version of the constraint definition that was checked.'; comment on column constraint_run.simulation_dataset_id is e'' 'The simulation dataset id from when the constraint was checked.'; -comment on column constraint_run.definition_outdated is e'' - 'Tracks if the constraint definition is outdated because the constraint has been changed.'; comment on column constraint_run.results is e'' 'Results that were computed during the constraint check.'; comment on column constraint_run.requested_by is e'' diff --git a/merlin-server/sql/merlin/tables/metadata/constraint_definition_tags.sql b/merlin-server/sql/merlin/tables/metadata/constraint_definition_tags.sql new file mode 100644 index 0000000000..9c631d9dcc --- /dev/null +++ b/merlin-server/sql/merlin/tables/metadata/constraint_definition_tags.sql @@ -0,0 +1,14 @@ +create table metadata.constraint_definition_tags ( + constraint_id integer not null, + constraint_revision integer not null, + tag_id integer not null references metadata.tags + on update cascade + on delete cascade, + primary key (constraint_id, constraint_revision, tag_id), + foreign key (constraint_id, constraint_revision) references constraint_definition + on update cascade + on delete cascade +); + +comment on table metadata.constraint_definition_tags is e'' + 'The tags associated with a specific constraint defintion.'; diff --git a/merlin-server/sql/merlin/tables/metadata/constraint_tags.sql b/merlin-server/sql/merlin/tables/metadata/constraint_tags.sql index 3dbcc33e5c..26065b0227 100644 --- a/merlin-server/sql/merlin/tables/metadata/constraint_tags.sql +++ b/merlin-server/sql/merlin/tables/metadata/constraint_tags.sql @@ -1,5 +1,5 @@ create table metadata.constraint_tags ( - constraint_id integer not null references public."constraint" + constraint_id integer not null references public.constraint_metadata on update cascade on delete cascade, tag_id integer not null references metadata.tags From f5100c5c66441b1a4b8128fc00fdf4dbcd71bdbc Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 4 Jan 2024 16:13:29 -0800 Subject: [PATCH 066/159] Add Plan and Model Constraint Specifications --- .../tables/constraint_model_specification.sql | 30 +++++++++++++++++ .../tables/constraint_specification.sql | 33 +++++++++++++++++++ merlin-server/sql/merlin/tables/plan.sql | 20 +++++++++++ 3 files changed, 83 insertions(+) create mode 100644 merlin-server/sql/merlin/tables/constraint_model_specification.sql create mode 100644 merlin-server/sql/merlin/tables/constraint_specification.sql diff --git a/merlin-server/sql/merlin/tables/constraint_model_specification.sql b/merlin-server/sql/merlin/tables/constraint_model_specification.sql new file mode 100644 index 0000000000..101c762058 --- /dev/null +++ b/merlin-server/sql/merlin/tables/constraint_model_specification.sql @@ -0,0 +1,30 @@ +create table constraint_model_specification( + model_id integer not null + references mission_model + on update cascade + on delete cascade, + constraint_id integer not null, + constraint_revision integer, -- latest is NULL + + constraint constraint_model_spec_pkey + primary key (model_id, constraint_id), + constraint model_spec_constraint_exists + foreign key (constraint_id) + references constraint_metadata(id) + on update cascade + on delete restrict, + constraint model_spec_constraint_definition_exists + foreign key (constraint_id, constraint_revision) + references constraint_definition(constraint_id, revision) + on update cascade + on delete restrict +); + +comment on table constraint_model_specification is e'' +'The set of constraints that all plans using the model should include in their constraint specification.'; +comment on column constraint_model_specification.model_id is e'' +'The model which this specification is for. Half of the primary key.'; +comment on column constraint_model_specification.constraint_id is e'' +'The id of a specific constraint in the specification. Half of the primary key.'; +comment on column constraint_model_specification.constraint_revision is e'' +'The version of the constraint definition to use. Leave NULL to use the latest version.'; diff --git a/merlin-server/sql/merlin/tables/constraint_specification.sql b/merlin-server/sql/merlin/tables/constraint_specification.sql new file mode 100644 index 0000000000..44f17d2893 --- /dev/null +++ b/merlin-server/sql/merlin/tables/constraint_specification.sql @@ -0,0 +1,33 @@ +create table constraint_specification( + plan_id integer not null + references plan + on update cascade + on delete cascade, + constraint_id integer not null, + constraint_revision integer, -- latest is NULL + enabled boolean not null default true, + + constraint constraint_specification_pkey + primary key (plan_id, constraint_id), + constraint plan_spec_constraint_exists + foreign key (constraint_id) + references constraint_metadata(id) + on update cascade + on delete restrict, + constraint plan_spec_constraint_definition_exists + foreign key (constraint_id, constraint_revision) + references constraint_definition(constraint_id, revision) + on update cascade + on delete restrict +); + +comment on table constraint_specification is e'' +'The set of constraints to be checked for a given plan.'; +comment on column constraint_specification.plan_id is e'' +'The plan which this specification is for. Half of the primary key.'; +comment on column constraint_specification.constraint_id is e'' +'The id of a specific constraint in the specification. Half of the primary key.'; +comment on column constraint_specification.constraint_revision is e'' +'The version of the constraint definition to use. Leave NULL to use the latest version.'; +comment on column constraint_specification.enabled is e'' +'Whether to run a given constraint. Defaults to TRUE.'; diff --git a/merlin-server/sql/merlin/tables/plan.sql b/merlin-server/sql/merlin/tables/plan.sql index b2b7344510..c84082cfec 100644 --- a/merlin-server/sql/merlin/tables/plan.sql +++ b/merlin-server/sql/merlin/tables/plan.sql @@ -93,6 +93,26 @@ after insert on plan for each row execute function create_simulation_row_for_new_plan(); +create function populate_constraint_spec_new_plan() +returns trigger +language plpgsql as $$ +begin + insert into constraint_specification (plan_id, constraint_id, constraint_revision) + select new.id, cms.constraint_id, cms.constraint_revision + from constraint_model_specification cms + where cms.model_id = new.model_id; + return new; +end; +$$; + +comment on function populate_constraint_spec_new_plan() is e'' +'Populates the plan''s constraint specification with the contents of its model''s specification.'; + +create trigger populate_constraint_spec_new_plan_trigger +after insert on plan +for each row +execute function populate_constraint_spec_new_plan(); + -- Insert or Update Triggers create function plan_set_updated_at() From 9cc5df23e836956bc71e9819b0f7d9e698754e81 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 11 Jan 2024 08:47:45 -0800 Subject: [PATCH 067/159] DB Migration --- .../38_constraint_versioning/down.sql | 217 +++++++++++++ .../38_constraint_versioning/up.sql | 289 ++++++++++++++++++ .../sql/merlin/applied_migrations.sql | 1 + 3 files changed, 507 insertions(+) create mode 100644 deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/down.sql create mode 100644 deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/up.sql diff --git a/deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/down.sql b/deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/down.sql new file mode 100644 index 0000000000..f25b2ad613 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/down.sql @@ -0,0 +1,217 @@ +/**** ADD CONSTRAINTS ****/ +create table "constraint" ( + id integer generated always as identity, + + name text not null, + description text not null default '', + definition text not null, + + plan_id integer null, + model_id integer null, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + owner text, + updated_by text, + + constraint constraint_synthetic_key + primary key (id), + constraint constraint_owner_exists + foreign key (owner) + references metadata.users + on update cascade + on delete set null, + constraint constraint_updated_by_exists + foreign key (updated_by) + references metadata.users + on update cascade + on delete set null, + constraint constraint_scoped_to_plan + foreign key (plan_id) + references plan + on update cascade + on delete cascade, + constraint constraint_scoped_to_model + foreign key (model_id) + references mission_model + on update cascade + on delete cascade, + constraint constraint_has_one_scope + check ( + -- Model-scoped + (plan_id is null and model_id is not null) or + -- Plan-scoped + (plan_id is not null and model_id is null) + ) +); + +comment on table "constraint" is e'' + 'A constraint associated with an individual plan.'; + +comment on column "constraint".id is e'' + 'The synthetic identifier for this constraint.'; +comment on column "constraint".name is e'' + 'A human-meaningful name.'; +comment on column "constraint".description is e'' + 'A detailed description suitable for long-form documentation.'; +comment on column "constraint".definition is e'' + 'An executable expression in the Merlin constraint language.'; +comment on column "constraint".plan_id is e'' + 'The ID of the plan owning this constraint, if plan-scoped.'; +comment on column "constraint".model_id is e'' + 'The ID of the mission model owning this constraint, if model-scoped.'; +comment on column "constraint".owner is e'' + 'The user responsible for this constraint.'; +comment on column "constraint".updated_by is e'' + 'The user who last modified this constraint.'; +comment on column "constraint".created_at is e'' + 'The time at which this constraint was created.'; +comment on column "constraint".updated_at is e'' + 'The time at which this constraint was last modified.'; + +create function constraint_check_constraint_run() + returns trigger + security definer + language plpgsql as $$ +begin + update constraint_run + set definition_outdated = true + where constraint_id = new.id + and constraint_definition != new.definition + and definition_outdated = false; + update constraint_run + set definition_outdated = false + where constraint_id = new.id + and constraint_definition = new.definition + and definition_outdated = true; + return new; +end$$; +create trigger constraint_check_constraint_run_trigger + after update on "constraint" + for each row + when (new.definition != old.definition) +execute function constraint_check_constraint_run(); + +-- Timestamp Trigger will be added after data migration for data consistency + +/****** TAGS *******/ +drop table metadata.constraint_definition_tags; + +alter table metadata.constraint_tags drop constraint constraint_tags_constraint_id_fkey; +alter table metadata.constraint_tags add foreign key (constraint_id) + references "constraint" + on update cascade + on delete cascade; + +/****** CONSTRAINT RUN ******/ +-- Clear cache +truncate table constraint_run; + +alter table constraint_run +drop constraint constraint_run_key, +drop constraint constraint_run_to_constraint_definition, +drop column constraint_revision, +add column definition_outdated boolean default false not null, +add column constraint_definition text not null, +add constraint constraint_run_to_constraint + foreign key (constraint_id) + references "constraint" + on delete cascade, +add constraint constraint_run_key + primary key (constraint_id, constraint_definition, simulation_dataset_id); + +comment on column constraint_run.constraint_definition is e'' + 'The definition of the constraint when it was checked, used to determine staleness.'; +comment on column constraint_run.definition_outdated is e'' + 'Tracks if the constraint definition is outdated because the constraint has been changed.'; + +/******* DATA MIGRATION *******/ +-- Add model constraints +-- Because multiple models may be using the same constraint/constraint definition, we have to regenerate the constraint's id +with specified_definition(constraint_id, model_id, definition, definition_creation) as ( + select cd.constraint_id, cd.revision, ms.model_id, cd.definition, cd.created_at + from constraint_model_specification ms + left join constraint_definition cd using (constraint_id) + where (ms.constraint_revision is not null and ms.constraint_revision = cd.revision) + or (ms.constraint_revision is null and cd.revision = (select def.revision + from constraint_definition def + where def.constraint_id = ms.constraint_id + order by def.revision desc limit 1))) +insert into "constraint"(name, description, definition, model_id, created_at, updated_at, owner, updated_by) +select cm.name, cm.description, sd.definition, sd.model_id, cm.created_at, greatest(cm.updated_at::timestamptz, sd.definition_creation::timestamptz), cm.owner, cm.updated_by + from specified_definition sd + left join constraint_metadata cm on cm.id = sd.constraint_id; + +-- Add plan constraints +-- Because multiple plans may be using the same constraint/constraint definition, we have to regenerate the constraint's id +with + specified_plan_definition(constraint_id, revision, plan_id, definition, definition_creation) as ( + select cd.constraint_id, cd.revision, s.plan_id, cd.definition, cd.created_at + from constraint_specification s + left join constraint_definition cd using (constraint_id) + where (s.constraint_revision is not null and s.constraint_revision = cd.revision) + or (s.constraint_revision is null and cd.revision = (select def.revision + from constraint_definition def + where def.constraint_id = s.constraint_id + order by def.revision desc limit 1))), + specified_model_definition(constraint_id, revision, plan_id, definition, definition_creation) as ( + select cd.constraint_id, cd.revision, p.id, cd.definition, cd.created_at + from constraint_model_specification cms + left join constraint_definition cd using (constraint_id) + left join plan p using (model_id) + where (cms.constraint_revision is not null and cms.constraint_revision = cd.revision) + or (cms.constraint_revision is null and cd.revision = (select def.revision + from constraint_definition def + where def.constraint_id = cms.constraint_id + order by def.revision desc limit 1))) +insert into "constraint"(name, description, definition, plan_id, created_at, updated_at, owner, updated_by) + select cm.name, cm.description, pd.definition, pd.plan_id, cm.created_at, + greatest(cm.updated_at::timestamptz, pd.definition_creation::timestamptz), cm.owner, cm.updated_by + from specified_plan_definition pd + left join constraint_metadata cm on cm.id = pd.constraint_id +-- Exclude constraints that match the model spec +except + select cm.name, cm.description, md.definition, md.plan_id, cm.created_at, + greatest(cm.updated_at::timestamptz, md.definition_creation::timestamptz), cm.owner, cm.updated_by + from specified_model_definition md + left join constraint_metadata cm on cm.id = md.constraint_id; + +-- Add timestamp trigger for future entries +create or replace function constraint_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp +before update on "constraint" +for each row +execute function constraint_set_updated_at(); + +/****** NEW TABLES ******/ +/*-- PLAN TRIGGER --*/ +drop trigger populate_constraint_spec_new_plan_trigger on plan; +drop function populate_constraint_spec_new_plan(); + +/*-- CONSTRAINT MODEL SPECIFICATION --*/ +drop table constraint_model_specification; + +/*-- CONSTRAINT SPECIFICATION --*/ +drop table constraint_specification; + +/*-- CONSTRAINT DEFINITION --*/ +drop trigger constraint_definition_set_revision on constraint_definition; +drop function constraint_definition_set_revision(); +drop table constraint_definition; + +/*-- CONSTRAINT METADATA --*/ +drop trigger set_timestamp on constraint_metadata; +drop function constraint_metadata_set_updated_at(); + +drop index name_unique_if_published; +drop table constraint_metadata; + +call migrations.mark_migration_rolled_back('38'); diff --git a/deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/up.sql b/deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/up.sql new file mode 100644 index 0000000000..16c5ac2583 --- /dev/null +++ b/deployment/hasura/migrations/AerieMerlin/38_constraint_versioning/up.sql @@ -0,0 +1,289 @@ +/****** NEW TABLES ******/ +/*-- CONSTRAINT METADATA --*/ +create table constraint_metadata( + id integer generated by default as identity, + + name text not null, + description text not null default '', + public boolean not null default false, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + owner text, + updated_by text, + + constraint constraint_metadata_pkey + primary key (id), + constraint constraint_owner_exists + foreign key (owner) + references metadata.users + on update cascade + on delete set null, + constraint constraint_updated_by_exists + foreign key (updated_by) + references metadata.users + on update cascade + on delete set null +); + +-- A partial index is used to enforce name uniqueness only on constraints visible to other users +create unique index name_unique_if_published on constraint_metadata (name) where public; + +comment on table constraint_metadata is e'' + 'The metadata for a constraint'; +comment on column constraint_metadata.id is e'' + 'The unique identifier of the constraint'; +comment on column constraint_metadata.name is e'' + 'A human-meaningful name.'; +comment on column constraint_metadata.description is e'' + 'A detailed description suitable for long-form documentation.'; +comment on column constraint_metadata.owner is e'' + 'The user responsible for this constraint.'; +comment on column constraint_metadata.public is e'' + 'Whether this constraint is visible to all users.'; +comment on column constraint_metadata.updated_by is e'' + 'The user who last modified this constraint''s metadata.'; +comment on column constraint_metadata.created_at is e'' + 'The time at which this constraint was created.'; +comment on column constraint_metadata.updated_at is e'' + 'The time at which this constraint''s metadata was last modified.'; + +/*-- CONSTRAINT DEFINITION --*/ +create table constraint_definition( + constraint_id integer not null, + revision integer not null default 0, + definition text not null, + author text, + created_at timestamptz not null default now(), + + constraint constraint_definition_pkey + primary key (constraint_id, revision), + constraint constraint_definition_constraint_exists + foreign key (constraint_id) + references constraint_metadata + on update cascade + on delete cascade, + constraint constraint_definition_author_exists + foreign key (author) + references metadata.users + on update cascade + on delete set null +); + +comment on table constraint_definition is e'' + 'The specific revisions of a constraint''s definition'; +comment on column constraint_definition.revision is e'' + 'An identifier of this definition.'; +comment on column constraint_definition.definition is e'' + 'An executable expression in the Merlin constraint language.'; +comment on column constraint_definition.author is e'' + 'The user who authored this revision.'; +comment on column constraint_definition.created_at is e'' + 'When this revision was created.'; + +create function constraint_definition_set_revision() +returns trigger +volatile +language plpgsql as $$ +declare + max_revision integer; +begin + -- Grab the current max value of revision, or -1, if this is the first revision + select coalesce((select revision + from constraint_definition + where constraint_id = new.constraint_id + order by revision desc + limit 1), -1) + into max_revision; + + new.revision = max_revision + 1; + return new; +end +$$; +create trigger constraint_definition_set_revision + before insert on constraint_definition + for each row + execute function constraint_definition_set_revision(); + +/*-- CONSTRAINT SPECIFICATION --*/ +create table constraint_specification( + plan_id integer not null + references plan + on update cascade + on delete cascade, + constraint_id integer not null, + constraint_revision integer, -- latest is NULL + enabled boolean not null default true, + + constraint constraint_specification_pkey + primary key (plan_id, constraint_id), + constraint plan_spec_constraint_exists + foreign key (constraint_id) + references constraint_metadata(id) + on update cascade + on delete restrict, + constraint plan_spec_constraint_definition_exists + foreign key (constraint_id, constraint_revision) + references constraint_definition(constraint_id, revision) + on update cascade + on delete restrict +); + +comment on table constraint_specification is e'' +'The set of constraints to be checked for a given plan.'; +comment on column constraint_specification.plan_id is e'' +'The plan which this specification is for. Half of the primary key.'; +comment on column constraint_specification.constraint_id is e'' +'The id of a specific constraint in the specification. Half of the primary key.'; +comment on column constraint_specification.constraint_revision is e'' +'The version of the constraint definition to use. Leave NULL to use the latest version.'; +comment on column constraint_specification.enabled is e'' +'Whether to run a given constraint. Defaults to TRUE.'; + +/*-- CONSTRAINT MODEL SPECIFICATION --*/ +create table constraint_model_specification( + model_id integer not null + references mission_model + on update cascade + on delete cascade, + constraint_id integer not null, + constraint_revision integer, -- latest is NULL + + constraint constraint_model_spec_pkey + primary key (model_id, constraint_id), + constraint model_spec_constraint_exists + foreign key (constraint_id) + references constraint_metadata(id) + on update cascade + on delete restrict, + constraint model_spec_constraint_definition_exists + foreign key (constraint_id, constraint_revision) + references constraint_definition(constraint_id, revision) + on update cascade + on delete restrict +); + +comment on table constraint_model_specification is e'' +'The set of constraints that all plans using the model should include in their constraint specification.'; +comment on column constraint_model_specification.model_id is e'' +'The model which this specification is for. Half of the primary key.'; +comment on column constraint_model_specification.constraint_id is e'' +'The id of a specific constraint in the specification. Half of the primary key.'; +comment on column constraint_model_specification.constraint_revision is e'' +'The version of the constraint definition to use. Leave NULL to use the latest version.'; + +/*-- PLAN TRIGGER --*/ +create function populate_constraint_spec_new_plan() +returns trigger +language plpgsql as $$ +begin + insert into constraint_specification (plan_id, constraint_id, constraint_revision) + select new.id, cms.constraint_id, cms.constraint_revision + from constraint_model_specification cms + where cms.model_id = new.model_id; + return new; +end; +$$; +comment on function populate_constraint_spec_new_plan() is e'' +'Populates the plan''s constraint specification with the contents of its model''s specification.'; + +create trigger populate_constraint_spec_new_plan_trigger +after insert on plan +for each row +execute function populate_constraint_spec_new_plan(); + +/******* DATA MIGRATION *******/ +insert into constraint_metadata(id, name, description, public, created_at, updated_at, owner, updated_by) +select c.id, c.name, c.description, false, c.created_at, c.updated_at, c.owner, c.updated_by + from "constraint" c; + +insert into constraint_definition(constraint_id, definition, author, created_at) +select cm.id, c.definition, c.owner, c.created_at + from "constraint" c join constraint_metadata cm using (id); + +insert into constraint_model_specification(model_id, constraint_id, constraint_revision) +select c.model_id, cd.constraint_id, cd.revision + from "constraint" c join constraint_definition cd on c.id = cd.constraint_id + where c.model_id is not null; + +insert into constraint_specification(plan_id, constraint_id, constraint_revision, enabled) +select c.plan_id, cd.constraint_id, cd.revision, true + from "constraint" c join constraint_definition cd on c.id = cd.constraint_id + where c.plan_id is not null; + +-- Additionally pull the model specs in to the plan specs +insert into constraint_specification (plan_id, constraint_id, constraint_revision, enabled) +select p.id, cms.constraint_id, cms.constraint_revision, true + from constraint_model_specification cms join plan p using (model_id); + +-- Remove "by default" generation now that we have moved the existing constraints +alter table constraint_metadata alter column id set generated always; + +-- Add timestamp trigger for future entries +create function constraint_metadata_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp +before update on constraint_metadata +for each row +execute function constraint_metadata_set_updated_at(); + +/****** CONSTRAINT RUN ******/ +-- Clear cache +truncate table constraint_run; + +-- Update constraint_run to reference new table +alter table constraint_run +drop constraint constraint_run_key, +drop constraint constraint_run_to_constraint, +drop column constraint_definition, +drop column definition_outdated, +add column constraint_revision integer not null, +add constraint constraint_run_to_constraint_definition + foreign key (constraint_id, constraint_revision) + references constraint_definition + on delete cascade, +add constraint constraint_run_key + primary key (constraint_id, constraint_revision, simulation_dataset_id); + + comment on column constraint_run.constraint_revision is e'' + 'The version of the constraint definition that was checked.'; + +/****** TAGS *******/ +alter table metadata.constraint_tags drop constraint constraint_tags_constraint_id_fkey; +alter table metadata.constraint_tags add foreign key (constraint_id) + references public.constraint_metadata + on update cascade + on delete cascade; + +create table metadata.constraint_definition_tags ( + constraint_id integer not null, + constraint_revision integer not null, + tag_id integer not null references metadata.tags + on update cascade + on delete cascade, + primary key (constraint_id, constraint_revision, tag_id), + foreign key (constraint_id, constraint_revision) references constraint_definition + on update cascade + on delete cascade +); +comment on table metadata.constraint_definition_tags is e'' + 'The tags associated with a specific constraint defintion.'; + + +/**** DROP CONSTRAINTS ****/ +drop trigger set_timestamp on "constraint"; +drop trigger constraint_check_constraint_run_trigger on "constraint"; + +drop function constraint_set_updated_at(); +drop function constraint_check_constraint_run(); + +drop table "constraint"; + +call migrations.mark_migration_applied('38'); diff --git a/merlin-server/sql/merlin/applied_migrations.sql b/merlin-server/sql/merlin/applied_migrations.sql index fecaa4df73..91a0125aa7 100644 --- a/merlin-server/sql/merlin/applied_migrations.sql +++ b/merlin-server/sql/merlin/applied_migrations.sql @@ -40,3 +40,4 @@ call migrations.mark_migration_applied('34'); call migrations.mark_migration_applied('35'); call migrations.mark_migration_applied('36'); call migrations.mark_migration_applied('37'); +call migrations.mark_migration_applied('38'); From e30efee811abc094a56ada2984b2ab4121a6d1d6 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 10 Jan 2024 10:55:35 -0800 Subject: [PATCH 068/159] Update DBTests - Update Tests in TagsTests - Add functions in TagsTests for applying/fetching tags on a constraint definition - Update inserting a constraint in MerlinDBTestHelper --- .../database/MerlinDatabaseTestHelper.java | 21 +++-- .../nasa/jpl/aerie/database/TagsTests.java | 82 +++++++++++++++---- 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java index 507f506284..0623c96d2d 100644 --- a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java +++ b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/MerlinDatabaseTestHelper.java @@ -210,7 +210,7 @@ void assignPreset(int presetId, int activityId, int planId, String userSession) } } - void unassignPreset(int presetId, int activityId, int planId) throws SQLException { +void unassignPreset(int presetId, int activityId, int planId) throws SQLException { try(final var statement = connection.createStatement()){ statement.execute( //language=sql @@ -222,17 +222,22 @@ void unassignPreset(int presetId, int activityId, int planId) throws SQLExceptio } - int insertConstraintPlan(int plan_id, String name, String definition, User user) throws SQLException { + int insertConstraint(String name, String definition, User user) throws SQLException { try(final var statement = connection.createStatement()) { final var res = statement.executeQuery( """ - INSERT INTO public.constraint - (name, description, definition, plan_id, owner, updated_by) - VALUES ('%s', 'Merlin DB Test Constraint', '%s', %d, '%s', '%s') - RETURNING id; - """.formatted(name, definition, plan_id, user.name, user.name)); + WITH metadata(id, owner) AS ( + INSERT INTO public.constraint_metadata(name, description, owner, updated_by) + VALUES ('%s', 'Merlin DB Test Constraint', '%s', '%s') + RETURNING id, owner + ) + INSERT INTO public.constraint_definition(constraint_id, definition, author) + SELECT m.id, '%s', m.owner + FROM metadata m + RETURNING constraint_id; + """.formatted(name, user.name, user.name, definition)); res.next(); - return res.getInt("id"); + return res.getInt("constraint_id"); } } } diff --git a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/TagsTests.java b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/TagsTests.java index f985092819..d74d359644 100644 --- a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/TagsTests.java +++ b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/TagsTests.java @@ -63,6 +63,7 @@ void afterEach() throws SQLException { helper.clearTable("metadata.tags"); helper.clearTable("metadata.activity_directive_tags"); helper.clearTable("metadata.constraint_tags"); + helper.clearTable("metadata.constraint_definition_tags"); helper.clearTable("metadata.snapshot_activity_tags"); } @@ -327,14 +328,24 @@ void assignTagToConstraint(int constraint_id, int tag_id) throws SQLException { } } + void assignTagToConstraintRevision(int constraint_id, int constraint_revision, int tag_id) throws SQLException { + try (final var statement = connection.createStatement()) { + statement.execute( + """ + INSERT INTO metadata.constraint_definition_tags (constraint_id, constraint_revision, tag_id) + VALUES (%d, %d, %d) + """.formatted(constraint_id, constraint_revision, tag_id)); + } + } + void removeTagFromConstraint(int constraint_id, int tag_id) throws SQLException { try (final var statement = connection.createStatement()) { statement.execute( """ - DELETE FROM metadata.constraint_tags - WHERE constraint_id = %d - AND tag_id = %d; - """.formatted(constraint_id, tag_id)); + DELETE FROM metadata.constraint_tags + WHERE constraint_id = %d + AND tag_id = %d; + """.formatted(constraint_id, tag_id)); } } @@ -343,12 +354,35 @@ ArrayList getTagsOnConstraint(int constraint_id) throws SQLException { final var tags = new ArrayList(); final var res = statement.executeQuery( """ - SELECT id, name, color, owner - FROM metadata.tags t, metadata.constraint_tags ct - WHERE ct.tag_id = t.id - AND ct.constraint_id = %d - ORDER BY id; - """.formatted(constraint_id)); + SELECT id, name, color, owner + FROM metadata.tags t, metadata.constraint_tags ct + WHERE ct.tag_id = t.id + AND ct.constraint_id = %d + ORDER BY id; + """.formatted(constraint_id)); + while (res.next()) { + tags.add(new Tag( + res.getInt("id"), + res.getString("name"), + res.getString("color"), + res.getString("owner"))); + } + return tags; + } + } + + ArrayList getTagsOnConstraintRevision(int constraint_id, int constraint_revision) throws SQLException { + try (final var statement = connection.createStatement()) { + final var tags = new ArrayList(); + final var res = statement.executeQuery( + """ + SELECT id, name, color, owner + FROM metadata.tags t, metadata.constraint_definition_tags ct + WHERE ct.tag_id = t.id + AND ct.constraint_id = %d + AND ct.constraint_revision = %d + ORDER BY id; + """.formatted(constraint_id, constraint_revision)); while (res.next()) { tags.add(new Tag( res.getInt("id"), @@ -385,7 +419,7 @@ void removeTagFromActivityType(int modelId, String name) throws SQLException { WHERE model_id = %d AND name = '%s' RETURNING subsystem - """.formatted(missionModelId, "GrowBanana")); + """.formatted(modelId, name)); assertTrue(res.next()); assertNull(res.getObject("subsystem")); assertFalse(res.next()); @@ -402,17 +436,19 @@ void tagsAssociateCorrectly() throws SQLException { final var secondTagId = insertTag("Banana"); final var planId = merlinHelper.insertPlan(missionModelId); final var activityId = merlinHelper.insertActivity(planId); - final var constraintId = merlinHelper.insertConstraintPlan(planId, "Test Constraint", constraintDefinition, tagsUser); + final var constraintId = merlinHelper.insertConstraint("Test Constraint", constraintDefinition, tagsUser); assignTagToPlan(planId, tagId); assignTagToActivity(activityId, planId, tagId); assignTagToConstraint(constraintId, tagId); + assignTagToConstraintRevision(constraintId, 0, tagId); assignTagToPlan(planId, secondTagId); final var planTags = getTagsOnPlan(planId); final var activityTags = getTagsOnActivity(activityId, planId); final var constraintTags = getTagsOnConstraint(constraintId); + final var constraintRevisionTags = getTagsOnConstraintRevision(constraintId, 0); final ArrayList expected = new ArrayList<>(); expected.add(new Tag(tagId, "Farm", null, "TagsTest")); @@ -424,6 +460,7 @@ void tagsAssociateCorrectly() throws SQLException { assertEquals(expectedPlan, planTags); assertEquals(expected, activityTags); assertEquals(expected, constraintTags); + assertEquals(expected, constraintRevisionTags); } @Test @@ -431,11 +468,12 @@ void tagsDissociateCorrectly() throws SQLException { final var secondTagId = insertTag("Banana"); final var planId = merlinHelper.insertPlan(missionModelId); final var activityId = merlinHelper.insertActivity(planId); - final var constraintId = merlinHelper.insertConstraintPlan(planId, "Test Constraint", constraintDefinition, tagsUser); + final var constraintId = merlinHelper.insertConstraint("Test Constraint", constraintDefinition, tagsUser); assignTagToPlan(planId, tagId); assignTagToActivity(activityId, planId, tagId); assignTagToConstraint(constraintId, tagId); + assignTagToConstraintRevision(constraintId, 0, tagId); assignTagToPlan(planId, secondTagId); @@ -444,6 +482,7 @@ void tagsDissociateCorrectly() throws SQLException { final var planTags = getTagsOnPlan(planId); final var activityTags = getTagsOnActivity(activityId, planId); final var constraintTags = getTagsOnActivity(activityId, planId); + final var constraintRevisionTags = getTagsOnConstraintRevision(constraintId, 0); final var expected = new ArrayList(1); expected.add(new Tag(tagId, "Farm", null, tagsUser.name())); @@ -454,6 +493,7 @@ void tagsDissociateCorrectly() throws SQLException { assertEquals(expectedPlan, planTags); assertEquals(expected, activityTags); assertEquals(expected, constraintTags); + assertEquals(expected, constraintRevisionTags); } @Test @@ -477,7 +517,7 @@ void cannotApplyNonexistentTag() throws SQLException { } } // Constraint - final var constraintId = merlinHelper.insertConstraintPlan(planId, "Test Constraint", constraintDefinition, tagsUser); + final var constraintId = merlinHelper.insertConstraint("Test Constraint", constraintDefinition, tagsUser); try { assignTagToConstraint(constraintId, -1); } catch (SQLException e) { @@ -485,6 +525,14 @@ void cannotApplyNonexistentTag() throws SQLException { throw e; } } + // Constraint Revision + try { + assignTagToConstraintRevision(constraintId, 0, -1); + } catch (SQLException e) { + if(!e.getMessage().contains("insert or update on table \"constraint_definition_tags\" violates foreign key constraint \"constraint_definition_tags_tag_id_fkey\"")) { + throw e; + } + } // Activity Type merlinHelper.insertActivityType(missionModelId, "GrowBanana"); try { @@ -501,21 +549,24 @@ void tagDeleteCascadesInMerlin() throws SQLException { final var secondTagId = insertTag("Banana"); final var planId = merlinHelper.insertPlan(missionModelId); final var activityId = merlinHelper.insertActivity(planId); - final var constraintId = merlinHelper.insertConstraintPlan(planId, "Test Constraint", constraintDefinition, tagsUser); + final var constraintId = merlinHelper.insertConstraint("Test Constraint", constraintDefinition, tagsUser); assignTagToPlan(planId, tagId); assignTagToActivity(activityId, planId, tagId); assignTagToConstraint(constraintId, tagId); + assignTagToConstraintRevision(constraintId, 0, tagId); assignTagToPlan(planId, secondTagId); assignTagToActivity(activityId, planId, secondTagId); assignTagToConstraint(constraintId, secondTagId); + assignTagToConstraintRevision(constraintId, 0, secondTagId); deleteTag(tagId); final var planTags = getTagsOnPlan(planId); final var activityTags = getTagsOnActivity(activityId, planId); final var constraintTags = getTagsOnConstraint(constraintId); + final var constraintRevisionTags = getTagsOnConstraintRevision(constraintId, 0); final ArrayList expected = new ArrayList<>(); expected.add(new Tag(secondTagId, "Banana", null, tagsUser.name())); @@ -523,6 +574,7 @@ void tagDeleteCascadesInMerlin() throws SQLException { assertEquals(expected, planTags); assertEquals(expected, activityTags); assertEquals(expected, constraintTags); + assertEquals(expected, constraintRevisionTags); } @Test From 15d92c77ac20c43347e50f9b639956793e61802a Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 10 Jan 2024 13:50:48 -0800 Subject: [PATCH 069/159] Update Hasura Metadata --- deployment/hasura/metadata/actions.graphql | 2 +- .../metadata/constraint_definition_tags.yaml | 50 +++++++++ .../tables/metadata/constraint_tags.yaml | 6 +- .../AerieMerlin/tables/public_constraint.yaml | 86 --------------- .../tables/public_constraint_definition.yaml | 100 +++++++++++++++++ .../tables/public_constraint_metadata.yaml | 102 ++++++++++++++++++ ...public_constraint_model_specification.yaml | 70 ++++++++++++ .../tables/public_constraint_run.yaml | 15 ++- .../public_constraint_specification.yaml | 63 +++++++++++ .../tables/public_mission_model.yaml | 4 +- .../AerieMerlin/tables/public_plan.yaml | 4 +- .../databases/AerieMerlin/tables/tables.yaml | 6 +- 12 files changed, 411 insertions(+), 97 deletions(-) create mode 100644 deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_definition_tags.yaml delete mode 100644 deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint.yaml create mode 100644 deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_definition.yaml create mode 100644 deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_metadata.yaml create mode 100644 deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_model_specification.yaml create mode 100644 deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_specification.yaml diff --git a/deployment/hasura/metadata/actions.graphql b/deployment/hasura/metadata/actions.graphql index 8349d072b6..436134dabc 100644 --- a/deployment/hasura/metadata/actions.graphql +++ b/deployment/hasura/metadata/actions.graphql @@ -281,8 +281,8 @@ type ResourceSamplesResponse { type ConstraintResponse { success: String! constraintId: Int!, + constraintRevision: Int!, constraintName: String!, - type: String!, errors: [UserCodeError!]! results: [ConstraintResult!]! } diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_definition_tags.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_definition_tags.yaml new file mode 100644 index 0000000000..cefc7b2a8b --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_definition_tags.yaml @@ -0,0 +1,50 @@ +table: + name: constraint_definition_tags + schema: metadata +configuration: + custom_name: "constraint_definition_tags" +object_relationships: + - name: constraint_definition + using: + foreign_key_constraint_on: + - constraint_id + - constraint_revision + - name: tag + using: + foreign_key_constraint_on: tag_id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [constraint_id, constraint_revision, tag_id] + check: {} + - role: user + permission: + columns: [constraint_id, constraint_revision, tag_id] + check: {"constraint_definition":{"_or":[ + {"author":{"_eq":"X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]}} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: {"constraint_definition":{"_or":[ + {"author":{"_eq":"X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]}} diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_tags.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_tags.yaml index 5a4bce2311..bab682fd06 100644 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_tags.yaml +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/metadata/constraint_tags.yaml @@ -4,7 +4,7 @@ table: configuration: custom_name: "constraint_tags" object_relationships: - - name: constraint + - name: constraint_metadata using: foreign_key_constraint_on: constraint_id - name: tag @@ -34,11 +34,11 @@ insert_permissions: - role: user permission: columns: [constraint_id, tag_id] - check: {"constraint":{"owner":{"_eq":"X-Hasura-User-Id"}}} + check: {"constraint_metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}} delete_permissions: - role: aerie_admin permission: filter: {} - role: user permission: - filter: {"constraint":{"owner":{"_eq":"X-Hasura-User-Id"}}} + filter: {"constraint_metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}} diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint.yaml deleted file mode 100644 index f2333bac42..0000000000 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint.yaml +++ /dev/null @@ -1,86 +0,0 @@ -table: - name: constraint - schema: public -object_relationships: - - name: plan - using: - foreign_key_constraint_on: plan_id - - name: mission_model - using: - foreign_key_constraint_on: model_id -array_relationships: - - name: tags - using: - foreign_key_constraint_on: - column: constraint_id - table: - name: constraint_tags - schema: metadata -select_permissions: - - role: aerie_admin - permission: - columns: '*' - filter: {} - allow_aggregations: true - - role: user - permission: - columns: '*' - filter: {} - allow_aggregations: true - - role: viewer - permission: - columns: '*' - filter: {} - allow_aggregations: true -insert_permissions: - - role: aerie_admin - permission: - columns: [name, description, definition, plan_id, model_id] - check: {} - set: - owner: "x-hasura-user-id" - updated_by: "x-hasura-user-id" - - role: user - permission: - columns: [name, description, definition, plan_id, model_id] - check: {"_or": [ - {"plan":{"owner":{"_eq":"X-Hasura-User-Id"}}}, - {"plan":{"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}}, - {"mission_model":{"plans":{"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}}}, - {"mission_model":{"plans":{"owner":{"_eq":"X-Hasura-User-Id"}}}}]} - set: - owner: "x-hasura-user-id" - updated_by: "x-hasura-user-id" -update_permissions: - - role: aerie_admin - permission: - columns: [name, description, definition, owner, model_id, plan_id] - filter: {} - set: - updated_by: "x-hasura-user-id" - - role: user - permission: - columns: [name, description, definition, owner, model_id, plan_id] - filter: {"_or": [ - {"plan":{"owner":{"_eq":"X-Hasura-User-Id"}}}, - {"plan":{"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}}, - {"mission_model":{"plans":{"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}}}, - {"mission_model":{"plans":{"owner":{"_eq":"X-Hasura-User-Id"}}}}]} - check: { "_or": [ - { "plan": { "owner": { "_eq": "X-Hasura-User-Id" } } }, - { "plan": { "collaborators": { "collaborator": { "_eq": "X-Hasura-User-Id" } } } }, - { "mission_model": { "plans": { "collaborators": { "collaborator": { "_eq": "X-Hasura-User-Id" } } } } }, - { "mission_model": { "plans": { "owner": { "_eq": "X-Hasura-User-Id" } } } } ] } - set: - updated_by: "x-hasura-user-id" -delete_permissions: - - role: aerie_admin - permission: - filter: {} - - role: user - permission: - filter: {"_or": [ - {"plan":{"owner":{"_eq":"X-Hasura-User-Id"}}}, - {"plan":{"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}}, - {"mission_model":{"plans":{"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}}}, - {"mission_model":{"plans":{"owner":{"_eq":"X-Hasura-User-Id"}}}}]} diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_definition.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_definition.yaml new file mode 100644 index 0000000000..8ada554efc --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_definition.yaml @@ -0,0 +1,100 @@ +table: + name: constraint_definition + schema: public +object_relationships: + - name: metadata + using: + foreign_key_constraint_on: constraint_id +array_relationships: + - name: models_using + using: + foreign_key_constraint_on: + columns: + - constraint_id + - constraint_revision + table: + name: constraint_model_specification + schema: public + - name: plans_using + using: + foreign_key_constraint_on: + columns: + - constraint_id + - constraint_revision + table: + name: constraint_specification + schema: public + - name: tags + using: + foreign_key_constraint_on: + columns: + - constraint_id + - constraint_revision + table: + name: constraint_definition_tags + schema: metadata +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' +# Select is allowed if: +# - the constraint is public, +# - the user is the owner, or +# - the user has permission to edit a constraint specification it is on, or +# - the constraint is on their plan's model spec + filter: {"metadata": {"_or":[ + {"public":{"_eq":true}}, + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"plans_using":{"plan":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}]}}}, + {"models_using":{"model":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"plans":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}]}}]}}}]}} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {"metadata": {"_or":[ + {"public":{"_eq":true}}, + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"plans_using":{"plan":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}]}}}, + {"models_using":{"model":{"owner":{"_eq":"X-Hasura-User-Id"}}}}]}} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [constraint_id, definition] + check: {} + set: + author: "x-hasura-user-id" + - role: user + permission: + columns: [constraint_id, definition] + check: {"_or":[{"metadata":{"public":{"_eq":true}}},{"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]} + set: + author: "x-hasura-user-id" +update_permissions: + - role: aerie_admin + permission: + columns: [definition, author] + filter: {} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: + {"_or":[ + {"author": {"_eq": "X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]} diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_metadata.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_metadata.yaml new file mode 100644 index 0000000000..ef244a6b12 --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_metadata.yaml @@ -0,0 +1,102 @@ +table: + name: constraint_metadata + schema: public +array_relationships: + - name: tags + using: + foreign_key_constraint_on: + column: constraint_id + table: + name: constraint_tags + schema: metadata + - name: versions + using: + foreign_key_constraint_on: + column: constraint_id + table: constraint_definition + schema: public + - name: models_using + using: + foreign_key_constraint_on: + column: constraint_id + table: constraint_model_specification + schema: public + - name: plans_using + using: + foreign_key_constraint_on: + column: constraint_id + table: constraint_specification + schema: public +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' +# Select is allowed if: +# - the constraint is public, +# - the user is the owner, or +# - the user has permission to edit a constraint specification it is on, or +# - the constraint is on their plan's model spec + filter: {"_or":[ + {"public":{"_eq":true}}, + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"plans_using":{"plan":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}]}}}, + {"models_using":{"model":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"plans":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}]}}]}}}]} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {"_or":[ + {"public":{"_eq":true}}, + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"plans_using":{"plan":{"_or":[ + {"owner":{"_eq":"X-Hasura-User-Id"}}, + {"collaborators":{"collaborator":{"_eq":"X-Hasura-User-Id"}}}]}}}, + {"models_using":{"model":{"owner":{"_eq":"X-Hasura-User-Id"}}}}]} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [name, description, public] + check: {} + set: + owner: "x-hasura-user-id" + updated_by: "x-hasura-user-id" + - role: user + permission: + columns: [name, description, public] + check: {} + set: + owner: "x-hasura-user-id" + updated_by: "x-hasura-user-id" +update_permissions: + - role: aerie_admin + permission: + columns: [name, description, public, owner] + filter: {} + set: + updated_by: "x-hasura-user-id" + - role: user + permission: + columns: [name, description, public, owner] + filter: { "owner": { "_eq": "X-Hasura-User-Id" } } + check: {} + set: + updated_by: "x-hasura-user-id" +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: { "owner": {"_eq": "X-Hasura-User-Id"} } diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_model_specification.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_model_specification.yaml new file mode 100644 index 0000000000..36a98060ca --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_model_specification.yaml @@ -0,0 +1,70 @@ +table: + name: constraint_model_specification + schema: public +object_relationships: + - name: model + using: + foreign_key_constraint_on: model_id + - name: constraint_metadata + using: + manual_configuration: + column_mapping: + constraint_id: id + insertion_order: null + remote_table: + name: constraint_metadata + schema: public + - name: constraint_definition + using: + manual_configuration: + column_mapping: + constraint_id: constraint_id + constraint_revision: revision + insertion_order: null + remote_table: + name: constraint_definition + schema: public +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [model_id, constraint_id, constraint_revision] + check: {} + - role: user + permission: + columns: [model_id, constraint_id, constraint_revision] + check: { "_and": [ + { "model": { "owner": { "_eq": "X-Hasura-User-Id"} } }, + { "constraint_metadata": { "_or": [ { "public": { "_eq": true } }, + { "owner": { "_eq": "X-Hasura-User-Id" } } ] } } ] } +update_permissions: + - role: aerie_admin + permission: + columns: [constraint_revision] + filter: {} + - role: user + permission: + columns: [constraint_revision] + filter: {"model": {"owner": {"_eq": "X-Hasura-User-Id"}}} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: {"model": {"owner": {"_eq": "X-Hasura-User-Id"}}} diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_run.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_run.yaml index 3b7082388c..dba2b08d88 100644 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_run.yaml +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_run.yaml @@ -2,9 +2,20 @@ table: name: constraint_run schema: public object_relationships: - - name: constraint + - name: constraint_definition using: - foreign_key_constraint_on: constraint_id + foreign_key_constraint_on: + - constraint_id + - constraint_revision + - name: constraint_metadata + using: + manual_configuration: + column_mapping: + constraint_id: id + insertion_order: null + remote_table: + name: constraint_metadata + schema: public - name: simulation_dataset using: foreign_key_constraint_on: simulation_dataset_id diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_specification.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_specification.yaml new file mode 100644 index 0000000000..5635e7cce4 --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_constraint_specification.yaml @@ -0,0 +1,63 @@ +table: + name: constraint_specification + schema: public +object_relationships: + - name: plan + using: + foreign_key_constraint_on: plan_id + - name: constraint_metadata + using: + foreign_key_constraint_on: constraint_id + - name: constraint_definition + using: + foreign_key_constraint_on: + - constraint_id + - constraint_revision +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [plan_id, constraint_id, constraint_revision, enabled] + check: {} + - role: user + permission: + columns: [plan_id, constraint_id, constraint_revision, enabled] + check: { "_and": [ + # User is a plan owner/collaborator + { "plan": { "_or": [ { "owner": { "_eq": "X-Hasura-User-Id" } }, + { "collaborators": { "collaborator": { "_eq": "X-Hasura-User-Id" } } } ] } }, + # The constraint is public, the user is the constraint owner, or the constraint is on the plan's model constraint specification + { "constraint_metadata": { "_or": [ { "public": { "_eq": true } }, + { "owner": { "_eq": "X-Hasura-User-Id" } }, + { "models_using": { "model": { "plans": { "id": { "_ceq": ["$","plan_id"] } } } } } ] } } ] } +update_permissions: + - role: aerie_admin + permission: + columns: [constraint_revision, enabled] + filter: {} + - role: user + permission: + columns: [constraint_revision, enabled] + filter: { "plan": { "_or": [ { "owner": { "_eq": "X-Hasura-User-Id" } },{ "collaborators": { "collaborator": { "_eq": "X-Hasura-User-Id" }}}]}} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: { "plan": { "_or": [ { "owner": { "_eq": "X-Hasura-User-Id" } },{ "collaborators": { "collaborator": { "_eq": "X-Hasura-User-Id" }}}]}} diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_mission_model.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_mission_model.yaml index fa3eda5c8e..4b7ace8a04 100644 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_mission_model.yaml +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_mission_model.yaml @@ -20,12 +20,12 @@ array_relationships: table: name: activity_type schema: public -- name: constraints +- name: constraint_specification using: foreign_key_constraint_on: column: model_id table: - name: constraint + name: constraint_model_specification schema: public - name: plans using: diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml index 81346d3c27..4c014fc640 100644 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml @@ -22,12 +22,12 @@ array_relationships: table: name: activity_directive schema: public -- name: constraints +- name: constraint_specification using: foreign_key_constraint_on: column: plan_id table: - name: constraint + name: constraint_specification schema: public - name: collaborators using: diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/tables.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/tables.yaml index 86c854428e..2dd25b6456 100644 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/tables.yaml +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/tables.yaml @@ -7,8 +7,11 @@ - "!include public_activity_type.yaml" - "!include public_anchor_validation_status.yaml" - "!include public_conflicting_activities.yaml" -- "!include public_constraint.yaml" +- "!include public_constraint_definition.yaml" +- "!include public_constraint_metadata.yaml" +- "!include public_constraint_model_specification.yaml" - "!include public_constraint_run.yaml" +- "!include public_constraint_specification.yaml" - "!include public_dataset.yaml" - "!include public_event.yaml" - "!include public_merge_request.yaml" @@ -51,6 +54,7 @@ # Metadata - "!include metadata/tags.yaml" - "!include metadata/activity_directive_tags.yaml" +- "!include metadata/constraint_definition_tags.yaml" - "!include metadata/constraint_tags.yaml" - "!include metadata/plan_snapshot_tags.yaml" - "!include metadata/plan_tags.yaml" From 8afa30559e48240c17892e5769d54e1b7b73d848 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 10 Jan 2024 12:11:44 -0800 Subject: [PATCH 070/159] Update ConstraintsResults - remove ConstraintType - add constraintRevision - remove MissionModelRepository.getConstraints - rename getAllConstraintsInPlan to getPlanConstraints --- .../constraints/json/ConstraintParsers.java | 7 +-- .../constraints/model/ConstraintResult.java | 10 ++-- .../constraints/model/ConstraintType.java | 7 --- .../server/http/ResponseSerializers.java | 8 +-- .../server/mocks/InMemoryPlanRepository.java | 2 +- .../merlin/server/models/Constraint.java | 4 +- .../remotes/MissionModelRepository.java | 1 - .../merlin/server/remotes/PlanRepository.java | 2 +- .../postgres/GetModelConstraintsAction.java | 57 ------------------- .../PostgresMissionModelRepository.java | 25 -------- .../postgres/PostgresPlanRepository.java | 3 +- .../server/services/ConstraintAction.java | 12 +--- .../services/LocalMissionModelService.java | 9 --- .../server/services/LocalPlanService.java | 2 +- .../server/services/MissionModelService.java | 3 - .../server/mocks/StubMissionModelService.java | 5 -- 16 files changed, 19 insertions(+), 138 deletions(-) delete mode 100644 constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintType.java delete mode 100644 merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetModelConstraintsAction.java diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java index 69ac2ace80..728e513d6c 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/json/ConstraintParsers.java @@ -1,6 +1,5 @@ package gov.nasa.jpl.aerie.constraints.json; -import gov.nasa.jpl.aerie.constraints.model.ConstraintType; import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; import gov.nasa.jpl.aerie.constraints.model.LinearProfile; import gov.nasa.jpl.aerie.constraints.model.Profile; @@ -323,13 +322,13 @@ static JsonParser keepTrueSegmentP(JsonParser new ConstraintResult(violations, gaps, constraintType, resourceNames, constraintId, constraintName)), - $ -> tuple($.violations, $.gaps, $.constraintType, $.resourceIds, $.constraintId, $.constraintName) + untuple((violations, gaps, resourceNames, constraintId, constraintRevision, constraintName) -> new ConstraintResult(violations, gaps, resourceNames, constraintId, constraintRevision, constraintName)), + $ -> tuple($.violations, $.gaps, $.resourceIds, $.constraintId, $.constraintRevision, $.constraintName) ); static final JsonParser intervalDurationP = diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintResult.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintResult.java index 41593a3733..958ebfc964 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintResult.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintResult.java @@ -12,9 +12,9 @@ public final class ConstraintResult { public final List gaps; // The rest will be initialized after AST evaluation by the constraints action. - public ConstraintType constraintType; public List resourceIds; public Long constraintId; + public Long constraintRevision; public String constraintName; public ConstraintResult() { @@ -29,16 +29,16 @@ public ConstraintResult(List violations, List gaps) { public ConstraintResult( final List violations, final List gaps, - final ConstraintType constraintType, final List resourceIds, final Long constraintId, + final Long constraintRevision, final String constraintName ) { this.violations = violations; this.gaps = gaps; - this.constraintType = constraintType; this.resourceIds = resourceIds; this.constraintId = constraintId; + this.constraintRevision = constraintRevision; this.constraintName = constraintName; } @@ -70,14 +70,14 @@ public boolean equals(final Object o) { ConstraintResult that = (ConstraintResult) o; return violations.equals(that.violations) && gaps.equals(that.gaps) - && constraintType == that.constraintType && Objects.equals(resourceIds, that.resourceIds) && Objects.equals(constraintId, that.constraintId) + && Objects.equals(constraintRevision, that.constraintRevision) && Objects.equals(constraintName, that.constraintName); } @Override public int hashCode() { - return Objects.hash(violations, gaps, constraintType, resourceIds, constraintId, constraintName); + return Objects.hash(violations, gaps, resourceIds, constraintId, constraintRevision, constraintName); } } diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintType.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintType.java deleted file mode 100644 index d414de8f26..0000000000 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/ConstraintType.java +++ /dev/null @@ -1,7 +0,0 @@ -package gov.nasa.jpl.aerie.constraints.model; - -// This will go away with https://github.com/NASA-AMMOS/aerie/issues/892 when there's only 1 type of constraint. -public enum ConstraintType { - model, - plan -} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java index 560727f92d..6935fc4604 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java @@ -326,7 +326,7 @@ public static JsonValue serializeConstraintResults(final Map getAllConstraintsInPlan(final PlanId planId) { + public Map getPlanConstraints(final PlanId planId) { return Map.of(); } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/Constraint.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/Constraint.java index 0e425c1e93..d9c41b3e26 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/Constraint.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/Constraint.java @@ -1,6 +1,4 @@ package gov.nasa.jpl.aerie.merlin.server.models; -import gov.nasa.jpl.aerie.constraints.model.ConstraintType; - -public record Constraint (Long id, String name, String description, String definition, ConstraintType type) { +public record Constraint (Long id, Long revision, String name, String description, String definition) { } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/MissionModelRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/MissionModelRepository.java index f19b5d3b71..506f6739f7 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/MissionModelRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/MissionModelRepository.java @@ -17,7 +17,6 @@ public interface MissionModelRepository { // Queries Map getAllMissionModels(); MissionModelJar getMissionModel(String id) throws NoSuchMissionModelException; - Map getConstraints(String missionModelId) throws NoSuchMissionModelException; Map getActivityTypes(String missionModelId) throws NoSuchMissionModelException; // Mutations diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/PlanRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/PlanRepository.java index 32915e5b3f..7eabd94ffe 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/PlanRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/PlanRepository.java @@ -35,7 +35,7 @@ public interface PlanRepository { long getPlanRevision(PlanId planId) throws NoSuchPlanException; RevisionData getPlanRevisionData(PlanId planId) throws NoSuchPlanException; - Map getAllConstraintsInPlan(PlanId planId) throws NoSuchPlanException; + Map getPlanConstraints(PlanId planId) throws NoSuchPlanException; long addExternalDataset( PlanId planId, diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetModelConstraintsAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetModelConstraintsAction.java deleted file mode 100644 index 2d38e5a8d1..0000000000 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetModelConstraintsAction.java +++ /dev/null @@ -1,57 +0,0 @@ -package gov.nasa.jpl.aerie.merlin.server.remotes.postgres; - -import org.intellij.lang.annotations.Language; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/*package-local*/ final class GetModelConstraintsAction implements AutoCloseable { - // We left join through the mission_model table in order to distinguish - // a mission model without any constraints from a non-existent mission model. - // A mission model without constraints will produce a placeholder row with nulls. - private static final @Language("SQL") String sql = """ - select c.id, c.name, c.description, c.definition - from mission_model AS m - left join "constraint" AS c - on m.id = c.model_id - where m.id = ? - """; - - private final PreparedStatement statement; - - public GetModelConstraintsAction(final Connection connection) throws SQLException { - this.statement = connection.prepareStatement(sql); - } - - public Optional> get(final long modelId) throws SQLException { - this.statement.setLong(1, modelId); - - try (final var results = this.statement.executeQuery()) { - if (!results.next()) return Optional.empty(); - - final var constraints = new ArrayList(); - do { - if (results.getObject(1) == null) continue; - - final var constraint = new ConstraintRecord( - results.getLong(1), - results.getString(2), - results.getString(3), - results.getString(4)); - - constraints.add(constraint); - } while (results.next()); - - return Optional.of(constraints); - } - } - - @Override - public void close() throws SQLException { - this.statement.close(); - } -} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresMissionModelRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresMissionModelRepository.java index c5bf12b48c..530c9fed15 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresMissionModelRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresMissionModelRepository.java @@ -1,11 +1,9 @@ package gov.nasa.jpl.aerie.merlin.server.remotes.postgres; -import gov.nasa.jpl.aerie.constraints.model.ConstraintType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.model.Resource; import gov.nasa.jpl.aerie.merlin.server.models.ActivityDirectiveForValidation; import gov.nasa.jpl.aerie.merlin.server.models.ActivityType; -import gov.nasa.jpl.aerie.merlin.server.models.Constraint; import gov.nasa.jpl.aerie.merlin.server.models.MissionModelId; import gov.nasa.jpl.aerie.merlin.server.models.MissionModelJar; import gov.nasa.jpl.aerie.merlin.server.remotes.MissionModelRepository; @@ -57,29 +55,6 @@ public MissionModelJar getMissionModel(final String missionModelId) throws NoSuc } } - @Override - public Map getConstraints(final String missionModelId) throws NoSuchMissionModelException { - try (final var connection = this.dataSource.getConnection()) { - try (final var getModelConstraintsAction = new GetModelConstraintsAction(connection)) { - return getModelConstraintsAction - .get(toMissionModelId(missionModelId)) - .orElseThrow(NoSuchMissionModelException::new) - .stream() - .collect(Collectors.toMap( - ConstraintRecord::id, - r -> new Constraint( - r.id(), - r.name(), - r.description(), - r.definition(), - ConstraintType.model))); - } - } catch (final SQLException ex) { - throw new DatabaseException( - "Failed to retrieve constraints for mission model with id `%s`".formatted(missionModelId), ex); - } - } - @Override public Map getActivityTypes(final String missionModelId) throws NoSuchMissionModelException { try (final var connection = this.dataSource.getConnection()) { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java index 1d68e0adca..42307753c8 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java @@ -1,6 +1,5 @@ package gov.nasa.jpl.aerie.merlin.server.remotes.postgres; -import gov.nasa.jpl.aerie.constraints.model.ConstraintType; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; @@ -180,7 +179,7 @@ public PostgresPlanRevisionData getPlanRevisionData(final PlanId planId) throws } @Override - public Map getAllConstraintsInPlan(final PlanId planId) throws NoSuchPlanException { + public Map getPlanConstraints(final PlanId planId) throws NoSuchPlanException { try (final var connection = this.dataSource.getConnection()) { try (final var getPlanConstraintsAction = new GetPlanConstraintsAction(connection)) { return getPlanConstraintsAction diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java index 7826d3b66f..43a8ce3633 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java @@ -60,17 +60,9 @@ public Map> getViolations(final PlanId planId, final Opt + " has not yet been simulated at its current revision")); } - final var constraintCode = new HashMap(); + final var constraintCode = new HashMap<>(this.planService.getConstraintsForPlan(planId));; final var constraintResultMap = new HashMap>(); - try { - constraintCode.putAll(this.missionModelService.getConstraints(plan.missionModelId)); - constraintCode.putAll(this.planService.getConstraintsForPlan(planId)); - } catch (final MissionModelService.NoSuchMissionModelException ex) { - throw new RuntimeException("Assumption falsified -- mission model for existing plan does not exist"); - } - - final var validConstraintRuns = this.constraintService.getValidConstraintRuns(constraintCode .values() .stream() @@ -238,8 +230,8 @@ public Map> getViolations(final PlanId planId, final Opt ConstraintResult constraintResult = expression.evaluate(preparedResults, environment); constraintResult.constraintName = entry.getValue().name(); + constraintResult.constraintRevision = entry.getValue().revision(); constraintResult.constraintId = entry.getKey(); - constraintResult.constraintType = entry.getValue().type(); constraintResult.resourceIds = List.copyOf(names); constraintResultMap.put(constraint, Failable.of(constraintResult)); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 4a42dee160..ba5873a833 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -73,15 +73,6 @@ public MissionModelJar getMissionModelById(final String id) throws NoSuchMission } } - @Override - public Map getConstraints(final String missionModelId) throws NoSuchMissionModelException { - try { - return this.missionModelRepository.getConstraints(missionModelId); - } catch (final MissionModelRepository.NoSuchMissionModelException ex) { - throw new NoSuchMissionModelException(missionModelId, ex); - } - } - @Override public Map getResourceSchemas(final String missionModelId) throws NoSuchMissionModelException, MissionModelLoadException diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java index 51139ba56e..e12c366e67 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalPlanService.java @@ -44,7 +44,7 @@ public RevisionData getPlanRevisionData(final PlanId planId) throws NoSuchPlanEx @Override public Map getConstraintsForPlan(final PlanId planId) throws NoSuchPlanException { - return this.planRepository.getAllConstraintsInPlan(planId); + return this.planRepository.getPlanConstraints(planId); } @Override diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java index ccf97076a7..6d18cf9229 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java @@ -25,9 +25,6 @@ public interface MissionModelService { MissionModelJar getMissionModelById(String missionModelId) throws NoSuchMissionModelException; - Map getConstraints(String missionModelId) - throws NoSuchMissionModelException; - Map getResourceSchemas(String missionModelId) throws NoSuchMissionModelException; diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java index 71bbedcc07..9a4cc8fea4 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/mocks/StubMissionModelService.java @@ -117,11 +117,6 @@ public MissionModelJar getMissionModelById(final String missionModelId) throws N return EXISTENT_MISSION_MODEL; } - @Override - public Map getConstraints(final String missionModelId) throws NoSuchMissionModelException { - return Map.of(); - } - @Override public Map getResourceSchemas(final String missionModelId) throws NoSuchMissionModelException { if (!Objects.equals(missionModelId, EXISTENT_MISSION_MODEL_ID)) { From 421cbc322aa77ef3833606baafbb2350e9fcd2e9 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 10 Jan 2024 12:48:15 -0800 Subject: [PATCH 071/159] Update Postgres Actions --- .../server/remotes/ConstraintRepository.java | 3 +- .../remotes/postgres/ConstraintRecord.java | 1 + .../postgres/GetPlanConstraintsAction.java | 31 +++++++++++++------ .../GetValidConstraintRunsAction.java | 22 ++++++++----- .../postgres/InsertConstraintRunsAction.java | 4 +-- .../PostgresConstraintRepository.java | 5 ++- .../postgres/PostgresPlanRepository.java | 4 +-- .../server/services/ConstraintAction.java | 8 ++--- .../server/services/ConstraintService.java | 3 +- .../services/LocalConstraintService.java | 5 ++- 10 files changed, 49 insertions(+), 37 deletions(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java index e99cf4e1cd..80b839be6e 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ConstraintRepository.java @@ -5,12 +5,11 @@ import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId; import gov.nasa.jpl.aerie.merlin.server.remotes.postgres.ConstraintRunRecord; -import java.util.List; import java.util.Map; public interface ConstraintRepository { void insertConstraintRuns(final Map constraintMap, final Map constraintResults, final Long simulationDatasetId); - Map getValidConstraintRuns(List constraintIds, SimulationDatasetId simulationDatasetId); + Map getValidConstraintRuns(Map constraints, SimulationDatasetId simulationDatasetId); } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/ConstraintRecord.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/ConstraintRecord.java index 814819035a..097b0c886f 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/ConstraintRecord.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/ConstraintRecord.java @@ -2,6 +2,7 @@ public record ConstraintRecord( long id, + long revision, String name, String description, String definition) {} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetPlanConstraintsAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetPlanConstraintsAction.java index 76c384102b..b106d97567 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetPlanConstraintsAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetPlanConstraintsAction.java @@ -11,14 +11,26 @@ /*package local*/ final class GetPlanConstraintsAction implements AutoCloseable { // We left join through the plan table in order to distinguish - // a plan without any constraints from a non-existent plan. - // A plan without constraints will produce a placeholder row with nulls. + // a plan without any enabled constraints from a non-existent plan. + // A plan without any enabled constraints will produce a placeholder row with nulls. private static final @Language("SQL") String sql = """ - select c.id, c.name, c.description, c.definition - from plan AS p - left join "constraint" AS c - on p.id = c.plan_id - where p.id = ? + select c.constraint_id, c.revision, c.name, c.description, c.definition + from plan p + left join (select cs.plan_id, cs.constraint_id, cd.revision, cm.name, cm.description, cd.definition + from constraint_specification cs + left join constraint_definition cd using (constraint_id) + left join public.constraint_metadata cm on cs.constraint_id = cm.id + where cs.enabled + and ((cs.constraint_revision is not null + and cs.constraint_revision = cd.revision) + or (cs.constraint_revision is null + and cd.revision = (select def.revision + from constraint_definition def + where def.constraint_id = cs.constraint_id + order by def.revision desc limit 1))) + ) c + on p.id = c.plan_id + where p.id = ?; """; private final PreparedStatement statement; @@ -39,9 +51,10 @@ public Optional> get(final long planId) throws SQLExcepti final var constraint = new ConstraintRecord( results.getLong(1), - results.getString(2), + results.getLong(2), results.getString(3), - results.getString(4)); + results.getString(4), + results.getString(5)); constraints.add(constraint); } while (results.next()); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetValidConstraintRunsAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetValidConstraintRunsAction.java index b98192cfff..b38c9cb156 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetValidConstraintRunsAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetValidConstraintRunsAction.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.merlin.server.remotes.postgres; +import gov.nasa.jpl.aerie.merlin.server.models.Constraint; import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId; import org.intellij.lang.annotations.Language; @@ -8,6 +9,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import static gov.nasa.jpl.aerie.constraints.json.ConstraintParsers.constraintResultP; import static gov.nasa.jpl.aerie.merlin.server.remotes.postgres.PostgresParsers.getJsonColumn; @@ -16,27 +18,26 @@ final class GetValidConstraintRunsAction implements AutoCloseable { private static final @Language("SQL") String sql = """ select cr.constraint_id, + cr.constraint_revision, cr.simulation_dataset_id, - cr.definition_outdated, cr.results from constraint_run as cr - where cr.definition_outdated = false - and cr.constraint_id = any(?) - and cr.simulation_dataset_id = ? + where cr.constraint_id = any(?) + and cr.simulation_dataset_id = ?; """; private final PreparedStatement statement; - private final List constraintIds; + private final Map constraints; private final SimulationDatasetId simulationDatasetId; - public GetValidConstraintRunsAction(final Connection connection, final List constraintIds, final SimulationDatasetId simulationDatasetId) throws SQLException { + public GetValidConstraintRunsAction(final Connection connection, final Map constraints, final SimulationDatasetId simulationDatasetId) throws SQLException { this.statement = connection.prepareStatement(sql); - this.constraintIds = constraintIds; + this.constraints = constraints; this.simulationDatasetId = simulationDatasetId; } public List get() throws SQLException { - this.statement.setArray(1, this.statement.getConnection().createArrayOf("integer", constraintIds.toArray())); + this.statement.setArray(1, this.statement.getConnection().createArrayOf("integer", constraints.keySet().toArray())); this.statement.setLong(2, simulationDatasetId.id()); try (final var results = this.statement.executeQuery()) { @@ -44,6 +45,11 @@ public List get() throws SQLException { while (results.next()) { final var constraintId = results.getLong("constraint_id"); + final var constraintRevision = results.getLong("constraint_revision"); + + // The cached result wasn't for the correct revision + if(constraints.get(constraintId).revision() != constraintRevision) continue; + final var resultString = results.getString("results"); // The constraint run didn't have any violations diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/InsertConstraintRunsAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/InsertConstraintRunsAction.java index b9af5975e4..fd2d536a86 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/InsertConstraintRunsAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/InsertConstraintRunsAction.java @@ -13,7 +13,7 @@ /* package local */ class InsertConstraintRunsAction implements AutoCloseable { private static final @Language("SQL") String sql = """ - insert into constraint_run (constraint_id, constraint_definition, simulation_dataset_id, results) + insert into constraint_run (constraint_id, constraint_revision, simulation_dataset_id, results) values (?, ?, ?, ?::json) """; @@ -29,7 +29,7 @@ public void apply( Long simulationDatasetId) throws SQLException { for (Constraint constraint : constraintMap.values()) { statement.setLong(1, constraint.id()); - statement.setString(2, constraint.definition()); + statement.setLong(2, constraint.revision()); statement.setLong(3, simulationDatasetId); if (results.get(constraint.id()) != null) { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java index 13a29000d5..edf533d459 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresConstraintRepository.java @@ -8,7 +8,6 @@ import javax.sql.DataSource; import java.sql.SQLException; import java.util.HashMap; -import java.util.List; import java.util.Map; public class PostgresConstraintRepository implements ConstraintRepository { @@ -34,9 +33,9 @@ public void insertConstraintRuns( } @Override - public Map getValidConstraintRuns(List constraintIds, SimulationDatasetId simulationDatasetId) { + public Map getValidConstraintRuns(Map constraints, SimulationDatasetId simulationDatasetId) { try (final var connection = this.dataSource.getConnection(); - final var validConstraintRunAction = new GetValidConstraintRunsAction(connection, constraintIds, simulationDatasetId)) { + final var validConstraintRunAction = new GetValidConstraintRunsAction(connection, constraints, simulationDatasetId)) { final var constraintRuns = validConstraintRunAction.get(); final var validConstraintRuns = new HashMap(); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java index 42307753c8..ebb2526ff6 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresPlanRepository.java @@ -190,10 +190,10 @@ public Map getPlanConstraints(final PlanId planId) throws NoSu ConstraintRecord::id, r -> new Constraint( r.id(), + r.revision(), r.name(), r.description(), - r.definition(), - ConstraintType.plan))); + r.definition()))); } } catch (final SQLException ex) { throw new DatabaseException( diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java index 43a8ce3633..46a7329612 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java @@ -60,13 +60,10 @@ public Map> getViolations(final PlanId planId, final Opt + " has not yet been simulated at its current revision")); } - final var constraintCode = new HashMap<>(this.planService.getConstraintsForPlan(planId));; + final var constraintCode = new HashMap<>(this.planService.getConstraintsForPlan(planId)); final var constraintResultMap = new HashMap>(); - final var validConstraintRuns = this.constraintService.getValidConstraintRuns(constraintCode - .values() - .stream() - .toList(), simDatasetId); + final var validConstraintRuns = this.constraintService.getValidConstraintRuns(constraintCode, simDatasetId); // Remove any constraints that we've already checked, so they aren't rechecked. for (ConstraintRunRecord constraintRun : validConstraintRuns.values()) { @@ -141,7 +138,6 @@ public Map> getViolations(final PlanId planId, final Opt final var constraint = entry.getValue(); final Expression expression; - // TODO: cache these results, @JoelCourtney is this in reference to caching the output of the DSL compilation? final ConstraintsDSLCompilationService.ConstraintsDSLCompilationResult constraintCompilationResult; try { constraintCompilationResult = constraintsDSLCompilationService.compileConstraintsDSL( diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java index 9c9fe2c253..ee9da3eb25 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintService.java @@ -5,10 +5,9 @@ import gov.nasa.jpl.aerie.merlin.server.models.SimulationDatasetId; import gov.nasa.jpl.aerie.merlin.server.remotes.postgres.ConstraintRunRecord; -import java.util.List; import java.util.Map; public interface ConstraintService { void createConstraintRuns(Map constraintMap, Map constraintResults, SimulationDatasetId simulationDatasetId); - Map getValidConstraintRuns(List constraints, SimulationDatasetId simulationDatasetId); + Map getValidConstraintRuns(Map constraints, SimulationDatasetId simulationDatasetId); } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java index 4c81fefcfb..6db80bcd20 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalConstraintService.java @@ -6,7 +6,6 @@ import gov.nasa.jpl.aerie.merlin.server.remotes.ConstraintRepository; import gov.nasa.jpl.aerie.merlin.server.remotes.postgres.ConstraintRunRecord; -import java.util.List; import java.util.Map; public class LocalConstraintService implements ConstraintService { @@ -24,7 +23,7 @@ public void createConstraintRuns(final Map constraintMap, fina } @Override - public Map getValidConstraintRuns(List constraints, SimulationDatasetId simulationDatasetId) { - return constraintRepository.getValidConstraintRuns(constraints.stream().map(Constraint::id).toList(), simulationDatasetId); + public Map getValidConstraintRuns(Map constraints, SimulationDatasetId simulationDatasetId) { + return constraintRepository.getValidConstraintRuns(constraints, simulationDatasetId); } } From f5fbcbdadac7439902e549403001825f6c764e36 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 10 Jan 2024 12:50:07 -0800 Subject: [PATCH 072/159] Remove Mission Model Service from ConstraintAction --- .../java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java | 1 - .../jpl/aerie/merlin/server/services/ConstraintAction.java | 3 --- 2 files changed, 4 deletions(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java index cf56b85d09..0da35c7219 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java @@ -90,7 +90,6 @@ public static void main(final String[] args) { constraintsDSLCompilationService, constraintService, planController, - missionModelController, simulationController ); final var generateConstraintsLibAction = new GenerateConstraintsLibAction(typescriptCodeGenerationService); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java index 46a7329612..15915ab13d 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java @@ -20,20 +20,17 @@ public class ConstraintAction { private final ConstraintsDSLCompilationService constraintsDSLCompilationService; private final ConstraintService constraintService; private final PlanService planService; - private final MissionModelService missionModelService; private final SimulationService simulationService; public ConstraintAction( final ConstraintsDSLCompilationService constraintsDSLCompilationService, final ConstraintService constraintService, final PlanService planService, - final MissionModelService missionModelService, final SimulationService simulationService ) { this.constraintsDSLCompilationService = constraintsDSLCompilationService; this.constraintService = constraintService; this.planService = planService; - this.missionModelService = missionModelService; this.simulationService = simulationService; } From a22e725abeab0f701c137490eb730bd96bb83d56 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 10 Jan 2024 11:01:22 -0800 Subject: [PATCH 073/159] Update e2eTests - Update and Add GQL queries related to constraints - Update types - Add ConstraintsTests for versioning and enabling/disabling constraints - Remove tests that checked `definitionOutdated`, which doesn't exist anymore --- .../nasa/jpl/aerie/e2e/ConstraintsTests.java | 192 +++++++++--------- .../aerie/e2e/types/CachedConstraintRun.java | 6 +- .../jpl/aerie/e2e/types/ConstraintRecord.java | 4 +- .../gov/nasa/jpl/aerie/e2e/utils/GQL.java | 83 +++++--- .../jpl/aerie/e2e/utils/HasuraRequests.java | 40 +++- 5 files changed, 195 insertions(+), 130 deletions(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java index 505843644b..d0438908db 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/ConstraintsTests.java @@ -2,6 +2,7 @@ import com.microsoft.playwright.Playwright; import gov.nasa.jpl.aerie.e2e.types.ConstraintError; +import gov.nasa.jpl.aerie.e2e.types.ConstraintRecord; import gov.nasa.jpl.aerie.e2e.types.ExternalDataset.ProfileInput; import gov.nasa.jpl.aerie.e2e.types.ExternalDataset.ProfileInput.ProfileSegmentInput; import gov.nasa.jpl.aerie.e2e.types.ValueSchema; @@ -12,6 +13,7 @@ import javax.json.Json; import javax.json.JsonValue; import java.io.IOException; +import java.util.Comparator; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -109,7 +111,6 @@ void constraintsSucceedOneViolation() throws IOException { assertTrue(constraintResponse.success()); assertEquals(constraintId, constraintResponse.constraintId()); assertEquals(constraintName, constraintResponse.constraintName()); - assertEquals("plan", constraintResponse.type()); // Check the Result assertTrue(constraintResponse.result().isPresent()); final var constraintResult = constraintResponse.result().get(); @@ -157,7 +158,6 @@ void constraintsSucceedAgainstVariantWithUnits() throws IOException { assertTrue(constraintResponse.success()); assertEquals(fruitConstraintId, constraintResponse.constraintId()); assertEquals(fruitConstraintName, constraintResponse.constraintName()); - assertEquals("plan", constraintResponse.type()); // Check the Result assertTrue(constraintResponse.result().isPresent()); final var constraintResult = constraintResponse.result().get(); @@ -190,7 +190,6 @@ void constraintsSucceedNoViolations() throws IOException { assertTrue(constraintResponses.get(0).success()); assertEquals(constraintId, constraintResponses.get(0).constraintId()); assertEquals(constraintName, constraintResponses.get(0).constraintName()); - assertEquals("plan", constraintResponses.get(0).type()); assertTrue( constraintResponses.get(0).result().isPresent()); assertEquals(0, constraintResponses.get(0).result().get().violations().size()); } @@ -208,7 +207,6 @@ void constraintCachedViolation() throws IOException { // Check properties final var cachedRun = cachedRuns.get(0); - assertFalse(cachedRun.definitionOutdated()); assertEquals(constraintId, cachedRun.constraintId()); assertEquals(simDatasetId, cachedRun.simDatasetId()); assertEquals(constraintDefinition, cachedRun.constraintDefinition()); @@ -226,15 +224,16 @@ void constraintCachedNoViolations() throws IOException { // Delete activity to avoid violation hasura.deleteActivity(planId, activityId); final var simDatasetId = hasura.awaitSimulation(planId).simDatasetId(); - hasura.checkConstraints(planId); + final var constraintsResults = hasura.checkConstraints(planId); final var cachedRuns = hasura.getConstraintRuns(simDatasetId); // There's the correct number of results assertEquals(1, cachedRuns.size()); + assertEquals(1, constraintsResults.size()); // Check properties final var cachedRun = cachedRuns.get(0); - assertFalse(cachedRun.definitionOutdated()); + assertEquals(constraintsResults.get(0).constraintRevision(), cachedRun.constraintRevision()); assertEquals(constraintId, cachedRun.constraintId()); assertEquals(simDatasetId, cachedRun.simDatasetId()); assertEquals(constraintDefinition, cachedRun.constraintDefinition()); @@ -249,21 +248,6 @@ void constraintCachedNoViolations() throws IOException { assertEquals(0,results.gaps().size()); } - @Test - void constraintCacheInvalidatedDefinition() throws IOException { - final int simDatasetId = hasura.awaitSimulation(planId).simDatasetId(); - hasura.checkConstraints(planId); - - // Updating the constraint definition should mark the constraint run as invalid - final String newDefinition = - "export default (): Constraint => Real.Resource(\"/peel\").equal(Real.Resource(\"/fruit\"))"; - hasura.updateConstraint(constraintId, newDefinition); - - final var cachedRuns = hasura.getConstraintRuns(simDatasetId); - assertEquals(1, cachedRuns.size()); - assertTrue(cachedRuns.get(0).definitionOutdated()); - } - /** * Test that an activity with a duration longer than one month is written to and read back from the database * successfully @@ -275,7 +259,7 @@ void constraintCacheInvalidatedDefinition() throws IOException { @Test void constraintsWorkMonthLongActivity() throws IOException { // Setup - hasura.updateConstraint( + hasura.updateConstraintDefinition( constraintId, "export default (): Constraint => Windows.During(ActivityType.ControllableDurationActivity).not()"); hasura.deleteActivity(planId, activityId); @@ -294,7 +278,6 @@ void constraintsWorkMonthLongActivity() throws IOException { assertTrue(constraintResponse.success()); assertEquals(constraintId,constraintResponse.constraintId()); assertEquals(constraintName, constraintResponse.constraintName()); - assertEquals("plan", constraintResponse.type()); //Check Result assertTrue(constraintResponse.result().isPresent()); @@ -328,7 +311,6 @@ void runConstraintsOnOldSimulation() throws IOException { assertEquals(1, newConstraintResponses.size()); assertEquals(constraintId, newConstraintResponses.get(0).constraintId()); assertEquals(constraintName, newConstraintResponses.get(0).constraintName()); - assertEquals("plan", newConstraintResponses.get(0).type()); assertTrue(newConstraintResponses.get(0).result().isPresent()); final var newConstraintResult = newConstraintResponses.get(0).result().get(); assertTrue(newConstraintResult.violations().isEmpty()); @@ -343,7 +325,6 @@ void runConstraintsOnOldSimulation() throws IOException { assertTrue(oldConstraintResponse.success()); assertEquals(constraintId, oldConstraintResponse.constraintId()); assertEquals(constraintName, oldConstraintResponse.constraintName()); - assertEquals("plan", oldConstraintResponse.type()); assertTrue(oldConstraintResponse.result().isPresent()); final var constraintResult = oldConstraintResponse.result().get(); @@ -366,19 +347,103 @@ void runConstraintsOnOldSimulation() throws IOException { assertTrue(constraintResult.gaps().isEmpty()); } + /** + * If a plan specifies a version of a constraint to use, then that version will be used + * when checking constraints. + * + * If the test fails with a compilation error, that means it used the latest version of the constraint + */ @Test - void cachedRunsNotOutdatedOnResim() throws IOException { - final int oldSimDatasetId = hasura.awaitSimulation(planId).simDatasetId(); - hasura.checkConstraints(planId); - - // Delete Activity to make the simulation outdated, then resim - hasura.deleteActivity(planId, activityId); + void constraintVersionLocking() throws IOException { hasura.awaitSimulation(planId); - // Get the old run - final var cachedRuns = hasura.getConstraintRuns(oldSimDatasetId); - assertEquals(1, cachedRuns.size()); - assertFalse(cachedRuns.get(0).definitionOutdated()); + // Update the plan's constraint specification to use a specific version + hasura.updatePlanConstraintSpecVersion(planId, constraintId, 0); + + // Update definition to have invalid syntax + final int newRevision = hasura.updateConstraintDefinition( + constraintId, + " error :-("); + + // Check constraints -- should succeed + final var initResults = hasura.checkConstraints(planId); + assertEquals(1, initResults.size()); + final var initConstraint = initResults.get(0); + assertEquals(constraintId, initConstraint.constraintId()); + assertEquals(0, initConstraint.constraintRevision()); + assertTrue(initConstraint.success()); + assertTrue(initConstraint.errors().isEmpty()); + assertTrue(initConstraint.result().isPresent()); + + // Update constraint spec to use invalid definition + hasura.updatePlanConstraintSpecVersion(planId, constraintId, newRevision); + + // Check constraints -- should fail + final var badDefinitionResults = hasura.checkConstraints(planId); + assertEquals(1, badDefinitionResults.size()); + final var badConstraint = badDefinitionResults.get(0); + assertEquals(constraintId, badConstraint.constraintId()); + assertFalse(badConstraint.success()); + assertEquals(constraintName, badConstraint.constraintName()); + assertEquals(2, badConstraint.errors().size()); + assertEquals("Constraint 'fruit_equal_peel' compilation failed:\n" + + " TypeError: TS2306 No default export. Expected a default export function with the signature: " + + "\"(...args: []) => Constraint | Promise\".", + badConstraint.errors().get(0).message()); + assertEquals("Constraint 'fruit_equal_peel' compilation failed:\n TypeError: TS1109 Expression expected.", + badConstraint.errors().get(1).message()); + + // Update constraint spec to use initial definition + hasura.updatePlanConstraintSpecVersion(planId, constraintId, 0); + + // Check constraints -- should match + assertEquals(initResults, hasura.checkConstraints(planId)); + } + + @Test + @DisplayName("Disabled Constraints are not checked") + void constraintIgnoreDisabled() throws IOException { + hasura.awaitSimulation(planId); + // Add a problematic constraint to the spec, then disable it + final String problemConstraintName = "bad constraint"; + final int problemConstraintId = hasura.insertPlanConstraint( + problemConstraintName, + planId, + "error :-(", + "constraint that shouldn't compile"); + try { + hasura.updatePlanConstraintSpecEnabled(planId, problemConstraintId, false); + + // Check constraints -- Validate that only the enabled constraint is included + final var initResults = hasura.checkConstraints(planId); + assertEquals(1, initResults.size()); + assertEquals(constraintId, initResults.get(0).constraintId()); + assertTrue(initResults.get(0).success()); + + // Enable disabled constraint + hasura.updatePlanConstraintSpecEnabled(planId, problemConstraintId, true); + + // Check constraints -- Validate that the other constraint is present and a failure + final var results = hasura.checkConstraints(planId); + results.sort(Comparator.comparing(ConstraintRecord::constraintId)); + assertEquals(2, results.size()); + assertEquals(constraintId, results.get(0).constraintId()); + assertTrue(results.get(0).success()); + + final var problemResults = results.get(1); + assertEquals(problemConstraintId, problemResults.constraintId()); + assertFalse(problemResults.success()); + assertEquals(problemConstraintName, problemResults.constraintName()); + assertEquals(2, problemResults.errors().size()); + assertEquals("Constraint 'bad constraint' compilation failed:\n" + + " TypeError: TS2306 No default export. Expected a default export function with the signature: " + + "\"(...args: []) => Constraint | Promise\".", + problemResults.errors().get(0).message()); + assertEquals("Constraint 'bad constraint' compilation failed:\n TypeError: TS1109 Expression expected.", + problemResults.errors().get(1).message()); + } finally { + hasura.deleteConstraint(problemConstraintId); + } } @Nested @@ -400,7 +465,7 @@ class WithExternalDatasets { @BeforeEach void beforeEach() throws IOException { // Change Constraint to be about MyBoolean - hasura.updateConstraint( + hasura.updateConstraintDefinition( constraintId, constraintDefinition); // Simulate Plan @@ -429,7 +494,6 @@ void oneViolationCurrentSimulation() throws IOException { assertTrue(noDatasetResponses.get(0).success()); assertEquals(constraintId, noDatasetResponses.get(0).constraintId()); assertEquals(constraintName, noDatasetResponses.get(0).constraintName()); - assertEquals("plan", noDatasetResponses.get(0).type()); assertTrue(noDatasetResponses.get(0).result().isPresent()); final var nRecordResults = noDatasetResponses.get(0).result().get(); @@ -439,7 +503,6 @@ void oneViolationCurrentSimulation() throws IOException { assertTrue(withDatasetResponses.get(0).success()); assertEquals(constraintId, withDatasetResponses.get(0).constraintId()); assertEquals(constraintName, withDatasetResponses.get(0).constraintName()); - assertEquals("plan", withDatasetResponses.get(0).type()); assertTrue(withDatasetResponses.get(0).result().isPresent()); final var wRecordResults = withDatasetResponses.get(0).result().get(); @@ -492,7 +555,6 @@ void oneViolationOutdatedSimIdPassed() throws IOException { assertTrue(constraintResponse.success()); assertEquals(constraintId, constraintResponse.constraintId()); assertEquals(constraintName, constraintResponse.constraintName()); - assertEquals("plan", constraintResponse.type()); assertTrue(constraintResponse.result().isPresent()); final var record = constraintResponse.result().get(); @@ -540,7 +602,6 @@ void compilationFailsOutdatedSimulationSimDatasetId() throws IOException { final var constraintResponse = constraintResponses.get(0); assertEquals(constraintId, constraintResponse.constraintId()); assertEquals(constraintName, constraintResponse.constraintName()); - assertEquals("plan", constraintResponse.type()); assertFalse(constraintResponse.success()); assertTrue(constraintResponse.result().isEmpty()); assertEquals(1,constraintResponse.errors().size()); @@ -564,62 +625,11 @@ void compilationFailsOutdatedSimulationNoSimDataset() throws IOException { assertFalse(constraintResponse.success()); assertEquals(constraintId, constraintResponse.constraintId()); assertEquals(constraintName, constraintResponse.constraintName()); - assertEquals("plan", constraintResponse.type()); assertTrue(constraintResponse.result().isEmpty()); assertEquals(1,constraintResponse.errors().size()); final ConstraintError error = constraintResponse.errors().get(0); assertEquals("Constraint 'fruit_equal_peel' compilation failed:\n" + " TypeError: TS2345 Argument of type '\"/my_boolean\"' is not assignable to parameter of type 'ResourceName'.",error.message()); - - } - - /** - * This test case evaluates the scenario where a user initially has a functioning constraint. - * However, during the modification process, they introduce a compilation error. - * The goal is to confirm that the user can successfully fix the compilation error, - * ensuring that the constraint compiles correctly and returns a valid response. - */ - @Test - @DisplayName("If a constraint is updated and then reverted, Constraints will load the cached value from before the update.") - void constraintInvalidModification() throws IOException { - // Simulate the plan to insure a new simulation set - hasura.awaitSimulation(planId); - - // Check the initial constraints responses it should compile - final var constraintsResponses = hasura.checkConstraints(planId); - assertEquals(1, constraintsResponses.size()); - assertTrue(constraintsResponses.get(0).success()); - assertEquals(constraintId, constraintsResponses.get(0).constraintId()); - assertEquals(constraintName, constraintsResponses.get(0).constraintName()); - assertEquals("plan", constraintsResponses.get(0).type()); - // Attempt to update a constraint with an invalid syntax and capture the error response - hasura.updateConstraint( - constraintId, - WithExternalDatasets.constraintDefinition + "; error"); - - // Check the constraints response after the invalid modification - final var constraintsErrorResponses = hasura.checkConstraints(planId); - assertEquals(1, constraintsErrorResponses.size()); - assertFalse(constraintsErrorResponses.get(0).success()); - assertEquals(constraintId, constraintsErrorResponses.get(0).constraintId()); - assertEquals(constraintName, constraintsErrorResponses.get(0).constraintName()); - assertEquals("plan", constraintsErrorResponses.get(0).type()); - assertTrue(constraintsErrorResponses.get(0).result().isEmpty()); - assertEquals(1, constraintsErrorResponses.get(0).errors().size()); - - // Restore the original valid constraint - hasura.updateConstraint( - constraintId, - WithExternalDatasets.constraintDefinition); - - // Check the constraints response after reverting to the valid constraint - final var constraintsCacheResponses = hasura.checkConstraints(planId); - assertEquals(1, constraintsCacheResponses.size()); - assertTrue(constraintsCacheResponses.get(0).success()); - - // Ensure that the constraints responses are the same before and after the invalid modification - assertEquals(constraintsResponses, constraintsCacheResponses); } - } } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/CachedConstraintRun.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/CachedConstraintRun.java index 663f92bb15..8ba9efd4e5 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/CachedConstraintRun.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/CachedConstraintRun.java @@ -5,17 +5,17 @@ public record CachedConstraintRun( int constraintId, + int constraintRevision, int simDatasetId, String constraintDefinition, - boolean definitionOutdated, Optional results ) { public static CachedConstraintRun fromJSON(JsonObject json) { return new CachedConstraintRun( json.getInt("constraint_id"), + json.getInt("constraint_revision"), json.getInt("simulation_dataset_id"), - json.getString("constraint_definition"), - json.getBoolean("definition_outdated"), + json.getJsonObject("constraint_definition").getString("definition"), json.getJsonObject("results").isEmpty() ? Optional.empty() : Optional.of(ConstraintResult.fromJSON(json.getJsonObject("results"))) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintRecord.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintRecord.java index 48cfebcfbb..5cac2eb096 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintRecord.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ConstraintRecord.java @@ -7,8 +7,8 @@ public record ConstraintRecord( boolean success, int constraintId, + int constraintRevision, String constraintName, - String type, Optional result, List errors @@ -17,8 +17,8 @@ public static ConstraintRecord fromJSON(JsonObject json){ return new ConstraintRecord( json.getBoolean("success"), json.getInt("constraintId"), + json.getInt("constraintRevision"), json.getString("constraintName"), - json.getString("type"), json.getJsonObject("results").isEmpty() ? Optional.empty() : Optional.of(ConstraintResult.fromJSON(json.getJsonObject("results"))), json.getJsonArray("errors").getValuesAs(ConstraintError::fromJSON)); } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index 6a687d9445..e2f6ace7ec 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -52,8 +52,8 @@ query checkConstraints($planId: Int!, $simulationDatasetId: Int) { constraintViolations(planId: $planId, simulationDatasetId: $simulationDatasetId) { success constraintId + constraintRevision constraintName - type results { resourceIds gaps { @@ -141,7 +141,10 @@ mutation DeleteActivityDirective($id: Int!, $plan_id: Int!) { }"""), DELETE_CONSTRAINT(""" mutation DeleteConstraint($id: Int!) { - delete_constraint_by_pk(id: $id) { + delete_constraint_specification(where: {constraint_id: {_eq: $id}}){ + affected_rows + } + delete_constraint_metadata_by_pk(id: $id) { id } }"""), @@ -172,6 +175,12 @@ mutation DeletePlan($id: Int!) { id } } + deleteConstraintSpec: delete_constraint_specification(where: {plan_id: {_eq: $id}}){ + returning { + constraint_id + constraint_revision + } + } }"""), DELETE_SCHEDULING_GOAL(""" mutation DeleteSchedulingGoal($goalId: Int!) { @@ -213,11 +222,13 @@ query GetActivityTypes($missionModelId: Int!) { GET_CONSTRAINT_RUNS(""" query getConstraintRuns($simulationDatasetId: Int!) { constraint_run(where: {simulation_dataset_id: {_eq: $simulationDatasetId}}) { - constraint_definition constraint_id + constraint_revision simulation_dataset_id - definition_outdated results + constraint_definition { + definition + } } }"""), GET_EFFECTIVE_ACTIVITY_ARGUMENTS_BULK(""" @@ -279,13 +290,16 @@ query GetPlan($id: Int!) { startOffset: start_offset type } - constraints { - definition - description - id - model_id - name - plan_id + constraint_specification { + constraint_id + constraint_revision + constraint_metadata{ + name + description + } + constraint_definition { + definition + } } duration id @@ -294,14 +308,6 @@ query GetPlan($id: Int!) { name parameters } - constraints { - definition - description - id - model_id - name - plan_id - } id parameters { parameters @@ -436,10 +442,10 @@ query GetTopicsEvents($datasetId: Int!) { } } }"""), - INSERT_CONSTRAINT(""" - mutation insertConstraint($constraint: constraint_insert_input!) { - constraint: insert_constraint_one(object: $constraint) { - id + INSERT_PLAN_SPEC_CONSTRAINT(""" + mutation insertConstraintAssignToPlanSpec($constraint: constraint_specification_insert_input!) { + constraint: insert_constraint_specification_one(object: $constraint){ + constraint_id } }"""), INSERT_PROFILE(""" @@ -497,12 +503,35 @@ query Simulate($plan_id: Int!) { }"""), UPDATE_CONSTRAINT(""" mutation updateConstraint($constraintId: Int!, $constraintDefinition: String!) { - update_constraint(where: {id: {_eq: $constraintId}}, _set: {definition: $constraintDefinition}) { - returning { - definition - } + constraint: insert_constraint_definition_one(object: {constraint_id: $constraintId, definition: $constraintDefinition}) { + definition + revision } }"""), + UPDATE_CONSTRAINT_SPEC_VERSION(""" + mutation updateConstraintSpecVersion($plan_id: Int!, $constraint_id: Int!, $constraint_revision: Int!) { + update_constraint_specification_by_pk( + pk_columns: {constraint_id: $constraint_id, plan_id: $plan_id}, + _set: {constraint_revision: $constraint_revision} + ) { + plan_id + constraint_id + constraint_revision + enabled + } + }"""), + UPDATE_CONSTRAINT_SPEC_ENABLED(""" + mutation updateConstraintSpecVersion($plan_id: Int!, $constraint_id: Int!, $enabled: Boolean!) { + update_constraint_specification_by_pk( + pk_columns: {constraint_id: $constraint_id, plan_id: $plan_id}, + _set: {enabled: $enabled} + ) { + plan_id + constraint_id + constraint_revision + enabled + } + }"""), UPDATE_ROLE_ACTION_PERMISSIONS(""" mutation updateRolePermissions($role: user_roles_enum!, $action_permissions: jsonb!) { permissions: update_user_role_permission_by_pk( diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index ed15f52718..aa5bf80841 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -35,7 +35,7 @@ public class HasuraRequests implements AutoCloseable { public HasuraRequests(Playwright playwright) { request = playwright.request().newContext( new APIRequest.NewContextOptions() - .setBaseURL(BaseURL.HASURA.url).setTimeout(0)); + .setBaseURL(BaseURL.HASURA.url)); } @Override @@ -735,20 +735,46 @@ public List getConstraintRuns(int simulationDatasetId) thro public int insertPlanConstraint(String name, int planId, String definition, String description) throws IOException { final var constraintInsertBuilder = Json.createObjectBuilder() - .add("name", name) .add("plan_id", planId) - .add("definition", definition) - .add("description", description); + .add("constraint_metadata", + Json.createObjectBuilder() + .add("data", + Json.createObjectBuilder() + .add("name", name) + .add("description", description) + .add("versions", + Json.createObjectBuilder() + .add("data", + Json.createObjectBuilder() + .add("definition", definition))))); final var variables = Json.createObjectBuilder().add("constraint", constraintInsertBuilder).build(); - return makeRequest(GQL.INSERT_CONSTRAINT, variables).getJsonObject("constraint").getInt("id"); + return makeRequest(GQL.INSERT_PLAN_SPEC_CONSTRAINT, variables).getJsonObject("constraint").getInt("constraint_id"); } - public void updateConstraint(int constraintId, String definition) throws IOException{ + public void updatePlanConstraintSpecVersion(int planId, int constraintId, int constraintRevision) throws IOException { + final var variables = Json.createObjectBuilder() + .add("plan_id", planId) + .add("constraint_id", constraintId) + .add("constraint_revision", constraintRevision) + .build(); + makeRequest(GQL.UPDATE_CONSTRAINT_SPEC_VERSION, variables); + } + + public void updatePlanConstraintSpecEnabled(int planId, int constraintId, boolean enabled) throws IOException { + final var variables = Json.createObjectBuilder() + .add("plan_id", planId) + .add("constraint_id", constraintId) + .add("enabled", enabled) + .build(); + makeRequest(GQL.UPDATE_CONSTRAINT_SPEC_ENABLED, variables); + } + + public int updateConstraintDefinition(int constraintId, String definition) throws IOException{ final var variables = Json.createObjectBuilder() .add("constraintId", constraintId) .add("constraintDefinition", definition) .build(); - makeRequest(GQL.UPDATE_CONSTRAINT, variables); + return makeRequest(GQL.UPDATE_CONSTRAINT, variables).getJsonObject("constraint").getInt("revision"); } public void deleteConstraint(int constraintId) throws IOException { From 32da2625c5311b1b4ff655f37a39f46564e8159e Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 28 Feb 2024 13:13:28 -0800 Subject: [PATCH 074/159] Update JNISpice Jar to JDK 21-compiled version --- third-party/JNISpice-N0067.jar | Bin 981795 -> 980873 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/third-party/JNISpice-N0067.jar b/third-party/JNISpice-N0067.jar index ed4292e68f6547bdaf5b55aafe78bd64907ec76c..26129fd9960578ae68a33c74a6ef84f9cc418987 100644 GIT binary patch delta 174251 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 H! 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�c*f%k9^!NfOA^;Zp!YX|5(5APjB^*_@Y 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%0A@gfaN7+9E%|= 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 delta 174962 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

- * Calling this method once before constructing your model will profile effects on every cell. + * Calling this method once before constructing your model will profile effects on every resource. * Profiling effects may be compute and/or memory intensive, and should not be used in production. *

*

- * If only a few cells are suspect, you can also call {@link Profiling#profileEffects} - * directly on just those cells, rather than profiling every cell. + * If only a few resources are suspect, you can also call {@link Profiling#profileEffects} + * directly on just those resource, rather than profiling every resource. *

*

* Call {@link Profiling#dump()} to see results. @@ -93,6 +98,27 @@ static > void set(MutableResource resource, Expiring static void detectBusyCells() { MutableResourceFlags.DETECT_BUSY_CELLS = true; } + + /** + * Turn on profiling for all {@link MutableResource}s created by {@link MutableResource#resource}. + * Also implies {@link MutableResource#detectBusyCells()}. + * + *

+ * Calling this method once before constructing your model will profile virtually every {@link MutableResource}. + * Profiling may be compute and/or memory intensive, and should not be used in production. + *

+ *

+ * If only a few resources are suspect, you can also call {@link Profiling#profile} + * directly on just those resource, rather than profiling every resource. + *

+ *

+ * Call {@link Profiling#dump()} to see results. + *

+ */ + static void profileAllResources() { + MutableResourceFlags.PROFILE_GET_DYNAMICS = true; + detectBusyCells(); + } } /** @@ -102,4 +128,5 @@ static void detectBusyCells() { */ final class MutableResourceFlags { public static boolean DETECT_BUSY_CELLS = false; + public static boolean PROFILE_GET_DYNAMICS = false; } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java index 2f32a88d07..b0b3a1f929 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resource.java @@ -1,8 +1,31 @@ package gov.nasa.jpl.aerie.contrib.streamline.core; +import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; + /** * A function returning a fully-wrapped dynamics, * and the primary way models track state and report results. */ public interface Resource extends ThinResource>> { + /** + * Turn on profiling for all resources derived through {@link ResourceMonad} + * or created by {@link MutableResource#resource}. + * + *

+ * Calling this method once before constructing your model will profile virtually every resource. + * Profiling may be compute and/or memory intensive, and should not be used in production. + *

+ *

+ * If only a few resources are suspect, you can also call {@link Profiling#profile} + * directly on just those resource, rather than profiling every resource. + *

+ *

+ * Call {@link Profiling#dump()} to see results. + *

+ */ + static void profileAllResources() { + ResourceMonad.profileAllResources(); + MutableResource.profileAllResources(); + } } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java index 04853d3c12..72dff71e60 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/monads/ResourceMonad.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.contrib.streamline.core.Expiring; import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; import gov.nasa.jpl.aerie.contrib.streamline.core.ThinResource; +import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; import gov.nasa.jpl.aerie.contrib.streamline.utils.*; import org.apache.commons.lang3.function.TriFunction; @@ -11,6 +12,7 @@ import java.util.function.Function; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profile; import static gov.nasa.jpl.aerie.contrib.streamline.utils.FunctionalUtils.curry; /** @@ -21,14 +23,37 @@ public final class ResourceMonad { private ResourceMonad() {} + private static boolean profileAllResources = false; + /** + * Turn on profiling for all getDynamics calls on {@link Resource}s derived through {@link ResourceMonad}. + * + *

+ * Calling this method once before constructing your model will profile getDynamics on every derived resource. + * Profiling may be compute and/or memory intensive, and should not be used in production. + *

+ *

+ * If only a few cells are suspect, you can also call {@link Profiling#profile} + * directly on just those resource, rather than profiling every resource. + *

+ *

+ * Call {@link Profiling#dump()} to see results. + *

+ */ + public static void profileAllResources() { + profileAllResources = true; + } + public static
Resource pure(A a) { - return ThinResourceMonad.pure(DynamicsMonad.pure(a))::getDynamics; + Resource result = ThinResourceMonad.pure(DynamicsMonad.pure(a))::getDynamics; + if (profileAllResources) result = profile(result); + return result; } public static Resource apply(Resource a, Resource> f) { Resource result = ThinResourceMonad.apply(a, ThinResourceMonad.map(f, DynamicsMonad::apply))::getDynamics; addDependency(result, a); addDependency(result, f); + if (profileAllResources) result = profile(result); return result; } @@ -43,6 +68,7 @@ public static Resource join(Resource> a) { // The ::getDynamics at the end up-converts back to Resource, from ThinResource Resource result = ThinResourceMonad.map(ThinResourceMonad.join(ThinResourceMonad.map(a$, ResourceMonad::distribute)), DynamicsMonad::join)::getDynamics; addDependency(result, a); + if (profileAllResources) result = profile(result); return result; } diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java index c161cccbb8..575f7688c3 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Naming.java @@ -4,6 +4,7 @@ import java.lang.ref.WeakReference; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -25,9 +26,19 @@ public final class Naming { private Naming() {} // Use a WeakHashMap so that naming a thing doesn't prevent it from being garbage-collected. - private static final WeakHashMap>> NAMES = new WeakHashMap<>(); - // Way to inject a temporary "anonymous" name, so derived names still work even when not all args are named. - private static final MutableObject> anonymousName = new MutableObject<>(Optional.empty()); + private static final WeakHashMap>> NAMES = new WeakHashMap<>(); + + private record NamingContext(Set visited, Optional anonymousName) { + NamingContext visit(Object thing) { + var newVisited = new HashSet<>(visited); + newVisited.add(thing); + return new NamingContext(newVisited, anonymousName); + } + + public NamingContext(String anonymousName) { + this(Set.of(), Optional.ofNullable(anonymousName)); + } + } /** * Register a name for thing, as a function of args' names. @@ -36,15 +47,12 @@ private Naming() {} public static T name(T thing, String nameFormat, Object... args) { // Only capture weak references to arguments, so we don't leak memory var args$ = Arrays.stream(args).map(WeakReference::new).toArray(WeakReference[]::new); - NAMES.put(thing, () -> { + NAMES.put(thing, context -> { Object[] argNames = new Object[args$.length]; for (int i = 0; i < args$.length; ++i) { - // Try to resolve the argument name by first looking up and using its registered name, - // or by falling back to the anonymous name. var argName$ = Optional.ofNullable(args$[i].get()) - .flatMap(Naming::getName) - .or(anonymousName::getValue); - if (argName$.isEmpty()) return Optional.empty(); + .flatMap(argRef -> getName(argRef, context)); + if (argName$.isEmpty()) return context.anonymousName(); argNames[i] = argName$.get(); } return Optional.of(nameFormat.formatted(argNames)); @@ -58,7 +66,7 @@ public static T name(T thing, String nameFormat, Object... args) { * returns empty. */ public static Optional getName(Object thing) { - return Optional.ofNullable(NAMES.get(thing)).flatMap(Supplier::get).or(anonymousName::getValue); + return getName(thing, new NamingContext(null)); } /** @@ -66,11 +74,14 @@ public static Optional getName(Object thing) { * Use anonymousName for anything without a name instead of returning empty. */ public static String getName(Object thing, String anonymousName) { - Naming.anonymousName.setValue(Optional.of(anonymousName)); - var result = getName(thing); - Naming.anonymousName.setValue(Optional.empty()); - // This will never throw, because anonymous name will guarantee that some name is found. - return result.orElseThrow(); + // This expression never throws, because context always has a name available. + return getName(thing, new NamingContext(anonymousName)).orElseThrow(); + } + + private static Optional getName(Object thing, NamingContext context) { + return context.visited.contains(thing) + ? context.anonymousName + : NAMES.getOrDefault(thing, NamingContext::anonymousName).apply(context.visit(thing)); } public static String argsFormat(Collection collection) { diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java index 4a369b8322..78ee47592b 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/debugging/Profiling.java @@ -12,6 +12,7 @@ import java.util.function.Supplier; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static java.lang.Math.*; import static java.util.Comparator.comparingLong; /** @@ -62,13 +63,15 @@ public static Resource profile(Resource resource) { public static Resource profile(String name, Resource resource) { Resource result = new Resource<>() { + private final Supplier name$ = computeName(name, this); + @Override public ErrorCatching> getDynamics() { - return resourceSamples.computeIfAbsent(computeName(name, this), k -> new CallStats()) + return resourceSamples.computeIfAbsent(name$.get(), k -> new CallStats()) .accrue(resource::getDynamics); } }; - assignName(result, name, resource); + assignName("Resource", result, name, resource); return result; } @@ -78,6 +81,8 @@ public static > MutableResource profile(MutableResou public static > MutableResource profile(String name, MutableResource resource) { MutableResource result = new MutableResource<>() { + private final Supplier name$ = computeName(name, this); + @Override public void emit(DynamicsEffect effect) { resource.emit(effect); @@ -85,11 +90,11 @@ public void emit(DynamicsEffect effect) { @Override public ErrorCatching> getDynamics() { - return resourceSamples.computeIfAbsent(computeName(name, this), k -> new CallStats()) + return resourceSamples.computeIfAbsent(name$.get(), k -> new CallStats()) .accrue(resource::getDynamics); } }; - assignName(result, name, resource); + assignName("MutableResource", result, name, resource); return result; } @@ -99,12 +104,14 @@ public static Condition profile(Condition condition) { public static Condition profile(String name, Condition condition) { Condition result = new Condition() { + private final Supplier name$ = computeName(name, this); + @Override public Optional nextSatisfied(boolean positive, Duration atEarliest, Duration atLatest) { - return accrue(conditionEvaluations, computeName(name, this), () -> condition.nextSatisfied(positive, atEarliest, atLatest)); + return accrue(conditionEvaluations, name$.get(), () -> condition.nextSatisfied(positive, atEarliest, atLatest)); } }; - assignName(result, name, condition); + assignName("Condition", result, name, condition); return result; } @@ -130,32 +137,22 @@ public static Supplier profileTask(Supplier task) { public static Supplier profileTask(String name, Supplier task) { Supplier result = new Supplier<>() { + private final Supplier name$ = computeName(name, this); + @Override public R get() { - return accrue(taskExecutions, computeName(name, this), task); + return accrue(taskExecutions, name$.get(), task); } }; - assignName(result, name, task); + assignName("Task", result, name, task); return result; } - private static long ANONYMOUS_CELL_RESOURCE_ID = 0; public static > MutableResource profileEffects(MutableResource resource) { - return new MutableResource<>() { - private String name = null; + MutableResource result = new MutableResource<>() { @Override public void emit(DynamicsEffect effect) { - // Get the name the first time an effect is emitted, - // which will be after any registrations happen. - if (name == null) { - name = getName(this, "..."); - if (name.equals("...")) { - var generatedName = "CellResource" + (ANONYMOUS_CELL_RESOURCE_ID++); - name(this, generatedName); - name = generatedName; - } - } - resource.emit(x -> accrue(effectsEmitted, name, () -> effect.apply(x))); + resource.emit(x -> accrue(effectsEmitted, getName(this, "..."), () -> effect.apply(x))); } @Override @@ -163,15 +160,20 @@ public ErrorCatching> getDynamics() { return resource.getDynamics(); } }; + assignName("MutableResource", result, null, resource); + return result; } - private static String computeName(String explicitName, Object profiledThing) { - return explicitName != null ? explicitName : getName(profiledThing, "..."); + private static Supplier computeName(String explicitName, Object profiledThing) { + return explicitName != null + ? () -> explicitName + : () -> getName(profiledThing, "..."); } - private static void assignName(Object profiledThing, String explicitName, Object originalThing) { + private static long ANONYMOUS_ID = 0; + private static void assignName(String typeName, Object profiledThing, String explicitName, Object originalThing) { if (explicitName == null) { - name(profiledThing, "%s", originalThing); + name(profiledThing, typeName + (ANONYMOUS_ID++) + " = %s", originalThing); } else { name(profiledThing, explicitName); } @@ -206,12 +208,13 @@ public static void dump() { } } + private static final int MAX_NAME_LENGTH = 60; private static void dumpSampleMap(Map map, long overallElapsedNanos, Comparator sortBy) { - final var nameLength = Math.max(5, map.keySet().stream().mapToInt(String::length).max().orElse(1)); + final var nameLength = min(MAX_NAME_LENGTH, max(5, map.keySet().stream().mapToInt(String::length).max().orElse(1))); final var totalCalls = map.values().stream().mapToLong(c1 -> c1.callsMade).sum(); final var totalNanos = map.values().stream().mapToLong(c1 -> c1.ownNanos).sum(); - final var callsLength = Math.max(5, String.valueOf(totalCalls).length()); - final var millisLength = Math.max(7, String.valueOf(totalNanos / 1_000_000).length()); + final var callsLength = max(5, String.valueOf(totalCalls).length()); + final var millisLength = max(7, String.valueOf(totalNanos / 1_000_000).length()); final var titleFormat = " %-" + nameLength + "s |" + " %" + callsLength + "s %7s |" @@ -255,7 +258,7 @@ private static void dumpSampleMap(Map map, long overallElapse var stats = entry.getValue(); System.out.printf( lineFormat, - entry.getKey(), + fit(entry.getKey(), nameLength), stats.callsMade, 100.0 * stats.callsMade / totalCalls, stats.totalNanos / 1_000_000, @@ -267,6 +270,13 @@ private static void dumpSampleMap(Map map, long overallElapse }); } + private static final String TRUNCATED_INDICATOR = " ..."; + private static String fit(String s, int maxNameLength) { + return s.length() <= maxNameLength + ? s + : s.substring(0, maxNameLength - TRUNCATED_INDICATOR.length()) + TRUNCATED_INDICATOR; + } + private static final class CallStats { public long callsMade = 0; public long totalNanos = 0; From dfc88d8d32c88834b8d3feead60d4b1515205960 Mon Sep 17 00:00:00 2001 From: David Legg Date: Thu, 1 Feb 2024 10:02:05 -0800 Subject: [PATCH 083/159] Connect new resource profiling to streamline example model. --- .../java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java | 3 +++ .../main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java index fbdf8a0a7a..0843605ebe 100644 --- a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Configuration.java @@ -7,6 +7,9 @@ public final class Configuration { @Parameter public boolean traceResources = false; + @Parameter + public boolean profileResources = false; + @Parameter public double approximationTolerance = 1e-2; diff --git a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java index 2021ed3c57..ece739efba 100644 --- a/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java +++ b/examples/streamline-demo/src/main/java/gov/nasa/jpl/aerie/streamline_demo/Mission.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.streamline_demo; +import gov.nasa.jpl.aerie.contrib.streamline.core.Resource; import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling; import gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar; import gov.nasa.jpl.aerie.merlin.framework.ModelActions; @@ -12,6 +13,7 @@ public final class Mission { public Mission(final gov.nasa.jpl.aerie.merlin.framework.Registrar registrar$, final Configuration config) { var registrar = new Registrar(registrar$, Registrar.ErrorBehavior.Log); if (config.traceResources) registrar.setTrace(); + if (config.profileResources) Resource.profileAllResources(); dataModel = new DataModel(registrar, config); errorTestingModel = new ErrorTestingModel(registrar, config); approximationModel = new ApproximationModel(registrar, config); From 62b4dbb56e59526f0c5a632a4a71ce4c50a96e14 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Wed, 21 Feb 2024 05:21:17 -1000 Subject: [PATCH 084/159] Added an option to turn off the transpiler. --- sequencing-server/src/env.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sequencing-server/src/env.ts b/sequencing-server/src/env.ts index ee8564799f..0f2d5e8c06 100644 --- a/sequencing-server/src/env.ts +++ b/sequencing-server/src/env.ts @@ -12,6 +12,7 @@ export type Env = { STORAGE: string; SEQUENCING_WORKER_NUM: string; SEQUENCING_MAX_WORKER_HEAP_MB: string; + TRANSPILER_ENABLED: string; }; export const defaultEnv: Env = { @@ -28,6 +29,7 @@ export const defaultEnv: Env = { STORAGE: 'sequencing_file_store', SEQUENCING_WORKER_NUM: '8', SEQUENCING_MAX_WORKER_HEAP_MB: '1000', + TRANSPILER_ENABLED: 'true', }; export function getEnv(): Env { @@ -47,6 +49,7 @@ export function getEnv(): Env { const SEQUENCING_WORKER_NUM = env['SEQUENCING_WORKER_NUM'] ?? defaultEnv.SEQUENCING_WORKER_NUM; const SEQUENCING_MAX_WORKER_HEAP_MB = env['SEQUENCING_MAX_WORKER_HEAP_MB'] ?? defaultEnv.SEQUENCING_MAX_WORKER_HEAP_MB; + const TRANSPILER_ENABLED = env['TRANSPILER_ENABLED'] ?? defaultEnv.TRANSPILER_ENABLED; return { HASURA_GRAPHQL_ADMIN_SECRET, LOG_FILE, @@ -61,5 +64,6 @@ export function getEnv(): Env { STORAGE, SEQUENCING_WORKER_NUM, SEQUENCING_MAX_WORKER_HEAP_MB, + TRANSPILER_ENABLED, }; } From 18f8f2bec568e02b5a7eed2614fad72bb3db67ed Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Wed, 21 Feb 2024 05:21:48 -1000 Subject: [PATCH 085/159] Expose the transpiler option in the docker-compose.yml --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index aa46392887..9df4a80fea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -95,6 +95,7 @@ services: SEQUENCING_LOCAL_STORE: /usr/src/app/sequencing_file_store SEQUENCING_WORKER_NUM: 8 SEQUENCING_MAX_WORKER_HEAP_MB: 1000 + TRANSPILER_ENABLED : "true" image: aerie_sequencing ports: ["27184:27184"] restart: always From 717ac548bbccb05e1b8fbf026e3831bbeba4dcef Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Thu, 15 Feb 2024 06:04:28 -1000 Subject: [PATCH 086/159] Implement background transpiler for faster expansion set and sequence generation downstream * Fetches the latest command dictionary, mission model, and expansion logic on a regular basis.Transpiles and caches the results. * Help to pay the upfront cost that we are seeing on Clipper where there expansion logic takes about 15 minutes to generate a expansion set of 130 authoring logic. --- sequencing-server/src/backgroundTranspiler.ts | 143 ++++++++++++++++++ sequencing-server/src/utils/hasura.ts | 96 +++++++++++- 2 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 sequencing-server/src/backgroundTranspiler.ts diff --git a/sequencing-server/src/backgroundTranspiler.ts b/sequencing-server/src/backgroundTranspiler.ts new file mode 100644 index 0000000000..f78863f882 --- /dev/null +++ b/sequencing-server/src/backgroundTranspiler.ts @@ -0,0 +1,143 @@ +import { piscina, graphqlClient, typeCheckingCache, promiseThrottler } from './app.js'; +import { objectCacheKeyFunction } from './lib/batchLoaders/index.js'; +import crypto from 'crypto'; +import { generateTypescriptForGraphQLActivitySchema } from './lib/codegen/ActivityTypescriptCodegen.js'; +import DataLoader from 'dataloader'; +import { activitySchemaBatchLoader } from './lib/batchLoaders/activitySchemaBatchLoader.js'; +import { commandDictionaryTypescriptBatchLoader } from './lib/batchLoaders/commandDictionaryTypescriptBatchLoader.js'; +import type { typecheckExpansion } from './worker'; +import { Result } from '@nasa-jpl/aerie-ts-user-code-runner/build/utils/monads.js'; +import { getLatestCommandDictionary, getLatestMissionModel, getExpansionRule } from './utils/hasura.js'; + +export async function backgroundTranspiler(numberOfThreads: number = 2) { + if (graphqlClient === null) { + return; + } + + // Fetch latest mission model + const { mission_model_aggregate } = await getLatestMissionModel(graphqlClient); + if (!mission_model_aggregate) { + console.log( + '[ Background Transpiler ] Unable to fetch the latest mission model. Aborting background transpiling...', + ); + return; + } + + // Fetch latest command dictionary + const { command_dictionary_aggregate } = await getLatestCommandDictionary(graphqlClient); + if (!command_dictionary_aggregate) { + console.log( + '[ Background Transpiler ] Unable to fetch the latest command dictionary. Aborting background transpiling...', + ); + return; + } + + const commandDictionaryId = command_dictionary_aggregate.aggregate.max.id; + const missionModelId = mission_model_aggregate.aggregate.max.id; + const { expansion_rule } = await getExpansionRule(graphqlClient, missionModelId, commandDictionaryId); + + if (expansion_rule === null || expansion_rule.length === 0) { + console.log(`[ Background Transpiler ] No expansion rules to transpile.`); + return; + } + + const commandTypescriptDataLoader = new DataLoader(commandDictionaryTypescriptBatchLoader({ graphqlClient }), { + cacheKeyFn: objectCacheKeyFunction, + name: null, + }); + const activitySchemaDataLoader = new DataLoader(activitySchemaBatchLoader({ graphqlClient }), { + cacheKeyFn: objectCacheKeyFunction, + name: null, + }); + + const commandTypes = await commandTypescriptDataLoader.load({ + dictionaryId: missionModelId, + }); + + if (commandTypes === null) { + console.log(`[ Background Transpiler ] Unable to fetch command ts lib. + Aborting transpiling...`); + return; + } + + // only process 'numberOfThreads' worth at a time ex. transpile 2 logics at a time + // This allows for expansion set and sequence expansion to utilize the remaining workers + for (let i = 0; i < expansion_rule.length; i += numberOfThreads) { + await Promise.all( + expansion_rule.slice(i, i + numberOfThreads).map(async expansion => { + await promiseThrottler.run(async () => { + // Assuming expansion_rule elements have the same type + if (expansion instanceof Error) { + console.log(`[ Background Transpiler ] Expansion: ${expansion.name} could not be loaded`, expansion); + return Promise.reject(`Expansion: ${expansion.name} could not be loaded`); + } + + const hash = crypto + .createHash('sha256') + .update( + JSON.stringify({ + commandDictionaryId, + missionModelId, + id: expansion.id, + expansionLogic: expansion.expansion_logic, + activityType: expansion.activity_type, + }), + ) + .digest('hex'); + + // ignore already transpiled hash info + if (typeCheckingCache.has(hash)) { + return Promise.resolve(); + } + + const activitySchema = await activitySchemaDataLoader.load({ + missionModelId, + activityTypeName: expansion.activity_type, + }); + + // log error + if (!activitySchema) { + console.log( + `[ Background Transpiler ] Activity schema for ${expansion.activity_type} could not be loaded`, + activitySchema, + ); + return Promise.reject('Activity schema for ${expansion.activity_type} could not be loaded'); + } + + const activityTypescript = generateTypescriptForGraphQLActivitySchema(activitySchema); + + // log error + if (!activityTypescript) { + console.log( + `[ Background Transpiler ] Unable to generate typescript for activity ${expansion.activity_type}`, + activityTypescript, + ); + return Promise.reject(`Unable to generate typescript for activity ${expansion.activity_type}`); + } + + const typecheckingResult = ( + piscina.run( + { + expansionLogic: expansion.expansion_logic, + commandTypes: commandTypes, + activityTypes: activityTypescript, + activityTypeName: expansion.activity_type, + }, + { name: 'typecheckExpansion' }, + ) as ReturnType + ).then(Result.fromJSON); + + //Display any errors + typecheckingResult.then(result => { + if (result.isErr()) { + console.log(`Error transpiling ${expansion.activity_type}:\n ${result.unwrapErr().map(e => e.message)}`); + } + }); + + typeCheckingCache.set(hash, typecheckingResult); + return typecheckingResult; + }); + }), + ); + } +} diff --git a/sequencing-server/src/utils/hasura.ts b/sequencing-server/src/utils/hasura.ts index 8b92280007..29beeb6f58 100644 --- a/sequencing-server/src/utils/hasura.ts +++ b/sequencing-server/src/utils/hasura.ts @@ -32,7 +32,7 @@ export const ENDPOINTS_WHITELIST = new Set([ '/seqjson/get-seqjson-for-sequence-standalone', '/seqjson/get-seqjson-for-seqid-and-simulation-dataset', '/seqjson/bulk-get-edsl-for-seqjson', - '/seqjson/get-edsl-for-seqjson' + '/seqjson/get-edsl-for-seqjson', ]); /** @@ -368,3 +368,97 @@ async function getMissionModelId( return missionModelId; } + +export async function getLatestMissionModel(graphqlClient: GraphQLClient): Promise<{ + mission_model_aggregate: { + aggregate: { + max: { + id: number; + }; + }; + }; +}> { + return graphqlClient.request<{ + mission_model_aggregate: { + aggregate: { + max: { + id: number; + }; + }; + }; + }>( + gql` + query GetLatestMissionModel { + mission_model_aggregate(order_by: { uploaded_file: { modified_date: asc } }) { + aggregate { + max { + id + } + } + } + } + `, + ); +} + +export async function getLatestCommandDictionary(graphqlClient: GraphQLClient): Promise<{ + command_dictionary_aggregate: { + aggregate: { + max: { + id: number; + }; + }; + }; +}> { + return graphqlClient.request<{ + command_dictionary_aggregate: { + aggregate: { + max: { + id: number; + }; + }; + }; + }>( + gql` + query GetLatestCommandDictionary { + command_dictionary_aggregate(order_by: { created_at: asc }) { + aggregate { + max { + id + } + } + } + } + `, + ); +} + +export async function getExpansionRule( + graphqlClient: GraphQLClient, + missionModelId: number, + commandDictionaryId: number, +): Promise<{ + expansion_rule: { + id: number; + activity_type: string; + expansion_logic: string; + }[]; +}> { + return graphqlClient.request<{ + expansion_rule: { + id: number; + activity_type: string; + expansion_logic: string; + }[]; + }>( + gql` + query GetExpansonLogic { + expansion_rule(where: {authoring_command_dict_id: {_eq: ${missionModelId}}, authoring_mission_model_id: {_eq: ${commandDictionaryId} }}) { + id + activity_type + expansion_logic + } + } + `, + ); +} From 3743ba72d5ebcfdee6c357f2cce45d48f75a824d Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Thu, 15 Feb 2024 06:06:47 -1000 Subject: [PATCH 087/159] Hook up the background transpiler. *Triggers the transpilation process upon server startup and at regular intervals (every 5 minutes). --- sequencing-server/src/app.ts | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/sequencing-server/src/app.ts b/sequencing-server/src/app.ts index dd1370f406..975259333f 100644 --- a/sequencing-server/src/app.ts +++ b/sequencing-server/src/app.ts @@ -27,6 +27,7 @@ import { getHasuraSession, canUserPerformAction, ENDPOINTS_WHITELIST } from './u import type { Result } from '@nasa-jpl/aerie-ts-user-code-runner/build/utils/monads'; import type { CacheItem, UserCodeError } from '@nasa-jpl/aerie-ts-user-code-runner'; import { PromiseThrottler } from './utils/PromiseThrottler.js'; +import { backgroundTranspiler } from './backgroundTranspiler.js'; const logger = getLogger('app'); @@ -39,7 +40,9 @@ app.use(bodyParser.json({ limit: '100mb' })); DbExpansion.init(); export const db = DbExpansion.getDb(); - +export let graphqlClient = new GraphQLClient(getEnv().MERLIN_GRAPHQL_URL, { + headers: { 'x-hasura-admin-secret': getEnv().HASURA_GRAPHQL_ADMIN_SECRET }, +}); export const piscina = new Piscina({ filename: new URL('worker.js', import.meta.url).pathname, minThreads: parseInt(getEnv().SEQUENCING_WORKER_NUM), @@ -62,10 +65,6 @@ export type Context = { }; app.use(async (req: Request, res: Response, next: NextFunction) => { - const graphqlClient = new GraphQLClient(getEnv().MERLIN_GRAPHQL_URL, { - headers: { 'x-hasura-admin-secret': getEnv().HASURA_GRAPHQL_ADMIN_SECRET }, - }); - // Check and make sure the user making the request has the required permissions. if ( !ENDPOINTS_WHITELIST.has(req.url) && @@ -247,4 +246,30 @@ app.listen(PORT, () => { logger.info(`Worker pool initialized: Total workers started: ${piscina.threads.length}, Heap Size per Worker: ${getEnv().SEQUENCING_MAX_WORKER_HEAP_MB} MB`); + + if (getEnv().TRANSPILER_ENABLED === 'true') { + //log that the tranpiler is on + logger.info(`Background Transpiler is 'on'`); + + let transpilerPromise: Promise | undefined; // Holds the transpilation promise + async function invokeTranspiler() { + try { + await backgroundTranspiler(); + } catch (error) { + console.error('Error during transpilation:', error); + } finally { + transpilerPromise = undefined; // Reset promise after completion + } + } + + // Immediately call the background transpiler + transpilerPromise = invokeTranspiler(); + + // Schedule next execution after 2 minutes, handling ongoing transpilation + setInterval(async () => { + if (!transpilerPromise) { + transpilerPromise = invokeTranspiler(); // Start a new transpilation + } + }, 60 * 2 * 1000); + } }); From 592f1c28549a8cf62279e67f48ff0576315ece9d Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Fri, 1 Mar 2024 13:33:12 -0800 Subject: [PATCH 088/159] add test for validations without mission models --- .../aerie/e2e/AutomaticValidationTests.java | 26 +++++++++++++++++++ .../aerie/e2e/types/ActivityValidation.java | 5 ++++ .../gov/nasa/jpl/aerie/e2e/utils/GQL.java | 9 +++++++ .../jpl/aerie/e2e/utils/HasuraRequests.java | 10 +++++++ 4 files changed, 50 insertions(+) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java index d59018b489..60b732917d 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.TestInstance; import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonValue; import java.io.IOException; import java.util.List; @@ -130,6 +132,30 @@ void instantiationError() throws IOException, InterruptedException { ), activityValidation); } + @Test + void noSuchMissionModelError() throws IOException, InterruptedException { + final var activityId = hasura.insertActivity( + planId, + "BiteBanana", + "1h", + JsonValue.EMPTY_JSON_OBJECT + ); + Thread.sleep(1000); // TODO consider a while loop here + + hasura.deleteMissionModel(modelId); + + final var arguments = Json.createObjectBuilder().add("biteSize", 2).build(); + hasura.updateActivityDirectiveArguments(planId, activityId, arguments); + Thread.sleep(1000); // TODO consider a while loop here + + final var activityValidations = hasura.getActivityValidations(planId); + final ActivityValidation activityValidation = activityValidations.get((long) activityId); + assertEquals( + new ActivityValidation.NoSuchMissionModelFailure("no such mission model", "0"), + activityValidation + ); + } + @Test void exceptionDuringValidationHandled() throws IOException, InterruptedException { final var exceptionActivityId = hasura.insertActivity( diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ActivityValidation.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ActivityValidation.java index 599e84d394..63fd59f92d 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ActivityValidation.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/ActivityValidation.java @@ -11,6 +11,7 @@ record Success() implements ActivityValidation {} record InstantiationFailure(List extraneousArguments, List missingArguments, List unconstructableArguments) implements ActivityValidation {} record ValidationFailure(List notices) implements ActivityValidation {} record NoSuchActivityTypeFailure(String message, String activityType) implements ActivityValidation {} + record NoSuchMissionModelFailure(String message, String modelId) implements ActivityValidation {} record ValidationNotice(List subjects, String message) { } record UnconstructableArgument(String name, String failure) { } @@ -44,6 +45,10 @@ static ActivityValidation fromJSON(JsonObject obj) { getStringArray($, "subjects"), $.asJsonObject().getString("message")))); case "NO_SUCH_ACTIVITY_TYPE" -> new NoSuchActivityTypeFailure(errors.getJsonObject("noSuchActivityError").getString("message"), errors.getJsonObject("noSuchActivityError").getString("activity_type")); + case "NO_SUCH_MISSION_MODEL" -> new NoSuchMissionModelFailure( + errors.getJsonObject("noSuchMissionModelError").getString("message"), + errors.getJsonObject("noSuchMissionModelError").getString("mission_model_id") + ); default -> throw new RuntimeException("Unhandled error type: " + type); }; } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index e2f6ace7ec..401a6ef8af 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -501,6 +501,15 @@ query Simulate($plan_id: Int!) { simulationDatasetId } }"""), + UPDATE_ACTIVITY_DIRECTIVE_ARGUMENTS(""" + mutation updateActivityDirectiveArguments($id: Int!, $plan_id: Int!, $arguments: jsonb!) { + updateActivityDirectiveArguments: update_activity_directive_by_pk( + pk_columns: {id: $id, plan_id: $plan_id}, + _set: {arguments: $arguments} + ) { + id + } + }"""), UPDATE_CONSTRAINT(""" mutation updateConstraint($constraintId: Int!, $constraintDefinition: String!) { constraint: insert_constraint_definition_one(object: {constraint_id: $constraintId, definition: $constraintDefinition}) { diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index aa5bf80841..d854205198 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -192,6 +192,16 @@ public int insertActivity(int planId, String type, String startOffset, JsonObjec return makeRequest(GQL.CREATE_ACTIVITY_DIRECTIVE, variables).getJsonObject("createActivityDirective").getInt("id"); } + public void updateActivityDirectiveArguments(int planId, int activityId, JsonObject arguments) throws IOException { + final var variables = Json.createObjectBuilder() + .add("plan_id", planId) + .add("id", activityId) + .add("arguments", arguments) + .build(); + + makeRequest(GQL.UPDATE_ACTIVITY_DIRECTIVE_ARGUMENTS, variables); + } + public void deleteActivity(int planId, int activityId) throws IOException { final var variables = Json.createObjectBuilder() .add("plan_id", planId) From 2cd63a4872f8322344583a61ffd237399dc305cf Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Fri, 1 Mar 2024 13:25:53 -0800 Subject: [PATCH 089/159] serialize `NoSuchMissionModel` exception out in activity validations --- .../aerie/merlin/server/http/ResponseSerializers.java | 8 ++++++++ .../merlin/server/services/LocalMissionModelService.java | 9 ++++++++- .../merlin/server/services/MissionModelService.java | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java index 6935fc4604..ff99b17c42 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java @@ -191,6 +191,14 @@ public static JsonValue serializeBulkArgumentValidationResponse(BulkArgumentVali return Json.createObjectBuilder(serializeInstantiationException(f.ex()).asJsonObject()) .add("type", "INSTANTIATION_ERRORS") .build(); + } else if (response instanceof BulkArgumentValidationResponse.NoSuchMissionModelError m) { + return Json.createObjectBuilder() + .add("errors", Json.createObjectBuilder() + .add("noSuchMissionModelError", serializeNoSuchMissionModelException(m.ex())) + .build()) + .add("success", JsonValue.FALSE) + .add("type", "NO_SUCH_MISSION_MODEL") + .build(); } // This should never happen, but we don't have exhaustive pattern matching diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index ba5873a833..91753212c0 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -136,7 +136,14 @@ public List validateActivityArgumentsBulk( final List activities ) throws NoSuchMissionModelException, MissionModelLoadException { // load mission model once for all activities - final var modelType = this.loadMissionModelType(modelId.toString()); + ModelType modelType; + try { + modelType = this.loadMissionModelType(modelId.toString()); + } catch (NoSuchMissionModelException e) { + return activities.stream() + .map(directive -> new BulkArgumentValidationResponse.NoSuchMissionModelError(e)) + .collect(Collectors.toList()); + } final var registry = DirectiveTypeRegistry.extract(modelType); // map all directives to validation response diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java index 6d18cf9229..8d9203fe8c 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/MissionModelService.java @@ -105,6 +105,7 @@ record InstantiationFailure(InstantiationException ex) implements BulkEffective sealed interface BulkArgumentValidationResponse { record Success() implements BulkArgumentValidationResponse { } record Validation(List notices) implements BulkArgumentValidationResponse { } + record NoSuchMissionModelError(NoSuchMissionModelException ex) implements BulkArgumentValidationResponse { } record NoSuchActivityError(NoSuchActivityTypeException ex) implements BulkArgumentValidationResponse { } record InstantiationError(InstantiationException ex) implements BulkArgumentValidationResponse { } } From 57ea272130f5c12cc8d56942381edce9ab3e8ec7 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Fri, 1 Mar 2024 13:32:48 -0800 Subject: [PATCH 090/159] remove `NoSuchMissionModel` exception catch in `workerLoop` --- .../aerie/merlin/server/services/LocalMissionModelService.java | 2 ++ .../jpl/aerie/merlin/server/services/ValidationWorker.java | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 91753212c0..27109ea0d1 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -139,6 +139,8 @@ public List validateActivityArgumentsBulk( ModelType modelType; try { modelType = this.loadMissionModelType(modelId.toString()); + // try and catch NoSuchMissionModel here, so we can serialize it out to each activity validation + // rather than catching it at a higher level in the workerLoop itself } catch (NoSuchMissionModelException e) { return activities.stream() .map(directive -> new BulkArgumentValidationResponse.NoSuchMissionModelError(e)) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java index 62ff4a87bb..b23be08c9c 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java @@ -49,9 +49,6 @@ public void workerLoop() { final var duration = (endTime - beginTime) / 1_000_000.0; logger.debug("processed model batch of size {} in {} ms", unvalidatedDirectives.size(), duration); } - - } catch (NoSuchMissionModelException ex) { - logger.error("Validation request failed due to no such mission model: {}", ex.toString()); } catch (InterruptedException ex) { // we were interrupted, so exit gracefully return; From e72a50a2641f8a89225c88a234c079b3fbd82c92 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Fri, 1 Mar 2024 14:19:47 -0800 Subject: [PATCH 091/159] catch `MissionModelLoadException` within validation model batch --- .../gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java | 1 - .../remotes/postgres/GetUnvalidatedDirectivesAction.java | 1 - .../merlin/server/services/LocalMissionModelService.java | 7 +++++-- .../jpl/aerie/merlin/server/services/ValidationWorker.java | 1 - 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java index 60b732917d..b86b03fe57 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/AutomaticValidationTests.java @@ -12,7 +12,6 @@ import org.junit.jupiter.api.TestInstance; import javax.json.Json; -import javax.json.JsonObject; import javax.json.JsonValue; import java.io.IOException; import java.util.List; diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetUnvalidatedDirectivesAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetUnvalidatedDirectivesAction.java index 0697906f63..1439ac1e74 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetUnvalidatedDirectivesAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/GetUnvalidatedDirectivesAction.java @@ -5,7 +5,6 @@ import gov.nasa.jpl.aerie.merlin.server.models.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.server.models.MissionModelId; import gov.nasa.jpl.aerie.merlin.server.models.PlanId; -import gov.nasa.jpl.aerie.merlin.server.models.Timestamp; import java.sql.Connection; import java.sql.PreparedStatement; diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 27109ea0d1..6c1c2b4c65 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -133,8 +133,7 @@ public List validateActivityArguments(final String missionMode public List validateActivityArgumentsBulk( final MissionModelId modelId, - final List activities - ) throws NoSuchMissionModelException, MissionModelLoadException { + final List activities) { // load mission model once for all activities ModelType modelType; try { @@ -145,6 +144,10 @@ public List validateActivityArgumentsBulk( return activities.stream() .map(directive -> new BulkArgumentValidationResponse.NoSuchMissionModelError(e)) .collect(Collectors.toList()); + } catch (MissionModelLoadException e) { + log.error("Caught MissionModelLoadException, skipping this batch but leaving validations pending..."); + log.error(e.toString()); + return List.of(); } final var registry = DirectiveTypeRegistry.extract(modelType); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java index b23be08c9c..b2d55afe23 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ValidationWorker.java @@ -1,7 +1,6 @@ package gov.nasa.jpl.aerie.merlin.server.services; import gov.nasa.jpl.aerie.merlin.server.models.ActivityDirectiveForValidation; -import gov.nasa.jpl.aerie.merlin.server.services.MissionModelService.NoSuchMissionModelException; import gov.nasa.jpl.aerie.merlin.server.services.MissionModelService.BulkArgumentValidationResponse; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; From 126a2e6f8bc00c82cb6df39651a71172e56d525b Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Fri, 1 Mar 2024 14:26:35 -0800 Subject: [PATCH 092/159] reorder `NoSuchMissionModelError` JSON entry insert order to match existing serializers --- .../jpl/aerie/merlin/server/http/ResponseSerializers.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java index ff99b17c42..f9bf187f58 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/ResponseSerializers.java @@ -193,11 +193,11 @@ public static JsonValue serializeBulkArgumentValidationResponse(BulkArgumentVali .build(); } else if (response instanceof BulkArgumentValidationResponse.NoSuchMissionModelError m) { return Json.createObjectBuilder() + .add("success", JsonValue.FALSE) + .add("type", "NO_SUCH_MISSION_MODEL") .add("errors", Json.createObjectBuilder() .add("noSuchMissionModelError", serializeNoSuchMissionModelException(m.ex())) .build()) - .add("success", JsonValue.FALSE) - .add("type", "NO_SUCH_MISSION_MODEL") .build(); } From 2347c833124a849b63523fb729a92f77caa6aab6 Mon Sep 17 00:00:00 2001 From: joswig Date: Mon, 4 Mar 2024 17:08:13 +0000 Subject: [PATCH 093/159] Release v2.5.0 --- gradle.properties | 2 +- sequencing-server/package-lock.json | 4 ++-- sequencing-server/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index b182f7909b..73e2c71b4d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ publishing.version= # Override for releases # Change the version number here -version.number=2.4.0 +version.number=2.5.0 # If you are publishing a release *manually* (i.e. not through github actions), # override this on the command line with `./gradlew publish -Pversion.isRelease=true`. diff --git a/sequencing-server/package-lock.json b/sequencing-server/package-lock.json index 2ecde0deb8..49a80b1c0a 100644 --- a/sequencing-server/package-lock.json +++ b/sequencing-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "sequencing-server", - "version": "2.4.0", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sequencing-server", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "dependencies": { "@js-temporal/polyfill": "~0.4.3", diff --git a/sequencing-server/package.json b/sequencing-server/package.json index 75ff7d7534..6dfaa2f26f 100644 --- a/sequencing-server/package.json +++ b/sequencing-server/package.json @@ -1,6 +1,6 @@ { "name": "sequencing-server", - "version": "2.4.0", + "version": "2.5.0", "description": "Aerie sequencing server", "type": "module", "license": "MIT", From 7bde422c035387dc69c31ddb1cf8734308cfdf0d Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 4 Mar 2024 08:26:17 -0800 Subject: [PATCH 094/159] Update array `scheduling_specifications` relationship to object relationship Scheduling Specs have had an object relationship with the Plans table since [#711](https://github.com/NASA-AMMOS/aerie/pull/711) --- .../metadata/databases/AerieMerlin/tables/public_plan.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml index 4c014fc640..3cb415ff5a 100644 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_plan.yaml @@ -67,10 +67,10 @@ array_relationships: name: plan schema: public remote_relationships: -- name: scheduling_specifications +- name: scheduling_specification definition: to_source: - relationship_type: array + relationship_type: object source: AerieScheduler table: schema: public From 8977a4fbc02010d49f3ddeaf9c60177a9f18a50a Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 4 Mar 2024 08:30:39 -0800 Subject: [PATCH 095/159] Update GQL query in E2E Tests --- e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index 401a6ef8af..68554dedff 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -315,7 +315,7 @@ query GetPlan($id: Int!) { } name revision - scheduling_specifications { + scheduling_specification { id } simulations { From 2ee8276318cf481af66d530ec46befca7765c5c1 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 13 Mar 2024 17:06:29 -0700 Subject: [PATCH 096/159] update env vars --- deployment/docker-compose.yml | 2 +- docker-compose.yml | 1 - e2e-tests/docker-compose-many-workers.yml | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 75294c3fc4..2733516e79 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -35,6 +35,7 @@ services: MERLIN_DB_USER: "${AERIE_USERNAME}" MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store MERLIN_PORT: 27183 + ENABLE_CONTINUOUS_VALIDATION_THREAD: true JAVA_OPTS: > -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN -Dorg.slf4j.simpleLogger.logFile=System.err @@ -143,7 +144,6 @@ services: environment: ORIGIN: http://localhost PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/" - PUBLIC_LOGIN_PAGE: "enabled" PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000 PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000 PUBLIC_HASURA_CLIENT_URL: http://localhost:8080/v1/graphql diff --git a/docker-compose.yml b/docker-compose.yml index 9df4a80fea..a1c323f1ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,6 @@ services: environment: NODE_TLS_REJECT_UNAUTHORIZED: "0" PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/" - PUBLIC_LOGIN_PAGE: "enabled" ORIGIN: http://localhost PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000 PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000 diff --git a/e2e-tests/docker-compose-many-workers.yml b/e2e-tests/docker-compose-many-workers.yml index e0a8277254..a936866e93 100644 --- a/e2e-tests/docker-compose-many-workers.yml +++ b/e2e-tests/docker-compose-many-workers.yml @@ -104,7 +104,6 @@ services: environment: NODE_TLS_REJECT_UNAUTHORIZED: "0" PUBLIC_AERIE_FILE_STORE_PREFIX: "/usr/src/app/merlin_file_store/" - PUBLIC_LOGIN_PAGE: "enabled" ORIGIN: http://localhost PUBLIC_GATEWAY_CLIENT_URL: http://localhost:9000 PUBLIC_GATEWAY_SERVER_URL: http://aerie_gateway:9000 From 69a73f9045a68c943a4a73116bfded88cdac0e01 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 10:53:16 -0800 Subject: [PATCH 097/159] Timeline package build infrastructure --- settings.gradle | 1 + timeline/build.gradle | 62 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 timeline/build.gradle diff --git a/settings.gradle b/settings.gradle index 48b2422579..95d0c6ce7e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ include 'contrib' // Service support include 'parsing-utilities' include 'permissions' +include 'timeline' // Services for deployment within the Aerie infrastructure include 'merlin-server' diff --git a/timeline/build.gradle b/timeline/build.gradle new file mode 100644 index 0000000000..478ff92968 --- /dev/null +++ b/timeline/build.gradle @@ -0,0 +1,62 @@ +import org.jetbrains.kotlin.gradle.dsl.jvm.JvmTargetValidationMode +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' + id "org.jetbrains.kotlin.jvm" version "1.9.22" + id 'java-library' + id 'org.jetbrains.dokka' version '1.9.10' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':merlin-driver') + implementation project(':merlin-sdk') + implementation project(':parsing-utilities') + + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' + testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + +// dokkaHtmlPlugin 'org.jetbrains.dokka:kotlin-as-java-plugin:1.9.10' +} + +tasks.withType(KotlinJvmCompile.class).configureEach { + jvmTargetValidationMode = JvmTargetValidationMode.WARNING +} + +tasks.named('test') { + useJUnitPlatform() +} + +kotlin { + jvmToolchain(21) +} + +dokkaHtml.configure { + dokkaSourceSets { + named("main") { + // used as project name in the header + moduleName.set("Timeline") + + reportUndocumented.set(true) + failOnWarning.set(true) + + // contains descriptions for the module and the packages + includes.from("MODULE_DOCS.md") + + // adds source links that lead to this repository, allowing readers + // to easily find source code for inspected declarations + sourceLink.configure { + localDirectory.set(file("timeline/src/main/kotlin")) + remoteUrl.set(URL( + "https://github.com/NASA-AMMOS/aerie/blob/feat/java-timeline-library/timeline/src/main/kotlin" + )) + remoteLineSuffix.set("#L") + } + } + } +} From 83a1828a52631d6e030dddf99960f6c0786f275d Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:44:05 -0800 Subject: [PATCH 098/159] Lowest level data structures (BaseTimeline, Interval, etc) --- .../nasa/jpl/aerie/timeline/BaseTimeline.kt | 17 + .../jpl/aerie/timeline/BoundsTransformer.kt | 28 ++ .../nasa/jpl/aerie/timeline/CollectOptions.kt | 19 + .../gov/nasa/jpl/aerie/timeline/Duration.kt | 245 +++++++++++++ .../gov/nasa/jpl/aerie/timeline/Interval.kt | 326 ++++++++++++++++++ .../gov/nasa/jpl/aerie/timeline/Timeline.kt | 58 ++++ .../aerie/timeline/payloads/IntervalLike.kt | 16 + .../aerie/timeline/BoundsTransformerTest.kt | 18 + .../nasa/jpl/aerie/timeline/IntervalTest.kt | 195 +++++++++++ 9 files changed, 922 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt new file mode 100644 index 0000000000..652ee64f18 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BaseTimeline.kt @@ -0,0 +1,17 @@ +package gov.nasa.jpl.aerie.timeline + +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike + +/** + * The basic timeline container that all higher-level timeline collections ultimately delegate to. + * + * Only the most extreme power-users should ever need to construct this manually. + */ +data class BaseTimeline, TL: Timeline>( + override val ctor: (Timeline) -> TL, + private val collector: (CollectOptions) -> List +): Timeline { + override fun collect(opts: CollectOptions) = collector(opts) + override fun > unsafeCast(ctor: (Timeline) -> RESULT) = + BaseTimeline(ctor, collector).specialize() +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt new file mode 100644 index 0000000000..5d315b5f07 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt @@ -0,0 +1,28 @@ +package gov.nasa.jpl.aerie.timeline + +/** A functional interface for transforming bounds for operations that transform intervals. */ +fun interface BoundsTransformer { + /** + * Convert the given bounds into the bounds that the child timeline should be collected with. + * + * This is typically the inverse of the motion caused by the operation itself. For example, + * if the operation shifts all intervals to the future by `Duration.SECOND`, this method should + * shift the bounds to the past by `Duration.SECOND`. + */ + operator fun invoke(bounds: Interval): Interval + + /** Helper functions for constructing bounds transformers. */ + companion object { + /** Does nothing. Used for operations that don't need to change the bounds. */ + @JvmStatic + val IDENTITY: BoundsTransformer = BoundsTransformer { i -> i } + + /** + * Creates a bounds transformer for the simple case of uniformly shifting the bounds. + * + * @param dur the duration that the INTERVALS are being shifted by; it is negated for you. + */ + @JvmStatic + fun shift(dur: Duration) = BoundsTransformer { i -> i.shiftBy(dur.negate()) } + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt new file mode 100644 index 0000000000..c3aa3af291 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt @@ -0,0 +1,19 @@ +package gov.nasa.jpl.aerie.timeline + +/** + * Options for collecting a timeline. + */ +data class CollectOptions( + /** The bounds on which to evaluate the timeline. */ + val bounds: Interval, + + /** + * Whether to truncate objects that extend outside the bounds. + * + * Objects with no intersection with the bounds should never be included in the results. + */ + val truncateMarginal: Boolean = true +) { + /** Creates a new options object with a [BoundsTransformer] applied. */ + fun transformBounds(boundsTransformer: BoundsTransformer) = CollectOptions(boundsTransformer(bounds), truncateMarginal) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt new file mode 100644 index 0000000000..dcfa5e1af4 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt @@ -0,0 +1,245 @@ +package gov.nasa.jpl.aerie.timeline + +import java.time.Instant +import java.time.temporal.ChronoUnit +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.round + +/** + * A relative duration of time, represented as a long integer of microseconds. + */ +data class Duration(private val micros: Long) : Comparable { + + /** Returns the negative of this duration. */ + fun negate() = Duration(-micros) + /** @see negate */ + operator fun unaryMinus() = negate() + + /** Adds another duration to this. */ + operator fun plus(other: Duration) = Duration(micros + other.micros) + + /** Adds this to an instant, to produce another instant. */ + operator fun plus(instant: Instant) = instant + this + + /** Add this and another duration, saturating at the maximum and minimum bounds. */ + infix fun saturatingPlus(other: Duration) = Duration(saturatingAddInternal(micros, other.micros)) + + /** Subtracts another duration from this. */ + operator fun minus(other: Duration) = Duration(micros - other.micros) + + /** Subtract another duration from this, saturating at the maximum and minimum bounds. */ + infix fun saturatingMinus(other: Duration) = saturatingPlus(-other) + + /** Multiplies this by a long scalar. */ + operator fun times(scalar: Long) = Duration(micros * scalar) + /** Multiplies this by a double scalar. */ + operator fun times(scalar: Double) = roundNearest(scalar, this) + + /** + * Divides this by a duration divisor to produce a long, rounded down. + * + * To calculate this division as a double, use [ratioOver]. + */ + operator fun div(divisor: Duration) = micros / divisor.micros + /** Divides this by a long divisor. */ + operator fun div(divisor: Long) = Duration(micros / divisor) + /** Divides this by a double divisor. */ + operator fun div(divisor: Double) = roundNearest(1 / divisor, this) + + /** Remainder division between this and another duration. */ + operator fun rem(divisor: Duration) = Duration(micros % divisor.micros) + + /** Computes the ratio of this over another duration, as a double. */ + infix fun ratioOver(unit: Duration): Double { + val integralPart = micros / unit.micros + val fractionalPart = ((micros % unit.micros).toDouble() + / unit.micros.toDouble()) + return integralPart + fractionalPart + } + + /** Whether this is shorter than another duration. */ + infix fun shorterThan(other: Duration) = this < other + + /** Whether this is shorter than or equal to another duration. */ + infix fun shorterThanOrEqualTo(other: Duration) = this <= other + + /** Whether this is longer than another duration. */ + infix fun longerThan(other: Duration) = this > other + + /** Whether this is longer than or equal to another duration. */ + infix fun longerThanOrEqualTo(other: Duration) = this >= other + + /** Creates an interval from this to another duration, inclusive. */ + operator fun rangeTo(other: Duration) = Interval.between(this, other) + + /** Creates an interval from this to another duration, including the start but excluding the end. */ + operator fun rangeUntil(other: Duration) = Interval.betweenClosedOpen(this, other) + + /** Whether this duration is negative. */ + fun isNegative() = micros < 0 + + /** Whether this duration is positive. */ + fun isPositive() = micros > 0 + + /** Whether this duration is equal to zero. */ + fun isZero() = micros == 0L + + /** Converts this duration to an ISO 8601 duration string. */ + fun toISO8601() = java.time.Duration.of(micros, ChronoUnit.MICROS).toString() + + /** + * Obtain a human-readable representation of this duration. + * + * For example, `duration(-5, MINUTES).plus(1, SECOND).plus(2, MICROSECONDS)` is rendered as "-00:04:58.999998". + */ + override fun toString(): String { + var rest = this + val sign = if (isNegative()) "-" else "+" + val hours: Long + if (isNegative()) { + hours = -(rest / HOUR) + rest = -(rest % HOUR) + } else { + hours = rest / HOUR + rest %= HOUR + } + val minutes = rest / MINUTE + rest %= MINUTE + val seconds = rest / SECOND + rest %= SECOND + val microseconds = rest / MICROSECOND + return String.format("%s%02d:%02d:%02d.%06d", sign, hours, minutes, seconds, microseconds) + } + + /** Determine whether this duration is greater than, less than, or equal to another duration. */ + override fun compareTo(other: Duration) = micros.compareTo(other.micros) + + /***/ companion object { + /** + * The smallest observable span of time between instants. + * + * This quantity can be used to obtain the smallest representable deviation from a given instant, i.e. + * `instant.minus(EPSILON)` and `instant.plus(EPSILON)`. The precise deviation from `instant` should not be assumed, + * except that it is no larger than [Duration.MICROSECOND]. + * + */ + @JvmStatic val EPSILON = Duration(1) + + /** + * The empty span of time. + */ + @JvmStatic val ZERO = Duration(0) + + /** + * The largest observable negative span of time. Attempting to go "more negative" will cause an exception. + * + * The value of this quantity should not be assumed. + * Currently, this is precisely -9,223,372,036,854,775,808 microseconds, or approximately -293,274 years. + * + */ + @JvmStatic val MIN_VALUE = Duration(Long.MIN_VALUE) + + /** + * The largest observable positive span of time. Attempting to go "more positive" will cause an exception. + * + * The value of this quantity should not be assumed. + * Currently, this is precisely +9,223,372,036,854,775,807 microseconds, or approximately 293,274 years. + * + */ + @JvmStatic val MAX_VALUE = Duration(Long.MAX_VALUE) + + /** One microsecond (μs). */ + @JvmStatic val MICROSECOND = Duration(1) + + /** One millisecond (ms), equal to 1000μs. */ + @JvmStatic val MILLISECOND = MICROSECOND * 1000 + + /** One second (s), equal to 1000ms. */ + @JvmStatic val SECOND = MILLISECOND * 1000 + + /** One minute (m), equal to 60s. */ + @JvmStatic val MINUTE = SECOND * 60 + + /** One hour (h), equal to 60m. */ + @JvmStatic val HOUR = MINUTE * 60 + + /** One hour (d), equal to 24h. */ + @JvmStatic val DAY = HOUR * 24 + + /** Constructs a duration with a given number of microseconds. */ + @JvmStatic fun microseconds(quantity: Long) = Duration(quantity) + + /** Constructs a duration with a given number of milliseconds. */ + @JvmStatic fun milliseconds(quantity: Long) = MILLISECOND * quantity + + /** Constructs a duration with a given number of seconds. */ + @JvmStatic fun seconds(quantity: Long) = SECOND * quantity + + /** Constructs a duration with a given number of minutes. */ + @JvmStatic fun minutes(quantity: Long) = MINUTE * quantity + + /** Constructs a duration with a given number of hours. */ + @JvmStatic fun hours(quantity: Long) = HOUR * quantity + + /** Constructs a duration with a given number of days. */ + @JvmStatic fun days(quantity: Long) = DAY * quantity + + /** + * Construct a duration in terms of a real quantity of some unit, + * rounding to the nearest representable value above. + */ + @JvmStatic fun roundUpward(quantity: Double, unit: Duration): Duration { + val integerPart = floor(quantity) + val decimalPart = quantity - integerPart + return unit * (integerPart.toLong()) + EPSILON.times(ceil(decimalPart * (unit / EPSILON)).toLong()) + } + + /** + * Construct a duration in terms of a real quantity of some unit, + * rounding to the nearest representable value below. + */ + @JvmStatic fun roundDownward(quantity: Double, unit: Duration): Duration { + val integerPart = floor(quantity) + val decimalPart = quantity - integerPart + return unit * (integerPart.toLong()) + + EPSILON * (floor(decimalPart * (unit / EPSILON)).toLong()) + } + + /** + * Construct a duration in terms of a real quantity of some unit, + * rounding to the nearest representable value. + */ + @JvmStatic fun roundNearest(quantity: Double, unit: Duration): Duration { + val integerPart = floor(quantity) + val decimalPart = quantity - integerPart + return unit * (integerPart.toLong()) + EPSILON * (round(decimalPart * (unit / EPSILON)).toLong()) + } + + private fun saturatingAddInternal(left: Long, right: Long): Long { + val result = left + right + return if (result xor left and (result xor right) < 0) { + Long.MIN_VALUE - (result ushr java.lang.Long.SIZE - 1) + } else result + } + + /** Finds the minimum duration among the arguments. */ + @JvmStatic fun min(vararg durations: Duration) = durations.min() + + /** Finds the minimum duration among the arguments. */ + @JvmStatic fun max(vararg durations: Duration) = durations.max() + + /** Adds a duration to an instant, to produce another instant. */ + @JvmStatic operator fun Instant.plus(duration: Duration): Instant = plusMillis(duration / MILLISECOND) + .plusNanos(1000 * ((duration % MILLISECOND).micros)) + + /** Subtracts a duration from an instant, to produce another instant. */ + @JvmStatic operator fun Instant.minus(other: Instant) = Duration(other.until(this, ChronoUnit.MICROS)) + + /** Parses a duration from a ISO 8601 formatted duration string. */ + @JvmStatic fun parseISO8601(iso8601String: String?): Duration { + val javaDuration = java.time.Duration.parse(iso8601String) + return microseconds(javaDuration.seconds * 1000000L + javaDuration.nano / 1000L) + } + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt new file mode 100644 index 0000000000..68794eae6a --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt @@ -0,0 +1,326 @@ +package gov.nasa.jpl.aerie.timeline + +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike + +/** + * An Interval on the timeline, represented by start and end points + * and start and end inclusivity. + */ +data class Interval( + /***/ val start: Duration, + /***/ val end: Duration, + /** Whether this interval contains its start time. */ + val startInclusivity: Inclusivity = Inclusivity.Inclusive, + /** Whether this interval contains its end time. */ + val endInclusivity: Inclusivity = startInclusivity +): IntervalLike { + + /** Constructs an interval that contains both its endpoints. */ + constructor(start: Duration, end: Duration) : this(start, end, Inclusivity.Inclusive, Inclusivity.Inclusive) + + /** + * Labels to indicate whether an interval includes its endpoints. + */ + enum class Inclusivity { + /***/ Inclusive, + /***/ Exclusive; + + /** Returns the opposite inclusivity. */ + fun opposite(): Inclusivity = if ((this == Inclusive)) Exclusive else Inclusive + /** Returns true if this is `Exclusive` and the argument is `Inclusive`; otherwise false. */ + fun moreRestrictiveThan(other: Inclusivity): Boolean = this == Exclusive && other == Inclusive + } + + /** + * Needed to satisfy the IntervalLike interface. No need to use in a non-polymorphic context. + * + * @suppress + */ + override val interval + get() = this + + /** + * Needed to satisfy the IntervalLike interface. No need to use in a non-polymorphic context. + * + * @suppress + */ + override fun withNewInterval(i: Interval) = i + + /** Whether this interval includes its start point. */ + fun includesStart() = startInclusivity == Inclusivity.Inclusive + /** Whether this interval includes its end point. */ + fun includesEnd() = endInclusivity == Inclusivity.Inclusive + + /** + * Whether this interval contains any points. + * + * An interval can be empty if it ends before it starts (negative duration), + * or if it has zero duration and excludes both of its endpoints. + */ + fun isEmpty() = + if (end < start) true + else if (end > start) false + else !(includesStart() && includesEnd()) + + /** + * Whether this interval contains only a single point. + * + * Use this instead of `.duration.isZero()` to avoid overflow on long intervals. + */ + fun isPoint() = (includesStart() && includesEnd() && (start === end)) + + /** + * Shifts the start and end points equally forward or backward in time. + * + * @param shiftStart Duration to shift the start by. Negative means backward in time. + * @param shiftEnd Duration to shift the end by. Defaults to [shiftStart] + */ + fun shiftBy(shiftStart: Duration, shiftEnd: Duration = shiftStart) = between( + start.saturatingPlus(shiftStart), + end.saturatingPlus(shiftEnd), + startInclusivity, + endInclusivity + ) + + /** Length of the interval, represented as a Duration object. */ + fun duration(): Duration = if(isEmpty()) Duration.ZERO else end - start + + /** Whether this interval happens after another interval with no overlap. */ + infix fun isStrictlyAfter(x: Interval) = compareStartToEnd(x) > 0 + /** Whether this interval happens before another interval with no overlap. */ + infix fun isStrictlyBefore(x: Interval) = compareEndToStart(x) < 0 + + /** + * Compare the start times of this and another interval. + * + * @param other the interval to compare to + * @return -1 if this interval starts first, 1 if the other interval starts first, or 0 if the two intervals start at the same time. + * + * If the start inclusivities are not equal, the inclusive start is considered to be first. + * + * Assumes neither interval is empty. + */ + fun compareStarts(other: Interval): Int { + val timeComparison: Int = start.compareTo(other.start) + return if (timeComparison != 0) timeComparison + else if (startInclusivity == other.startInclusivity) 0 + else if (startInclusivity == Inclusivity.Inclusive) -1 + else 1 + } + + /** + * Compare the end times of this and another interval. + * + * @param other the interval to compare to + * @return -1 if this interval ends first, 1 if the other interval ends first, or 0 if the two intervals end at the same time. + * + * If the end inclusivities are not equal, the exclusive end is considered to be first. + * + * Assumes neither interval is empty. + */ + fun compareEnds(other: Interval): Int { + val timeComparison: Int = end.compareTo(other.end) + return if (timeComparison != 0) timeComparison + else if (endInclusivity == other.endInclusivity) 0 + else if (endInclusivity == Inclusivity.Inclusive) 1 + else -1 + } + + /** Opposite of [compareEndToStart]. */ + fun compareStartToEnd(other: Interval) = -other.compareEndToStart(this) + + /** + * Compares the end of this interval to the start of another interval. + * + * Returns -1 if this ends before the other starts. + * Returns 1 if this ends after (see below) the other starts. + * Returns 0 if this exactly meets (see below) the other with no overlap + * + * To clarify, `compareEndToStart([a, b), [b, c)) == 0`, + * but `compareEndToStart([a, b], [b, c]) == 1`. This might be unintuitive, + * but I've found this to be much more useful in practice than `-1` and `0`, respectively. + * This is because as long as this interval starts before the argument, we get a few properties: + * - -1 indicates a gap between them + * - 1 indicates overlap between them + * - 0 indicates no gap and no overlap + * + * Assumes neither interval is empty. + */ + fun compareEndToStart(other: Interval): Int { + val timeComparison: Int = this.end.compareTo(other.start) + return if (timeComparison != 0) timeComparison + else if (this.endInclusivity != other.startInclusivity) 0 + else if (this.endInclusivity == Inclusivity.Inclusive) 1 + else -1 + } + + /** + * Whether this and another interval start at the same time, accounting for inclusivity. + * + * Assumes neither interval is empty. + */ + infix fun hasSameStartAs(other: Interval) = compareStarts(other) == 0 + /** + * Whether this and another interval end at the same time, accounting for inclusivity. + * + * Assumes neither interval is empty. + */ + infix fun hasSameEndAs(other: Interval) = compareEnds(other) == 0 + + /** + * Whether this interval precedes another and touches it without overlap. + * + * Assumes neither interval is empty. + */ + infix fun meets(other: Interval) = (this.end == other.start) && (this.endInclusivity != other.startInclusivity) + /** + * Whether this interval comes after another and touches it without overlap. + * + * Assumes neither interval is empty. + */ + infix fun metBy(other: Interval) = other meets this + + /** Whether this interval [meets] or is [metBy] another. */ + infix fun adjacentTo(x: Interval) = metBy(x) || meets(x) + + /** Whether a given time is contained in this interval. */ + operator fun contains(d: Duration) = !intersection(at(d)).isEmpty() + /** Whether this interval contains the entirety of another. */ + operator fun contains(x: Interval) = intersection(x) == x + + /** + * Calculates the intersection between this interval and another. + * + * If either interval is empty, or there is no overlap, the result will be + * an empty interval. The exact endpoints of the empty interval are not meaningful, + * only the fact that it is empty. + */ + infix fun intersection(other: Interval): Interval { + if (this.isEmpty() || other.isEmpty()) return EMPTY + + val start: Duration + val startInclusivity: Inclusivity + if (compareStarts(other) > 0) { + start = this.start + startInclusivity = this.startInclusivity + } else { + start = other.start + startInclusivity = other.startInclusivity + } + val end: Duration + val endInclusivity: Inclusivity + if (compareEnds(other) < 0) { + end = this.end + endInclusivity = this.endInclusivity + } else { + end = other.end + endInclusivity = other.endInclusivity + } + return between(start, end, startInclusivity, endInclusivity) + } + + /** + * Calculates the union between this interval and another, as a list of intervals. + * + * The union of two intervals is not necessarily an interval, if they do not overlap. In this case + * the two intervals are returned in the list separately. + * If they do overlap, the list will contain a single element which is the union interval. + * + * If either interval is empty, it will not be included in the result. + */ + infix fun union(other: Interval): List { + if (intersection(other).isEmpty() && !adjacentTo(other)) return listOf(this, other).filterNot { it.isEmpty() } + + val start: Duration + val startInclusivity: Inclusivity + val end: Duration + val endInclusivity: Inclusivity + + if (compareStarts(other) < 0) { + start = this.start + startInclusivity = this.startInclusivity + } else { + start = other.start + startInclusivity = other.startInclusivity + } + if (compareEnds(other) > 0) { + end = this.end + endInclusivity = this.endInclusivity + } else { + end = other.end + endInclusivity = other.endInclusivity + } + return listOf(between(start, end, startInclusivity, endInclusivity)) + } + + /** The smallest interval that contains both this and another interval. */ + infix fun hull(other: Interval): Interval { + val union = union(other) + return if (union.isEmpty()) EMPTY + else if (union.size == 1) union[0] + else { + val sorted = union.sortedWith(Interval::compareStarts) + between(sorted[0].start, sorted[1].end, sorted[0].startInclusivity, sorted[1].endInclusivity) + } + } + + /** + * Removes all points in the argument interval from this interval. This is essentially the opposite of union. + * + * If the two intervals intersect, the result will be this interval with the intersection removed. + * If the removal splits this interval into two pieces, they are returned as separate elements of a list. + * + * @return a list of intervals containing all points in this interval which are not contained in the argument. + */ + operator fun minus(other: Interval): List { + if (isEmpty()) return listOf() + if (intersection(other).isEmpty()) return listOf(this) + + val left = between(start, other.start, startInclusivity, other.startInclusivity.opposite()) + val right = between(other.end, end, other.endInclusivity.opposite(), endInclusivity) + return listOf(left, right).filterNot { it.isEmpty() } + } + + /***/ + override fun toString(): String { + return if (isEmpty()) { + "(empty)" + } else { + "${if (includesStart()) "[" else "("}$start, $end${if (includesEnd()) "]" else ")"}" + } + } + + /** Helper functions for constructing Intervals. */ + companion object { + /** + * Constructs an interval between two durations. + * + * @param start The starting time of the interval. + * @param end The ending time of the interval. + * @return A non-empty interval if start < end, or an empty interval otherwise. + */ + @JvmStatic fun between( + start: Duration, + end: Duration, + startInclusivity: Inclusivity = Inclusivity.Inclusive, + endInclusivity: Inclusivity = startInclusivity + ) = Interval(start, end, startInclusivity, endInclusivity) + + /** + * Constructs an interval between two durations that includes its start and excludes its end. + * + * @param start The starting time of the interval. + * @param end The ending time of the interval. + */ + @JvmStatic fun betweenClosedOpen(start: Duration, end: Duration) = between(start, end, Inclusivity.Inclusive, Inclusivity.Exclusive) + + /** Constructs an interval containing a single time point, represented as a Duration object. */ + @JvmStatic fun at(point: Duration) = point .. point + + /** Shorthand for an empty interval. */ + @JvmStatic val EMPTY: Interval = Duration.ZERO .. (Duration.ZERO - Duration.EPSILON) + + /** The widest representable interval (from long min to long max microseconds). */ + @JvmStatic val MIN_MAX = Duration.MIN_VALUE .. Duration.MAX_VALUE + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt new file mode 100644 index 0000000000..a1445a2983 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt @@ -0,0 +1,58 @@ +package gov.nasa.jpl.aerie.timeline + +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike + +/** + * Interface of the raw timeline object. + * + * Should be implemented by composing a raw timeline in your container + * and delegating to it with the `by` keyword. See any timeline for examples. + */ +interface Timeline, THIS: Timeline> { + /** + * [(DOC)][collect] Evaluates the stack of operations and produces a list of timeline payload objects. + * + * The resulting list may have some property invariants depending on what type of timeline was + * evaluated. For example, calling `collect` on a profile type will result in an ordered, non-overlapping, + * coalesced list of segments. The only invariant that all timeline types share is that the list objects + * will be within the provided bounds. + */ + fun collect(opts: CollectOptions): List + + /** + * [(DOC)][collect] A simplified version of [collect]. + * + * Uses defaults for all other [CollectOptions] fields. + * + * @param bounds bounds of evaluation (defaults to [Interval.MIN_MAX] if not provided). + */ + fun collect(bounds: Interval = Interval.MIN_MAX) = collect(CollectOptions(bounds)) + + /** + * [(DOC)][unsafeCast] **UNSAFE!** Casts this timeline type to another type without changing its contents. + * + * The payload type [V] must be equal between the two types. + * + * This operation allows you to break the invariants of more specialized timeline types. For example, casting + * [`Intervals>`][gov.nasa.jpl.aerie.timeline.collections.Intervals] to [Booleans][gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans] + * without sorting and coalescing the segments could result in an invalid profile. In cases like that, it is better to + * use [flattenIntoProfile][gov.nasa.jpl.aerie.timeline.ops.ParallelOps.flattenIntoProfile] or [reduceIntoProfile][gov.nasa.jpl.aerie.timeline.ops.ParallelOps.reduceIntoProfile]. + */ + fun > unsafeCast(ctor: (Timeline) -> RESULT): RESULT + + /** + * This timeline's constructor. + * + * Used for operations that produce another timeline of the same type, + * and therefore don't need the user to provide a constructor manually. + * + * It is highly unlikely that users will ever need to access this property, + * because the [gov.nasa.jpl.aerie.timeline.ops.GeneralOps.unsafeOperate] function accesses this for them. + * + * @suppress + */ + val ctor: (Timeline) -> THIS + + /** @suppress */ + fun specialize() = ctor(this) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt new file mode 100644 index 0000000000..02f3c7647e --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt @@ -0,0 +1,16 @@ +package gov.nasa.jpl.aerie.timeline.payloads + +import gov.nasa.jpl.aerie.timeline.Interval + +/** + * An interface for objects that have a interval-like presence on a timeline. + * + * For example, profile segments, activity instances, and plain intervals are all interval-like. + */ +interface IntervalLike { + /** The interval this occupies on the timeline. */ + val interval: Interval + + /** Creates a new object that is identical except that it exists on a different interval. */ + fun withNewInterval(i: Interval): I +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt new file mode 100644 index 0000000000..edac277d6c --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformerTest.kt @@ -0,0 +1,18 @@ +package gov.nasa.jpl.aerie.timeline + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class BoundsTransformerTest { + @Test + fun shift() { + val transformer = BoundsTransformer.shift(seconds(1)) + + assertEquals( + between(seconds(-1), seconds(1)), + transformer(between(seconds(0), seconds(2))) + ) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt new file mode 100644 index 0000000000..3eb8ecb4df --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/IntervalTest.kt @@ -0,0 +1,195 @@ +package gov.nasa.jpl.aerie.timeline + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.EMPTY +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Companion.betweenClosedOpen +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class IntervalTest { + @Nested + inner class InclusivityTest { + @Test + fun opposite() { + assertEquals(Inclusive, Exclusive.opposite()) + } + + @Test + fun moreRestrictiveThan() { + assertTrue(Exclusive.moreRestrictiveThan(Inclusive)) + assertFalse(Exclusive.moreRestrictiveThan(Exclusive)) + } + } + + @Test + fun at() { + val t = seconds(45) + + val i = at(t) + assertEquals(t, i.start) + assertEquals(t, i.end) + assertEquals(Inclusive, i.startInclusivity) + assertEquals(Inclusive, i.endInclusivity) + } + + @Test + fun between() { + val t1 = seconds(1) + val t2 = seconds(5) + + var i = between(t1, t2) + assertEquals(t1, i.start) + assertEquals(t2, i.end) + assertEquals(Inclusive, i.startInclusivity) + assertEquals(Inclusive, i.endInclusivity) + + i = between(t1, t2, Exclusive) + assertEquals(Exclusive, i.startInclusivity) + assertEquals(Exclusive, i.endInclusivity) + + i = between(t1, t2, Inclusive, Exclusive) + assertEquals(Inclusive, i.startInclusivity) + assertEquals(Exclusive, i.endInclusivity) + + val i2 = betweenClosedOpen(t1, t2) + assertEquals(i, i2) + } + + @Test + fun isEmpty() { + assertFalse(at(seconds(5)).isEmpty()) + assertFalse((seconds(1) .. seconds(2)).isEmpty()) + assertTrue((seconds(2) .. seconds(1)).isEmpty()) + assertTrue(between(seconds(1), seconds(1), Inclusive, Exclusive).isEmpty()) + assertTrue(EMPTY.isEmpty()) + } + + @Test + fun isSingleton() { + assertTrue(at(seconds(1)).isPoint()) + assertFalse((seconds(1) .. seconds(2)).isPoint()) + assertFalse((seconds(2) .. seconds(1)).isPoint()) + } + + @Test + fun duration() { + assertEquals(seconds(1), (seconds(1) .. seconds(2)).duration()) + assertEquals(Duration.ZERO, EMPTY.duration()) + assertEquals(Duration.ZERO, (seconds(2) .. seconds(1)).duration()) + } + + @Test + fun intersect() { + assertEquals( + (seconds(1) .. seconds(2)), + seconds(1) .. seconds(2) intersection seconds(0) .. seconds(4) + ) + assertEquals( + (seconds(1) .. seconds(2)), + (seconds(0) .. seconds(2)).intersection(seconds(1) .. seconds(3)) + ) + assertEquals( + betweenClosedOpen(seconds(1), seconds(2)), + between(seconds(0), seconds(2), Exclusive).intersection(seconds(1) .. seconds(3)) + ) + assertTrue((seconds(0) .. seconds(1)).intersection(seconds(2) .. seconds(3)).isEmpty()) + assertTrue((seconds(0) .. seconds(1)).intersection(EMPTY).isEmpty()) + } + + @Test + fun union() { + assertIterableEquals( + listOf(seconds(0) .. seconds(2)), + seconds(0) .. seconds(2) union at(seconds(1)) + ) + assertIterableEquals( + listOf(seconds(0) .. seconds(2)), + (seconds(0) .. seconds(2)).union(EMPTY) + ) + assertIterableEquals( + listOf(seconds(0) .. seconds(2)), + (seconds(0) .. seconds(1)).union(seconds(1) .. seconds(2)) + ) + assertIterableEquals( + listOf(seconds(0) .. seconds(2)), + betweenClosedOpen(seconds(0), seconds(1)).union(seconds(1) .. seconds(2)) + ) + assertIterableEquals( + listOf((seconds(0) .. seconds(1)), at(seconds(4))), + (seconds(0) .. seconds(1)).union(at(seconds(4))) + ) + assertIterableEquals( + listOf(seconds(0) .. seconds(1)), + (seconds(0) .. seconds(1)).union(EMPTY) + ) + } + + @Test + fun subtract() { + assertIterableEquals( + listOf(betweenClosedOpen(seconds(0), seconds(1)), between(seconds(1), seconds(2), Exclusive, Inclusive)), + (seconds(0) .. seconds(2)).minus(at(seconds(1))) + ) + assertIterableEquals( + listOf(seconds(0) .. seconds(1)), + (seconds(0) .. seconds(1)).minus(seconds(3) .. seconds(4)) + ) + assertIterableEquals( + listOf(betweenClosedOpen(seconds(0), seconds(1))), + (seconds(0) .. seconds(2)).minus(seconds(1) .. seconds(4)) + ) + assertIterableEquals( + listOf(seconds(3) .. seconds(4)), + (seconds(3) .. seconds(4)).minus(seconds(1) .. seconds(2)) + ) + assertIterableEquals( + listOf(between(seconds(3), seconds(4), Exclusive, Inclusive)), + (seconds(2) .. seconds(4)).minus(seconds(1) .. seconds(3)) + ) + } + + @Test + fun compareStarts() { + assertEquals(-1, (seconds(0) .. seconds(1)).compareStarts(at(seconds(1)))) + assertEquals(1, between(seconds(0), seconds(1), Exclusive).compareStarts(at(seconds(0)))) + assertEquals(0, (seconds(0) .. seconds(1)).compareStarts(at(seconds(0)))) + } + + @Test + fun compareEnds() { + assertEquals(1, (seconds(0) .. seconds(1)).compareEnds(at(seconds(0)))) + assertEquals(-1, between(seconds(0), seconds(1), Exclusive).compareEnds(at(seconds(1)))) + assertEquals(0, (seconds(0) .. seconds(1)).compareEnds(at(seconds(1)))) + assertEquals(-1, (seconds(0) ..< seconds(1)).compareEnds(at(seconds(1)))) + } + + @Test + fun compareEndToStart() { + assertEquals(-1, at(seconds(0)).compareEndToStart(at(seconds(1)))) + assertEquals(0, at(seconds(0)).compareEndToStart(between(seconds(0), seconds(1), Exclusive))) + assertEquals(1, (seconds(0) .. seconds(2)).compareEndToStart(seconds(1) .. seconds(4))) + } + + @Test + fun contains() { + assertTrue((seconds(0) .. seconds(2)).contains(seconds(1))) + assertFalse((seconds(0) .. seconds(2)).contains(seconds(3))) + assertTrue((seconds(0) .. seconds(2)).contains(at(seconds(1)))) + assertFalse((seconds(0) .. seconds(2)).contains(at(seconds(3)))) + assertTrue((seconds(0) .. seconds(2)).contains(seconds(1) .. seconds(2))) + assertFalse((seconds(0) .. seconds(2)).contains(seconds(1) .. seconds(3))) + } + + @Test + fun shiftBy() { + assertEquals((seconds(1) .. seconds(2)), (seconds(0) .. seconds(1)).shiftBy(seconds(1))) + assertEquals(at(seconds(1)), at(seconds(2)).shiftBy(seconds(-1))) + assertEquals((seconds(1) .. seconds(3)), (seconds(0) .. seconds(4)).shiftBy(seconds(1), seconds(-1))) + assertEquals(at(seconds(1)), (seconds(0) .. seconds(2)).shiftBy(seconds(1), seconds(-1))) + assertTrue((seconds(0) .. seconds(1)).shiftBy(seconds(2), seconds(0)).isEmpty()) + } +} From 5f3cf214677c32c61c3b455ecacfd019d1fd7738 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:33:52 -0800 Subject: [PATCH 099/159] Low level list utility functions --- .../nasa/jpl/aerie/timeline/util/Coalesce.kt | 63 ++++++++++++++++ .../nasa/jpl/aerie/timeline/util/ListUtils.kt | 51 +++++++++++++ .../jpl/aerie/timeline/util/CoalesceTest.kt | 38 ++++++++++ .../aerie/timeline/util/ListCollectorTest.kt | 75 +++++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt new file mode 100644 index 0000000000..e370346563 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Coalesce.kt @@ -0,0 +1,63 @@ +package gov.nasa.jpl.aerie.timeline.util + +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike + +/** Coalesces a list if the provided [shouldCoalesce] function is not `null`. */ +fun > maybeCoalesce(list: List, shouldCoalesce: (I.(I) -> Boolean)?) = + shouldCoalesce?.let { coalesceList(list, it) } ?: list + +/** + * Flattens overlapping segments into non-overlapping segments with unequal consecutive values. + * + * *Input condition*: segments must be sorted such that between each pair of consecutive elements, one of the following is true: + * - if the values are unequal, the start and end times (including inclusivity) must be strictly increasing + * - if the values are equal, the start time must be non-decreasing. + * + * This input condition is not checked, and violating it is undefined behavior. + * + * Empty intervals are removed, and their values are not considered for the purposes of the sorted input condition. + */ +fun > coalesceList(list: List, shouldCoalesce: I.(I) -> Boolean): List { + val mutableList = list.toMutableList() + if (mutableList.isEmpty()) return mutableList + var shortIndex = 0 + var startIndex = 0 + while(mutableList[startIndex].interval.isEmpty()) startIndex++ + var buffer = mutableList[shortIndex] + for (segment in mutableList.subList(startIndex + 1, mutableList.size)) { + if (segment.interval.isEmpty()) continue + val comparison = buffer.interval.compareEndToStart(segment.interval) + if (comparison == -1) { + if (!buffer.interval.isEmpty()) mutableList[shortIndex++] = buffer + buffer = segment + } else if (comparison == 0) { + if (buffer.shouldCoalesce(segment)) { + if (buffer.interval.compareEnds(segment.interval) < 0) { + buffer = buffer.withNewInterval( + Interval.between(buffer.interval.start, segment.interval.end, buffer.interval.startInclusivity, segment.interval.endInclusivity) + ) + } + } else { + if (!buffer.interval.isEmpty()) mutableList[shortIndex++] = buffer + buffer = segment + } + } else { + if (buffer.shouldCoalesce(segment)) { + if (buffer.interval.compareEnds(segment.interval) < 0) { + buffer = buffer.withNewInterval( + Interval.between(buffer.interval.start, segment.interval.end, buffer.interval.startInclusivity, segment.interval.endInclusivity) + ) + } + } else { + buffer = buffer.withNewInterval( + Interval.between(buffer.interval.start, segment.interval.start, buffer.interval.startInclusivity, segment.interval.startInclusivity.opposite()) + ) + if (!buffer.interval.isEmpty()) mutableList[shortIndex++] = buffer + buffer = segment + } + } + } + if (!buffer.interval.isEmpty()) mutableList[shortIndex++] = buffer + return mutableList.subList(0, shortIndex) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt new file mode 100644 index 0000000000..f345397d0b --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt @@ -0,0 +1,51 @@ +package gov.nasa.jpl.aerie.timeline.util + +import gov.nasa.jpl.aerie.timeline.CollectOptions +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike + +/** + * Sanitizes a list of [IntervalLike] objects for use in a timeline. + * + * 1. sorts the list by start time (or by end time if two start times are equal) + * 2. coalesces the list if applicable + * 3. wraps the list in a collect closure that truncates the list to a given set of [CollectOptions] + * + * @param list the list to sanitize + * @param shouldCoalesce a maybe-null two-argument function of [V]s that decides if they should be coalesced when they + * overlap. If `null`, no coalesce operation is performed. + * + * @return a collect closure that produces a sorted, possibly coalesced, and bounded list + */ +fun > preprocessList(list: List, shouldCoalesce: (V.(V) -> Boolean)? = null): (CollectOptions) -> List { + val sorted = list.sorted() + val coalesced = maybeCoalesce(sorted, shouldCoalesce) + return listCollector(coalesced) +} + +/** Returns a function that lazily truncates a given list of timeline objects to the bounds. */ +fun > listCollector(list: List) = { opts: CollectOptions -> truncateList(list, opts) } + +/** Eagerly truncates a list of timeline objects to known bounds. */ +fun > truncateList(list: List, opts: CollectOptions) = + if (opts.bounds == Interval.MIN_MAX) list + else if (!opts.truncateMarginal) { + list.filter { + val intersection = it.interval.intersection(opts.bounds) + !intersection.isEmpty() + } + } else { + list.mapNotNull { + val intersection = it.interval.intersection(opts.bounds) + if (intersection.isEmpty()) null + else if (intersection == it.interval) it + else it.withNewInterval(intersection) + } + } + +/** Returns a new list of intervals, sorted by start time, or end time in case of a tie. */ +fun > List.sorted() = sortedWith { a, b -> + val startComparison = a.interval.compareStarts(b.interval) + if (startComparison != 0) startComparison + else a.interval.compareEnds(b.interval) +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt new file mode 100644 index 0000000000..725b015903 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/CoalesceTest.kt @@ -0,0 +1,38 @@ +package gov.nasa.jpl.aerie.timeline.util + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.* +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class CoalesceTest { + + @Test + fun isNoopOnAlreadyCoalescedList() { + fun supplier() = listOf( + Segment(between(seconds(0), seconds(1), Inclusive, Exclusive), false), + Segment(between(seconds(1), seconds(2), Inclusive, Inclusive), true), + Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), false), + ) + + val expected = supplier() + val result = coalesceList(supplier(), Segment::valueEquals) + + assertIterableEquals(expected, result) + } + + @Test + fun removeEmptySegment() { + val result = coalesceList(listOf( + Segment(at(seconds(1)), false), + Segment(seconds(1) .. seconds(2), true) + ), Segment::valueEquals) + + val expected = listOf(Segment(seconds(1) .. seconds(2), true)) + + assertIterableEquals(expected, result) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt new file mode 100644 index 0000000000..a4702debaa --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/ListCollectorTest.kt @@ -0,0 +1,75 @@ +package gov.nasa.jpl.aerie.timeline.util + +import gov.nasa.jpl.aerie.timeline.CollectOptions +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.collections.Intervals + +val bounds = (seconds(0) .. seconds(10)) + +class ListCollectorTest { + @Test + fun isNoopOnArrayInBounds() { + val input = listOf( + seconds(2) .. seconds(3), + seconds(4) .. seconds(5), + ) + val result = Intervals(input).collect(bounds) + + assertIterableEquals(input, result) + } + + @Test + fun removeSegmentOutOfBounds() { + val input = listOf( + seconds(2) .. seconds(3), + seconds(14) .. seconds(15), + ) + val result = Intervals(input).collect(bounds) + + val expected = listOf( + seconds(2) .. seconds(3), + ) + + assertIterableEquals(expected, result) + } + + @Test + fun splitSegmentOnBounds() { + val input = listOf( + seconds(2) .. seconds(3), + seconds(9) .. seconds(11), + ) + val result = Intervals(input).collect(bounds) + + val expected = listOf( + seconds(2) .. seconds(3), + (seconds(9) .. seconds(10)) + ) + + assertIterableEquals(expected, result) + } + + @Test + fun dontTruncateMarginal() { + val input = listOf( + between(seconds(-3), seconds(-2)), + between(seconds(-1), seconds(1)), + seconds(4) .. seconds(5), + seconds(9) .. seconds(11), + seconds(13) .. seconds(14) + ) + + val result = Intervals(input).collect(CollectOptions(bounds, false)) + + val expected = listOf( + between(seconds(-1), seconds(1)), + seconds(4) .. seconds(5), + seconds(9) .. seconds(11), + ) + + assertIterableEquals(expected, result) + } +} From 3fd16ee1044019958b35c13a6f347b6ab16786b5 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:33:21 -0800 Subject: [PATCH 100/159] General operations that apply to all timelines --- .../nasa/jpl/aerie/timeline/ops/GeneralOps.kt | 227 ++++++++++++++++++ .../jpl/aerie/timeline/ops/GeneralOpsTest.kt | 215 +++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt new file mode 100644 index 0000000000..c7c3f7fb56 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt @@ -0,0 +1,227 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.collections.Intervals +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.util.coalesceList +import gov.nasa.jpl.aerie.timeline.util.sorted +import gov.nasa.jpl.aerie.timeline.util.truncateList + +/** + * General operations mixin for all timeline types. + * + * @property V collection payload type + * @property THIS collection type + */ +interface GeneralOps, THIS: GeneralOps>: Timeline { + /** [(DOC)][unsafeOperate] **UNSAFE!** A simpler version of [unsafeOperate] for operations that don't change the timeline type. */ + fun unsafeOperate(f: Timeline.(opts: CollectOptions) -> List) = unsafeOperate(ctor, f) + + /** + * [(DOC)][unsafeOperate] **UNSAFE!** The basic, most general operation method. All operations eventually delegate here. + * + * This doesn't actually execute the operation, it just creates a [BaseTimeline] object that holds + * the operation for lazy evaluation later, when [collect] is called. It then wraps the base timeline in + * the provided constructor (called specialization) and coalesces the timeline if applicable. + * + * ## Safety + * + * This function is unsafe because all timeline types have mathematical invariants, such as ordered segments + * for profiles, and this method allows those invariants to be broken if the user is not careful. + * In particular, all timelines assume the result of [collect] will be contained in the `bounds` argument. + * These invariants are often maintained automatically, but you should never assume. Violating them is UB. + * Use at your own risk. + * + * @param ctor the constructor of the new timeline type + * @param f a function which, given this and a [CollectOptions] object, produces a list of payload objects + * contained in the bounds. In Java, this is a two-argument function which takes a timeline object + * and [CollectOptions]. In Kotlin, this is a one-argument function of the options with a timeline receiver. + * (see [the kotlin docs](https://kotlinlang.org/docs/lambdas.html#instantiating-a-function-type)) + */ + fun , RESULT: GeneralOps> unsafeOperate(ctor: (Timeline) -> RESULT, f: Timeline.(opts: CollectOptions) -> List): RESULT { + val result = BaseTimeline(ctor) { f(it) }.specialize() + return result.shouldCoalesce()?.let { + BaseTimeline(ctor) { opts -> + coalesceList(result.collect(opts), it) + }.specialize() + } ?: result + } + + /** + * [(DOC)][convert] Safely converts to another timeline type that accepts the same payload type. + * + * Coalesces if necessary. + */ + fun > convert(ctor: (Timeline) -> RESULT): RESULT { + val result = unsafeCast(ctor) + return result.shouldCoalesce()?.let { + BaseTimeline(ctor) { opts -> + val sorted = result.collect(opts).sorted() + coalesceList(sorted, it) + }.specialize() + } ?: result + } + + /** + * [(DOC)][inspect] Inserts a no-op function into the operation stack to allow side effects, + * such as printing. + * + * @param f a function that receives a list of timeline objects and does nothing to them + */ + fun inspect(f: (List) -> Unit) = BaseTimeline(ctor) { + val list = collect(it) + f(list) + list + }.specialize() + + /** + * Produces a function that decides if two timeline objects should be coalesced together when they have overlap. + * + * All timeline collections are required to add either [gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceNoOp] or + * [gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp] to satisfy this requirement. + * + * The user should never need to call this directly, as it is already called by [unsafeOperate]. + * + * @suppress + */ + fun shouldCoalesce(): (V.(V) -> Boolean)? + + /** + * [(DOC)][unset] Unsets everything in a given interval. Timeline objects whose intervals fully contain the rejected interval may be + * split into two objects. + * + * Unfortunately this can't be used for any performance gain; instead of evaluating the timeline twice, once on either + * side of the rejected interval (and combining the results), it has to evaluate the timeline on the entire original bounds + * and cut out anything in the rejected interval. This is because using unset in combination with any operation that + * depends on interval duration may yield incorrect results for objects that border the rejected interval. + * + * @param reject the interval on which to delete or truncate any objects + */ + fun unset(reject: Interval) = unsafeOperate { opts -> + if (opts.bounds.intersection(reject).isEmpty()) return@unsafeOperate collect(opts) + collect(opts).flatMap { + it.interval.minus(reject).map { i -> it.withNewInterval(i) } + } + } + + /** [(DOC)][select] Restricts the timeline to only be evaluated in the given interval. */ + fun select(interval: Interval) = unsafeOperate { opts -> + collect(CollectOptions(opts.bounds.intersection(interval), opts.truncateMarginal)) + } + + /** + * [(DOC)][isolate] Similar to [filter], but returns an [Intervals] timeline + * + * @param f a predicate which decides if a given payload object's interval should be included in the result + */ + fun isolate(f: (V) -> Boolean) = filter(false, f).unsafeCast(::Intervals) + + /** [(DOC)][unsafeMap] **UNSAFE!** A simpler version of [unsafeMap] for operations that don't change the timeline type. */ + fun unsafeMap(boundsTransformer: BoundsTransformer, truncate: Boolean, f: (V) -> V) = unsafeMap(ctor, boundsTransformer, truncate, f) + + /** + * [(DOC)][unsafeMap] **UNSAFE!** Maps each timeline object to another object, of potentially a different type, at potentially a different + * time. + * + * This operation may require the bounds of evaluation to be changed for the timeline it is called on. + * This can happen if the intervals of any segment are being shifted. This is what the [boundsTransformer] + * argument is for. This will ensure that the timeline map is called on will evaluate on the proper bounds. + * + * In rare cases, operations might intentionally shift or create intervals outside the requested bounds, + * requiring them to be truncated. Pass `true` for [truncate] if so (this will add an extra step to truncate the result). + * + * @see [unsafeOperate] for an explanation of why this method is unsafe. + * + * @param R the result payload type + * @param RESULT the result timeline type + * + * @param ctor the constructor of the result timeline + * @param boundsTransformer how to transform the bounds for the timeline map is called on + * @param truncate whether to truncate the result before returning + * @param f a mapper function that converts each timeline object to another object + */ + fun , RESULT: GeneralOps> unsafeMap(ctor: (Timeline) -> RESULT, boundsTransformer: BoundsTransformer, truncate: Boolean, f: (V) -> R) = + unsafeOperate(ctor) { opts -> + val mapped = collect(opts.transformBounds(boundsTransformer)).map { f(it) } + if (truncate) truncateList(mapped, opts) + else mapped + } + + /** + * [(DOC)][unsafeFlatMap] **UNSAFE!** Maps each object to a nested timeline and flattens all the timelines into one. + * + * Very similar to [unsafeMap], except that the result of the mapper function is a [Segment] containing a timeline. + * After each object is mapped, each nested timeline will be collected on its segment's interval, and flattened + * together into a single timeline. + * + * @see [unsafeMap] for explanation of [boundsTransformer] and [truncate] + * @see [unsafeOperate] for an explanation of why this method is unsafe. + * + * @param R the result payload type + * @param NESTED the nested timeline type; typically the same as [RESULT] + * @param RESULT the result timeline type + * + * @param ctor the constructor of the result timeline + * @param boundsTransformer how to transform the bounds for the timeline map is called on + * @param truncate whether to truncate the result before returning + * @param f a mapper function that converts each timeline object to a segment of a nested timeline + */ + fun , NESTED: GeneralOps, RESULT: GeneralOps> unsafeFlatMap(ctor: (Timeline) -> RESULT, boundsTransformer: BoundsTransformer, truncate: Boolean, f: (V) -> Segment) = + unsafeOperate(ctor) { opts -> + val mapped = collect(opts.transformBounds(boundsTransformer)).flatMap { + val nested = f(it) + nested.value.collect(nested.interval) + } + if (truncate) truncateList(mapped, opts) + else mapped + } + + /** + * [(DOC)][unsafeMapIntervals] **UNSAFE!** Maps the interval of each object, leaving the rest of the object unchanged. + * + * @see [unsafeMap] for explanation of [boundsTransformer] and [truncate] + * @see [unsafeOperate] for an explanation of why this method is unsafe. + * + * @param boundsTransformer how to transform the bounds for the timeline map is called on + * @param truncate whether to truncate the result before returning + * @param f a mapper function that converts each timeline object to an interval to be used as the new interval for that object + */ + fun unsafeMapIntervals(boundsTransformer: BoundsTransformer, truncate: Boolean, f: (V) -> Interval) = unsafeMap(boundsTransformer, truncate) { v -> v.withNewInterval(f(v)) } + + /** + * [(DOC)][filter] Removes or retains objects based on a predicate. + * + * @param f a function which returns `true` if the object is to be retained, or `false` if the object is to be removed. + * @param preserveMargin whether the predicate needs the full intervals for objects that extend beyond the bounds + */ + fun filter(preserveMargin: Boolean = false, f: (V) -> Boolean) = unsafeOperate { opts -> + val result = collect(CollectOptions(opts.bounds, !preserveMargin && opts.truncateMarginal)).filter(f) + if (preserveMargin && opts.truncateMarginal) truncateList(result, opts) + else result + } + + /** + * [(DOC)][filterByDuration] Removes objects whose duration is outside a given valid interval. + * + * Note that while intervals are often used to represent a range of time instants, they are + * actually defined by relative durations from a zero point which is unknown by the interval + * (typically the plan start time). This means that intervals can also be used to represent + * a range of durations too. + * + * Objects that extend beyond the evaluation bounds are still considered with their full extent, even + * if [CollectOptions.truncateMarginal] is `true`. In that case, the margin is truncated after it has + * passed the filter. + * + * @param validInterval objects with durations outside this interval will be removed + */ + fun filterByDuration(validInterval: Interval) = filter(true) { validInterval.contains(it.interval.duration()) } + + /** [(DOC)][filterShorterThan] Removes objects whose duration is shorter than a given duration. */ + fun filterShorterThan(dur: Duration) = filter(true) { it.interval.duration() >= dur } + /** [(DOC)][filterLongerThan] Removes objects whose duration is longer than a given duration. */ + fun filterLongerThan(dur: Duration) = filter(true) { it.interval.duration() <= dur } + + /** [(DOC)][shift] Uniformly shifts the entire timeline in time (positive shifts toward the future). */ + fun shift(dur: Duration) = unsafeMapIntervals(BoundsTransformer.shift(dur), false) { it.interval.shiftBy(dur) } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt new file mode 100644 index 0000000000..a06ca60b9f --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOpsTest.kt @@ -0,0 +1,215 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.Duration.Companion.milliseconds +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Companion.betweenClosedOpen +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Inclusive +import gov.nasa.jpl.aerie.timeline.collections.Intervals +import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class GeneralOpsTest { + + @Test + fun operate() { + val result = Constants(Segment(seconds(0) .. seconds(1), "hello")).unsafeOperate { + collect(it).map { s -> Segment(s.interval, s.value + " world") } + }.collect() + + val expected = listOf(Segment(seconds(0) .. seconds(1), "hello world")) + + assertIterableEquals(expected, result) + } + + @Test + fun operateAutoCoalesce() { + val result = Constants( + Segment(seconds(0) .. seconds(1), "hello world"), + Segment(seconds(1) .. seconds(2), "hello there") + ).unsafeOperate { + collect(it).map { s -> Segment(s.interval, s.value.substring(0..4)) } + }.collect() + + val expected = listOf(Segment(seconds(0) .. seconds(2), "hello")) + + assertIterableEquals(expected, result) + } + + @Test + fun operateType() { + // this is just a test to make sure the return type of unsafeOperate is correct. + // we would get a compile error if it failed. + @Suppress("UNUSED_VARIABLE") + val result: Booleans = Booleans(true).unsafeOperate { collect(it) } + } + + @Test + fun inspect() { + var count: Int? = null + val tl = Intervals( + at(seconds(1)), + at(seconds(2)) + ).inspect { + count = it.size + } + + assertNull(count) + + tl.collect() + + assertEquals(2, count) + } + + @Test + fun unset() { + val result = Intervals( + seconds(0) .. seconds(2), + seconds(2) .. seconds(3), + seconds(3) .. seconds(5), + seconds(10) .. seconds(11) + ).unset(seconds(1) .. seconds(4)).collect() + + assertIterableEquals( + listOf( + betweenClosedOpen(seconds(0), seconds(1)), + between(seconds(4), seconds(5), Exclusive, Inclusive), + (seconds(10) .. seconds(11)) + ), + result + ) + } + + @Test + fun filter() { + val result = Numbers( + Segment(at(seconds(1)), 4), + Segment(at(seconds(2)), 5) + ).filter { it.value.toInt() % 2 == 0 }.collect() + + assertIterableEquals( + listOf(Segment(at(seconds(1)), 4)), + result + ) + } + + @Test + fun filterPreserveMargin() { + val intervals = Intervals( + between(seconds(-1), seconds(1)), + seconds(1) .. seconds(4), + seconds(4) .. seconds(8), + ) + + // without preserve margin + assertIterableEquals( + listOf(seconds(1) .. seconds(4)), + intervals.filter(false) { it.duration() >= seconds(2) } + .collect(seconds(0) .. seconds(5)) + ) + + // with preserve margin and truncate margin + // notice that the marginal intervals are retained and then later truncated to within the bounds + assertIterableEquals( + listOf( + seconds(0) .. seconds(1), + seconds(1) .. seconds(4), + seconds(4) .. seconds(5), + ), + intervals.filter(true) { it.duration() >= seconds(2) } + .collect(seconds(0) .. seconds(5)) + ) + + // with preserve margin, without truncate margin + // notice that the marginal intervals are retained and NOT truncated later + assertIterableEquals( + intervals.collect(), + intervals.filter(true) { it.duration() >= seconds(2) } + .collect(CollectOptions(seconds(0) .. seconds(5), false)) + ) + } + + @Test + fun map() { + val result = Intervals( + at(seconds(1)), + seconds(2) .. seconds(3) + ).unsafeMap(::Booleans, BoundsTransformer.IDENTITY, false) { Segment(it.interval, it.interval.isPoint()) } + .collect() + + assertIterableEquals( + listOf( + Segment(at(seconds(1)), true), + Segment(seconds(2) .. seconds(3), false), + ), + result + ) + } + + @Test + fun shiftBoundsTransform() { + val intervals = Intervals( + between(seconds(-1), seconds(0)), + seconds(2) .. seconds(4), + ).shift(Duration.SECOND) + + val expected = listOf( + seconds(0) .. seconds(1), + seconds(3) .. seconds(5) + ) + + assertIterableEquals( + expected, + intervals.collect() + ) + + assertIterableEquals( + expected, + intervals.collect(seconds(0) .. seconds(5)) + ) + } + + @Test + fun shiftOutOfBounds() { + val intervals = Intervals(at(seconds(3))).shift(seconds(3)) + + assertIterableEquals( + listOf(), + intervals.collect(seconds(0) .. seconds(5)) + ) + } + + @Test + fun flatMapTest() { + val result = Intervals( + seconds(2) .. seconds(8), + seconds(0) .. seconds(3) + ) + // converts each interval to a windows object. + // false for the first half of the interval, true for the second half + .unsafeFlatMap(::Booleans, BoundsTransformer.IDENTITY, false) { + val midpoint = it.interval.start.plus(it.interval.end) / 2 + Segment( + it.interval.interval, + Booleans(false).set(Booleans(Segment(between(midpoint, Duration.MAX_VALUE), true))) + ) + } + .collect() + + val expected = listOf( + Segment(betweenClosedOpen(seconds(0), milliseconds(1500)), false), + Segment(betweenClosedOpen(milliseconds(1500), seconds(2)), true), + Segment(betweenClosedOpen(seconds(2), seconds(5)), false), + Segment(seconds(5) .. seconds(8), true) + ) + + assertIterableEquals(expected, result) + } +} From 4e64ad600efeb63c7962ffe3e694ddd81410cd49 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:37:30 -0800 Subject: [PATCH 101/159] Implement Intervals (most general timeline type) --- .../aerie/timeline/collections/Intervals.kt | 20 ++ .../aerie/timeline/ops/NonZeroDurationOps.kt | 60 +++++ .../jpl/aerie/timeline/ops/ParallelOps.kt | 238 ++++++++++++++++++ .../jpl/aerie/timeline/payloads/Connection.kt | 14 ++ .../timeline/ops/NonZeroDurationOpsTest.kt | 60 +++++ .../jpl/aerie/timeline/ops/ParallelOpsTest.kt | 60 +++++ 6 files changed, 452 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt new file mode 100644 index 0000000000..5efc811a61 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt @@ -0,0 +1,20 @@ +package gov.nasa.jpl.aerie.timeline.collections + +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.ops.ParallelOps +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.ops.NonZeroDurationOps +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceNoOp +import gov.nasa.jpl.aerie.timeline.util.preprocessList + +/** A timeline of any [IntervalLike] object with no special operations. */ +data class Intervals>(private val timeline: Timeline>): + Timeline> by timeline, + ParallelOps>, + NonZeroDurationOps>, + CoalesceNoOp> +{ + constructor(vararg intervals: T): this(intervals.asList()) + constructor(intervals: List): this(BaseTimeline(::Intervals, preprocessList(intervals))) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt new file mode 100644 index 0000000000..8a0e029c17 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOps.kt @@ -0,0 +1,60 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.CollectOptions +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +import gov.nasa.jpl.aerie.timeline.util.truncateList + +/** + * Operations mixin for timelines of activity directives. + * + * Used for both generic directives, and specific directive types from the mission model. + */ +interface NonZeroDurationOps, THIS: NonZeroDurationOps>: GeneralOps { + /** + * [(DOC)][split] Splits payload objects into a variable number of equally sized pieces. + * + * The caller provides a function which, for each object, decides how many pieces it should be split into. + * If it returns `1`, the object will be unchanged. + * + * @param f a function that decides how many pieces each object should be split into + * + * @throws SplitException if [f] returns a number less than `1`, + * or a number greater than the number of microseconds contained in the object's interval + */ + fun split(f: (T) -> Int) = unsafeOperate { opts -> + val result = collect(CollectOptions(opts.bounds, false)).flatMap { + val numPieces = f(it) + val interval = it.interval + if (numPieces == 1) listOf(it) + else if (numPieces < 1) throw SplitException("Cannot split an interval into less than 1 piece (time ${interval.start}") + else if (interval.isPoint()) throw SplitException("Cannot split an interval with no duration (time ${interval.start}") + else { + val integerWidth = interval.duration() / Duration.EPSILON + val width = interval.duration() / numPieces.toLong() + + if (integerWidth < numPieces) + throw SplitException("Cannot split an interval only $integerWidth microseconds long into $numPieces pieces (time ${interval.start}") + + var currentTime = interval.start.plus(width) + val result = mutableListOf() + + result.add(it.withNewInterval(Interval.between(interval.start, currentTime, interval.startInclusivity, Exclusive))) + for (i in 1 ..< numPieces - 1) { + val nextTime = currentTime.plus(width) + result.add(it.withNewInterval(Interval.between(currentTime, nextTime, Exclusive))) + currentTime = nextTime + } + result.add(it.withNewInterval(Interval.between(currentTime, interval.end, Exclusive, interval.endInclusivity))) + + result + } + } + truncateList(result, opts) + } + + /** @see [split] */ + class SplitException(message: String): Exception(message) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt new file mode 100644 index 0000000000..debcb510ee --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt @@ -0,0 +1,238 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.collections.Intervals +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.payloads.Connection +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.util.map2SegmentLists +import gov.nasa.jpl.aerie.timeline.util.truncateList + +/** + * Operations mixin for timelines of potentially overlapping objects. + * + * Opposite of [SerialOps]. + */ +interface ParallelOps, THIS: ParallelOps>: GeneralOps { + /** [(DOC)][merge] Combines two timelines together by overlaying them. Does not perform any transformation. */ + infix fun > merge(other: GeneralOps) = unsafeOperate { opts -> + collect(opts) + other.collect(opts) + } + + /** [(DOC)][merge] Combines this timeline with a single payload object by overlaying them. */ + infix fun merge(i: T) = merge(Intervals(i)) + + /** + * [(DOC)][mapIntoProfile] Maps the objects into a parallel profile. + * + * Not currently usable because no parallel profile types are implemented. + * @suppress + */ + fun , RESULT>> mapIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, f: (T) -> V) = + unsafeMap(ctor, BoundsTransformer.IDENTITY, false) { Segment(it.interval, f(it)) } + + /** + * [(DOC)][flattenIntoProfile] Converts the payload objects into segments and flattens them into a serial profile. + * + * After the payload objects are converted into segments, any overlapping segments are + * resolved by overwriting the earlier segment with the later segment. + * + * @param ctor the constructor of the result timeline + * @param f a function which converts a payload object into the value of the resulting segment + */ + fun > flattenIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, f: (T) -> R) = + unsafeOperate(ctor) { bounds -> + val result = collect(bounds).mapTo(mutableListOf()) { Segment(it.interval, f(it)) } + result.sortWith { l, r -> l.interval.compareStarts(r.interval) } + result + } + + /** + * [(DOC)][reduceIntoProfile] Converts the payload objects into segments and combines them into a serial profile. + * + * After the payload objects are converted into segments, any overlapping segments are resolved by combining them + * with a binary operation similar to a reduce operation (as in functional programming). It is recommended to + * create the binary operation using the [BinaryOperation.reduce] function. + * + * ## Example + * + * Say you have a timeline of activity instances, each with an integer argument called `count`, + * and you want to extract a profile of the `count` arguments - but in the event that the activities overlap, + * you want to add the counts together. Your code would look like this: + * + * ``` + * myInstances.reduceIntoProfile(Numbers::new, BinaryOperation.reduce( + * (activity) -> activity.inner.count, // convert + * (acc, activity) -> acc + activity.inner.count // combine + * )) + * ``` + * + * If the plan has three relevant activities overlapping (A, B, and C): + * - first the converter will be called on A, starting the accumulator (`acc`) + * - then the combiner will be called for `acc` and B, updating the accumulator + * - then the combiner will be called for `acc` and C, etc + * + * If the plan also has other relevant activities the overlap sometime else, + * they will be combined independently of A, B, and C. + * + * @param ctor the constructor of the result profile + * @param op a binary operation for converting and combining the input objects + */ + fun > reduceIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, op: BinaryOperation) = + unsafeOperate(ctor) { opts -> + val bounds = opts.bounds + var acc: List> = listOf() + var remaining = collect(opts) + while (remaining.isNotEmpty()) { + var previousTime = bounds.start + var previousInclusivity = bounds.startInclusivity.opposite() + val partitioned = remaining.partition { obj -> + val startComparison = obj.interval.start.compareTo(previousTime) + if (startComparison > 0 || (startComparison == 0 && previousInclusivity != obj.interval.startInclusivity)) { + previousTime = obj.interval.end + previousInclusivity = obj.interval.endInclusivity + true + } else { + false + } + } + val batch = partitioned.first.map { Segment(it.interval, it) } + remaining = partitioned.second + acc = map2SegmentLists(batch, acc, op) + } + if (!opts.truncateMarginal) truncateList(acc, opts) + else acc + } + + /** + * [(DOC)][shiftEndpoints] Shifts the start and end points of each object. + * + * The start and end can be shifted by different amounts, stretching or squishing the interval. + * If the interval is empty after the shift, it is removed. + * + * @param shiftStart duration to shift the starts + * @param shiftEnd duration to shift the ends; defaults to [shiftStart] if not provided + */ + fun shiftEndpoints(shiftStart: Duration, shiftEnd: Duration = shiftStart) = + unsafeMapIntervals( + { i -> + Interval.between( + Duration.min(i.start.minus(shiftStart), i.start.minus(shiftEnd)), + Duration.max(i.end.minus(shiftStart), i.end.minus(shiftEnd)), + i.startInclusivity, + i.endInclusivity + ) + }, + true + ) { t -> t.interval.shiftBy(shiftStart, shiftEnd) } + + /** [(DOC)][active] Returns a [Booleans] profile that is true when this timeline has an active object. */ + fun active() = flattenIntoProfile(::Booleans) { _ -> true }.assignGaps(Booleans(false)) + + /** [(DOC)][countActive] Returns a [Numbers] profile that corresponds to the number of active objects at any given time. */ + fun countActive() = reduceIntoProfile(::Numbers, BinaryOperation.reduce( + { _, _ -> 1 }, + { _, acc, _ -> acc.toInt() + 1} + )).assignGaps(Numbers(0)) + + /** + * [(DOC)][accumulatedDuration] Creates a Real profile corresponding to the running total of time + * that this timeline has had an active object. + * + * @param unit base unit of time to count. As in, the resulting real profile will increase by + * `1` for each `unit` duration spent in a payload object. + * + * @see gov.nasa.jpl.aerie.timeline.ops.numeric.SerialNumericOps.integrate for further explanation of [unit]. + */ + fun accumulatedDuration(unit: Duration) = countActive().integrate(unit) + + /** + * [(DOC)][starts] Truncates each object to only its start time. + * + * The new interval is just the start time, whether the original interval + * included the start or not. Each object's content is unchanged. + */ + fun starts() = unsafeOperate { opts -> + val result = collect(CollectOptions(opts.bounds, false)) + .map { it.withNewInterval(Interval.at(it.interval.start)) } + truncateList(result, opts) + } + + /** + * [(DOC)][ends] Truncates each object to only its end time. + * + * The new interval is just the end time, whether the original interval + * included the end or not. Each object's content is unchanged. + */ + fun ends() = unsafeOperate { opts -> + val result = collect(CollectOptions(opts.bounds, false)) + .map { it.withNewInterval(Interval.at(it.interval.end)) } + truncateList(result, opts) + } + + /** + * [(DOC)][connectTo] Creates an [Intervals] object of [Connections][Connection] that associate of this timeline's + * object to the (chronologically) next object in another timeline that starts after this one ends. + * + * For a connection from an object A (in this timeline) to an object B (in the other timeline), the interval of the + * connection is [A.end, B.start]. The original intervals for A and B can be accessed from within the [Connection] object. + * + * This operation is not symmetric. All* objects in this timeline will be included in exactly one connection, but not + * all objects in the other timeline will be included, if there are no objects in this timeline that would connect to it. + * Similarly, objects in the other timeline can be connected to multiple times. + * + * If the other timeline ends prematurely and there are still more objects in this timeline, you can optionally connect + * to the end of the bounds, in which case [Connection.from] will be `null`. Otherwise, no connection will be generated. + * + * @param other the other timeline to connect to + * @param connectToBounds whether to connect to the end of the bounds if the other timeline ends prematurely + */ + fun , OTHER: ParallelOps>connectTo(other: ParallelOps, connectToBounds: Boolean) = + unsafeOperate(::Intervals) { opts -> + val sortedFrom = collect(opts).sortedWith { l, r -> l.interval.compareEnds(r.interval) } + val sortedTo = other.collect(opts).sortedWith { l, r -> l.interval.compareStarts(r.interval) } + val result = mutableListOf>() + var toIndex = 0 + for (from in sortedFrom) { + val startTime = from.interval.end + while (toIndex < sortedTo.size && from.interval.compareEndToStart(sortedTo[toIndex].interval) == 1) { + toIndex++ + } + if (!connectToBounds && toIndex == sortedTo.size) break + val endTime: Duration + val endInclusivity: Interval.Inclusivity + if (toIndex != sortedTo.size) { + val to = sortedTo[toIndex] + endTime = to.interval.start + result.add(Connection( + startTime .. endTime, + from, to + )) + } else { + endTime = opts.bounds.end + endInclusivity = opts.bounds.endInclusivity + result.add(Connection( + Interval.between(startTime, endTime, Interval.Inclusivity.Inclusive, endInclusivity), + from, null + )) + } + } + result + } + + /** + * [(DOC)][rollingDuration] Calculates the sum of durations of objects in a range leading the current time. + * + * This returns a real profile that equals, at each time `t`, the duration of objects in the interval `[t, t+range]`. + * + * Real profiles can't actually represent durations, only unitless numbers, so the result is actually calculated + * as a multiple of the provided [unit]. + * + * @param range how far into the future to look + * @param unit the time basis vector of the result; the unit of time that the result counts. + */ + fun rollingDuration(range: Duration, unit: Duration) = + accumulatedDuration(unit).shiftedDifference(range) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt new file mode 100644 index 0000000000..6af81a6605 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt @@ -0,0 +1,14 @@ +package gov.nasa.jpl.aerie.timeline.payloads + +import gov.nasa.jpl.aerie.timeline.Interval + +/** Represents a pairing of two other [IntervalLike] objects. */ +data class Connection, TO: IntervalLike>( + override val interval: Interval, + /** Object at the start of the connection. */ + val from: FROM?, + /** Object at the end of the connection. */ + val to: TO? +): IntervalLike> { + override fun withNewInterval(i: Interval) = Connection(i, from, to) +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt new file mode 100644 index 0000000000..a1ff62a826 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/NonZeroDurationOpsTest.kt @@ -0,0 +1,60 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.CollectOptions +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Inclusive +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class NonZeroDurationOpsTest { + @Test + fun split() { + val result = Numbers( + Segment(seconds(0) .. seconds(1), 1), + Segment(seconds(2) .. seconds(4), 2), + Segment(between(seconds(4), seconds(10), Exclusive), 3) + ).split { it.value }.collect() + + assertIterableEquals( + listOf( + Segment(seconds(0) .. seconds(1), 1), + Segment(between(seconds(2), seconds(3), Inclusive, Exclusive), 2), + Segment(between(seconds(3), seconds(4), Exclusive, Inclusive), 2), + Segment(between(seconds(4), seconds(6), Exclusive), 3), + Segment(between(seconds(6), seconds(8), Exclusive), 3), + Segment(between(seconds(8), seconds(10), Exclusive), 3), + ), + result + ) + } + + @Test + fun splitMarginal() { + val profile = Numbers( + Segment(between(seconds(-5), seconds(3)), 2), + Segment(seconds(7) .. seconds(13), 3), + ).split { it.value } + + assertIterableEquals( + listOf( + Segment(seconds(0) .. seconds(3), 2), + Segment(between(seconds(7), seconds(9), Inclusive, Exclusive), 3), + Segment(between(seconds(9), seconds(10), Exclusive, Inclusive), 3), + ), + profile.collect(seconds(0) .. seconds(10)) + ) + + assertIterableEquals( + listOf( + Segment(between(seconds(-1), seconds(3), Exclusive, Inclusive), 2), + Segment(between(seconds(7), seconds(9), Inclusive, Exclusive), 3), + Segment(between(seconds(9), seconds(11), Exclusive), 3), + ), + profile.collect(CollectOptions(seconds(0) .. seconds(10), false)) + ) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt new file mode 100644 index 0000000000..7284b8d400 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt @@ -0,0 +1,60 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Companion.betweenClosedOpen +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Inclusive +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.Intervals +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class ParallelOpsTest { + @Test + fun flattenIntoProfile() { + val result = Intervals( + Segment(seconds(1) .. seconds(3), 1), + Segment(seconds(0) .. seconds(2), 2), + Segment(seconds(4) .. seconds(6), 5) + ).flattenIntoProfile(::Numbers) { it.value + 1 }.collect() + + assertIterableEquals( + listOf( + Segment(betweenClosedOpen(seconds(0), seconds(1)), 3), + Segment(seconds(1) .. seconds(3), 2), + Segment(seconds(4) .. seconds(6), 6) + ), + result + ) + } + + @Test + fun reduceIntoProfile() { + val result = Intervals( + Segment(seconds(1) .. seconds(4), 1), + Segment(seconds(0) .. seconds(3), 2), + Segment(seconds(4) .. seconds(6), 5), + Segment(seconds(2) .. seconds(5), 3) + ).reduceIntoProfile(::Numbers, BinaryOperation.reduce( + { seg, _ -> seg.value }, + { seg, acc, _ -> seg.value + acc } + )).collect() + + assertIterableEquals( + listOf( + Segment(betweenClosedOpen(seconds(0), seconds(1)), 2), + Segment(betweenClosedOpen(seconds(1), seconds(2)), 3), + Segment(seconds(2) .. seconds(3), 6), + Segment(between(seconds(3), seconds(4), Exclusive), 4), + Segment(at(seconds(4)), 9), + Segment(between(seconds(4), seconds(5), Exclusive, Inclusive), 8), + Segment(between(seconds(5), seconds(6), Exclusive, Inclusive), 5) + ), + result + ) + } +} From 76c8a9b3cb6299a3534508ea40f9d6bccc8bde0f Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:41:45 -0800 Subject: [PATCH 102/159] Activity Instances and Directives --- .../aerie/timeline/collections/Directives.kt | 24 +++++++++++++++++++ .../aerie/timeline/collections/Instances.kt | 23 ++++++++++++++++++ .../jpl/aerie/timeline/ops/ActivityOps.kt | 8 +++++++ .../jpl/aerie/timeline/ops/DirectiveOps.kt | 10 ++++++++ .../jpl/aerie/timeline/ops/InstanceOps.kt | 10 ++++++++ .../timeline/ops/coalesce/CoalesceNoOp.kt | 9 +++++++ .../timeline/payloads/activities/Activity.kt | 13 ++++++++++ .../payloads/activities/AnyDirective.kt | 8 +++++++ .../payloads/activities/AnyInstance.kt | 9 +++++++ .../timeline/payloads/activities/Directive.kt | 24 +++++++++++++++++++ .../timeline/payloads/activities/Instance.kt | 24 +++++++++++++++++++ 11 files changed, 162 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt new file mode 100644 index 0000000000..063be169e1 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt @@ -0,0 +1,24 @@ +package gov.nasa.jpl.aerie.timeline.collections + +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.payloads.activities.Directive +import gov.nasa.jpl.aerie.timeline.ops.DirectiveOps +import gov.nasa.jpl.aerie.timeline.ops.ParallelOps +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceNoOp +import gov.nasa.jpl.aerie.timeline.util.preprocessList + +/** + * A timeline of activity directives. + * + * @param A the inner payload of the [Directive] type. + */ +data class Directives(private val timeline: Timeline, Directives>): + Timeline, Directives> by timeline, + ParallelOps, Directives>, + DirectiveOps>, + CoalesceNoOp, Directives> +{ + constructor(vararg directives: Directive): this(directives.asList()) + constructor(directives: List>): this(BaseTimeline(::Directives, preprocessList(directives))) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt new file mode 100644 index 0000000000..1ad4e559cc --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt @@ -0,0 +1,23 @@ +package gov.nasa.jpl.aerie.timeline.collections + +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.payloads.activities.Instance +import gov.nasa.jpl.aerie.timeline.ops.* +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceNoOp +import gov.nasa.jpl.aerie.timeline.util.preprocessList + +/** + * A timeline of activity instances. + * + * @param A the inner payload of the [Instance] type. + */ +data class Instances(private val timeline: Timeline, Instances>): + Timeline, Instances> by timeline, + ParallelOps, Instances>, + InstanceOps>, + CoalesceNoOp, Instances> +{ + constructor(vararg instances: Instance): this(instances.asList()) + constructor(instances: List>): this(BaseTimeline(::Instances, preprocessList(instances))) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt new file mode 100644 index 0000000000..18afe148de --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.payloads.activities.Activity + +/** + * Operations mixin for timelines of activities. + */ +interface ActivityOps, THIS: ActivityOps>: GeneralOps diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt new file mode 100644 index 0000000000..8dde663ea4 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.payloads.activities.Directive + +/** + * Operations mixin for timelines of activity directives. + * + * Used for both generic directives, and specific directive types from the mission model. + */ +interface DirectiveOps>: ActivityOps, THIS> diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt new file mode 100644 index 0000000000..2734cf8ddb --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.payloads.activities.Instance + +/** + * Operations mixin for timelines of activity instances. + * + * Used for both generic instances, and specific instance types from the mission model. + */ +interface InstanceOps>: ActivityOps, THIS>, NonZeroDurationOps, THIS> diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt new file mode 100644 index 0000000000..ed89ef2456 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceNoOp.kt @@ -0,0 +1,9 @@ +package gov.nasa.jpl.aerie.timeline.ops.coalesce + +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +import gov.nasa.jpl.aerie.timeline.ops.GeneralOps + +/** A no-op implementation of coalesce. */ +interface CoalesceNoOp, THIS: CoalesceNoOp>: GeneralOps { + override fun shouldCoalesce() = null +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt new file mode 100644 index 0000000000..73e62e6347 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt @@ -0,0 +1,13 @@ +package gov.nasa.jpl.aerie.timeline.payloads.activities + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike + +/** Unifying interface for activity instances and directives. */ +interface Activity: IntervalLike { + /** String type name of the activity. */ + val type: String + + /** Time that the activity starts. */ + val startTime: Duration +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt new file mode 100644 index 0000000000..80eb3f5c52 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.timeline.payloads.activities + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue + +/** A general-purpose container for representing the arguments any type of activity directive. */ +data class AnyDirective( + /***/ val arguments: Map +) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt new file mode 100644 index 0000000000..e2a3343c28 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt @@ -0,0 +1,9 @@ +package gov.nasa.jpl.aerie.timeline.payloads.activities + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue + +/** A general-purpose container for representing the arguments and computed attributes of any type of activity instance. */ +data class AnyInstance( + /***/ val arguments: Map, + /***/ val computedAttributes: Map +) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt new file mode 100644 index 0000000000..9291f7ea4b --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt @@ -0,0 +1,24 @@ +package gov.nasa.jpl.aerie.timeline.payloads.activities + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Interval + +/** A wrapper of any type of activity directive containing common data. */ +data class Directive( + /** The inner payload, typically either [AnyDirective] or a mission model activity type. */ + val inner: A, + + /** The name of this specific directive. */ + val name: String, + + override val type: String, + override val startTime: Duration +): Activity> { + override val interval: Interval + get() = Interval.at(startTime) + + override fun withNewInterval(i: Interval): Directive { + if (i.isPoint()) return Directive(inner, name, type, i.start) + else throw Exception("Cannot change directive time to a non-instantaneous interval.") + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt new file mode 100644 index 0000000000..cbcf62de7e --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt @@ -0,0 +1,24 @@ +package gov.nasa.jpl.aerie.timeline.payloads.activities + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Interval + +/** A wrapper of any type of activity instance containing common data. */ +data class Instance( + /** The inner payload, typically either [AnyInstance] or a mission model activity type. */ + val inner: A, + override val type: String, + + /** + * The maybe-null id of the directive associated with this instance. + * + * Will be `null` if this is a child activity. + */ + val directiveId: Long?, + override val interval: Interval, +): Activity> { + override val startTime: Duration + get() = interval.start + + override fun withNewInterval(i: Interval) = Instance(inner, type, directiveId, i) +} From 15efd4dcb77abfb494ea8b5385234673dca4979e Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:46:41 -0800 Subject: [PATCH 103/159] Serial profile infrastructure and operations --- .../jpl/aerie/timeline/BinaryOperation.kt | 124 +++++++++++ .../nasa/jpl/aerie/timeline/ops/SegmentOps.kt | 50 +++++ .../nasa/jpl/aerie/timeline/ops/SerialOps.kt | 158 ++++++++++++++ .../jpl/aerie/timeline/payloads/Segment.kt | 63 ++++++ .../aerie/timeline/util/Map2SegmentLists.kt | 161 +++++++++++++++ .../nasa/jpl/aerie/timeline/SegmentTest.kt | 18 ++ .../jpl/aerie/timeline/ops/SerialOpsTest.kt | 52 +++++ .../jpl/aerie/timeline/util/Map2SerialTest.kt | 195 ++++++++++++++++++ 8 files changed, 821 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BinaryOperation.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SegmentLists.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOpsTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SerialTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BinaryOperation.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BinaryOperation.kt new file mode 100644 index 0000000000..504f9ec2b4 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BinaryOperation.kt @@ -0,0 +1,124 @@ +package gov.nasa.jpl.aerie.timeline + +/** + * A generalized binary operation interface for maybe-null operands. + * + * This is a function interface for the [invoke] method, which takes in maybe-null + * left and right operands, and outputs a result. + * + * ## Construction Helper Functions + * + * Helper functions for constructing binary operations with common patterns are available + * in this interface's companion object [here][Companion]. Unfortunately, Kotlin's documentation + * generator Dokka doesn't like to show companion object methods inside interfaces, but all these + * methods can be called just like a static method (i.e. `BinaryOperation.combineOrNull(...)`). + */ +fun interface BinaryOperation { + + /** + * Calculate the operation. + * + * @param l left operand; can be null, usually when the operand has a gap. + * @param r right operand; can be null, usually when the operand has a gap. + * @param i interval on which the operation is being calculated. + */ + operator fun invoke(l: Left?, r: Right?, i: Interval): Out + + /** Helper functions for constructing binary operations. */ + companion object { + /** + * Constructs an operation from three different cases: + * - when only the left operand is present + * - when only the right operand is present + * - when both operands are present + */ + @JvmStatic fun cases( + left: (Left & Any, Interval) -> Out, + right: (Right & Any, Interval) -> Out, + combine: (Left & Any, Right & Any, Interval) -> Out + ) = BinaryOperation { l, r, i -> + if (l != null && r != null) combine(l, r, i) + else if (l != null) left(l, i) + else if (r != null) right(r, i) + else throw BinaryOperationBothNullException() + } + + /** A named overload of the default constructor. */ + @JvmStatic fun singleFunction(f: (Left?, Right?, Interval) -> Out) = BinaryOperation(f) + + /** + * Constructs an operation that combines the operands in some way if they are both present, + * and produces null if either operand is null. + * + * @param f operation to be invoked when both operands are present. + */ + @JvmStatic fun combineOrNull(f: (Left & Any, Right & Any, Interval) -> Out) = BinaryOperation { l, r, i -> + if (l == null || r == null) null + else f(l, r, i) + } + + /** + * Constructs an operation that combines the operands of equal type if they are both present. + * If either operand is not present, the other is passed through unchanged. + * + * This means that both operands and the output must all be the same type. + */ + @JvmStatic fun combineOrIdentity(f: (V & Any, V & Any, Interval) -> V) = BinaryOperation { l, r, i -> + if (l != null && r != null) f(l, r, i) + else l ?: r ?: throw BinaryOperationBothNullException() + } + + /** + * Constructs a binary operation for a reduce-style timeline operation. + * + * This pattern uses an input and output type, which in most cases will be equal. + * The code that invokes this operation will keep track of an "accumulator", which may start uninitialized. + * If the accumulator is `null`, the "convert" function is used to take one input and create the accumulator. + * If the accumulator is defined, the "combine" function is used to combine it with new inputs until the input is consumed. + * The output is the final value of the accumulator. + * + * @param convert Converts an input into the accumulator. + * @param combine Combines the accumulator with a new input. + * + * See [gov.nasa.jpl.aerie.timeline.ops.ParallelOps.reduceIntoProfile] for an example of where it should be used. + */ + @JvmStatic fun reduce( + convert: (new: In & Any, Interval) -> Out, + combine: (new: In & Any, acc: Out & Any, Interval) -> Out + ) = BinaryOperation { new, acc, i -> + if (acc != null && new != null) combine(new, acc, i) + else if (new != null) convert(new, i) + else acc ?: throw BinaryOperationBothNullException() + } + + /** + * Constructs a binary operation which passes operands through unchanged if only one is present. + * + * Throws [ZipOperationBothDefinedException] if both operands are present. + */ + @JvmStatic fun zip() = BinaryOperation { l, r, _ -> + if (l != null && r != null) throw ZipOperationBothDefinedException() + else l ?: (r ?: throw BinaryOperationBothNullException()) + } + + /** + * Constructs a binary operation which converts either operand to the output if only one is present. + * + * Throws [ZipOperationBothDefinedException] if both operands are present. + */ + @JvmStatic fun convertZip( + left: (Left & Any, Interval) -> Out, + right: (Right & Any, Interval) -> Out, + ) = BinaryOperation { l, r, i -> + if (l != null && r != null) throw ZipOperationBothDefinedException() + else if (l != null) left(l, i) + else if (r != null) right(r, i) + else throw BinaryOperationBothNullException() + } + } + + /** Thrown if both arguments in a binary operation are null. */ + class BinaryOperationBothNullException: Exception("Both arguments to binary operation were null.") + /** Thrown by [zip] if both arguments to the binary operation are defined. */ + class ZipOperationBothDefinedException: Exception("Both arguments to zip binary operation were defined.") +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt new file mode 100644 index 0000000000..6f8652489c --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt @@ -0,0 +1,50 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.BoundsTransformer.Companion.IDENTITY +import gov.nasa.jpl.aerie.timeline.payloads.Segment + +/** + * Operations mixin for timelines of segments. + */ +interface SegmentOps>: NonZeroDurationOps, THIS> { + /** + * [(DOC)][mapValues] Locally transforms the values of a profile without changing the intervals or profile type. + * + * @param f a function which takes a [Segment] and returns a new value of type [V] + */ + fun mapValues(f: (Segment) -> V) = unsafeMap(IDENTITY, false) { it.mapValue(f) } + + /** + * [(DOC)][mapValues] Locally transforms the values of a profile without changing the intervals. + * + * The result can be a different profile type. + * + * @param R the result's payload type + * @param RESULT the result's timeline type + * @param ctor the constructor of the result profile + * @param f a function which takes a [Segment] and returns a new value of any type + */ + fun > mapValues(ctor: (Timeline, RESULT>) -> RESULT, f: (Segment) -> R) = + unsafeMap(ctor, IDENTITY, false) { it.mapValue(f) } + + /** [(DOC)][flatMapValues] A simpler version of [flatMapValues] for operations that don't change the timeline type. */ + fun > flatMapValues(f: (Segment) -> NESTED) = + unsafeFlatMap(ctor, IDENTITY, false) { it.mapValue(f) } + + /** + * [(DOC)][flatMapValues] Maps segments into a collection of nested timelines and flattens them into their original intervals. + * + * Similar to [GeneralOps.unsafeFlatMap] except that the mapper function cannot change the interval the nested timeline + * is flattened into. + * + * @param R the result payload type + * @param NESTED the nested timeline type; typically the same as [RESULT] + * @param RESULT the result timeline type + * + * @param ctor the constructor of the result timeline + * @param f a mapper function that converts each timeline object to a nested timeline + */ + fun , RESULT: SegmentOps> flatMapValues(ctor: (Timeline, RESULT>) -> RESULT, f: (Segment) -> NESTED) = + unsafeFlatMap(ctor, IDENTITY, false) { it.mapValue(f) } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt new file mode 100644 index 0000000000..3ebcf852b7 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt @@ -0,0 +1,158 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.transpose +import gov.nasa.jpl.aerie.timeline.util.coalesceList +import gov.nasa.jpl.aerie.timeline.util.map2SegmentLists +import gov.nasa.jpl.aerie.timeline.util.truncateList + +/** + * Operations mixin for timelines of ordered, non-overlapping segments (profiles). + * + * Opposite of [ParallelOps]. + */ +interface SerialOps>: SegmentOps { + /** Overlays two profiles on each other, asserting that they both cannot be defined at the same time. */ + infix fun > zip(other: SerialOps) = map2Values(other, BinaryOperation.zip()) + + /** [(DOC)][assignGaps] Fills in gaps in this profile with another profile. */ + // While this is logically the converse of [set], they can't delegate to each other because it would mess up the return type. + infix fun > assignGaps(other: SerialOps) = + map2Values(other, BinaryOperation.combineOrIdentity { l, _, _, -> l }) + /** [(DOC)][assignGaps] Fills in gaps in this profile with a constant value. */ + infix fun assignGaps(v: V) = assignGaps(Constants(v)) + + /** [(DOC)][set] Overwrites this profile with another. Gaps in the argument profile will be filled in with this profile. */ + infix fun > set(other: SerialOps) = map2Values(other, BinaryOperation.combineOrIdentity { _, r, _ -> r }) + + /** + * [(DOC)][map2Values] Performs a local binary operation between two profiles where the result + * is the same type as this profile. + */ + fun > map2Values(other: SerialOps, op: BinaryOperation) = map2Values(ctor, other, op) + + /** + * [(DOC)][map2Values] Performs a local binary operation between two profiles. + * + * The operation will be evaluated on each pair of segments that overlap, with their + * intersection supplied as the interval argument to the [BinaryOperation]. The result of + * the operation is inserted in the result timeline at that intersection. Additionally, + * The operation will be evaluated on each segment in the profiles that overlaps with + * a gap in the other profile, and the gap will be indicated by a `null` in that operand's + * argument in [BinaryOperation]. The operation is not called for intervals that have a gap + * in both profiles - the result will automatically have a gap there. + * + * The binary operation may return `null`, which indicates that the result profile should have + * a gap. + * + * The operation is "local", meaning that while the operation is allowed to know when it is + * being evaluated, it is not allowed to change where the result segment should be placed. + * For that, you can use [unsafeMap], or more generally, [unsafeOperate]. + * + * @param W the other operand's payload type + * @param OTHER the other operand's timeline type + * @param R the result's payload type + * @param RESULT the result's timeline type + * + * @param ctor the result timeline's constructor + * @param other the other operand timeline + * @param op a binary operation between the two payload types that produces a maybe-null result + * + * @return a coalesced profile; an instance of the return type of [ctor] + */ + fun , R: Any, RESULT: GeneralOps, RESULT>> map2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SerialOps, op: BinaryOperation) = + unsafeOperate(ctor) { bounds -> map2SegmentLists(collect(bounds), other.collect(bounds), op) } + + /** + * [(DOC)][flatMap2Values] Performs a local binary operation that produces profiles, and flattens + * it into a profile of the same type as this. + */ + fun , NESTED: SerialOps> flatMap2Values(other: SerialOps, op: BinaryOperation) = flatMap2Values(ctor, other, op) + + /** + * [(DOC)][flatMap2Values] Performs a local binary operation that produces profiles, and flattens it. + * + * Similar to [map2Values], except it expects the [BinaryOperation] to return a profile. Each nested profile + * is then collected on the interval it corresponds to, and the results are concatenated into a single profile. + * + * This is useful for binary operations where at least one of the operand segments represents a value that + * varies within the segment, such as [gov.nasa.jpl.aerie.timeline.collections.profiles.Real]. + * + * @param W the other operand's payload type + * @param OTHER the other operand's timeline type + * @param R the result's payload type + * @param NESTED the nested profile type returned by the operation before flattening + * @param RESULT the result's timeline type + * + * @param ctor the result timeline's constructor + * @param other the other operand timeline + * @param op a binary operation between the two payload types that produces a maybe-null profile + * + * @return a coalesced flattened profile; an instance of the return type of [ctor] + */ + fun , R: Any, NESTED: SerialOps, RESULT: GeneralOps, RESULT>> flatMap2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SerialOps, op: BinaryOperation) = + unsafeOperate(ctor) { opts -> + map2SegmentLists(collect(opts), other.collect(opts), op) + .flatMap { it.value.collect(CollectOptions(it.interval, true)) } + } + + /** + * [(DOC)][detectEdges] Uses a [BinaryOperation] as a predicate to highlight edges between segments. + * + * The result is a [Booleans] profile, which is `false` inside segments, a gap when + * this profile is a gap, and possibly `true`, `false`, or a gap on segment edges according + * to the result of the predicate. + * + * The predicate will be called at the edge between two adjacent segments, where the values of + * the left and right segments are the left and right operands of the predicate, respectively. + * The predicate will also be call at any edge where a segment meets a gap, in which case the appropriate operand + * will be `null`. + * + * @param edgePredicate a binary operation between operands of type [V] that produces a boolean or `null` + * @return a [Booleans] object that contains `true` on the edges indicated by the predicate + */ + fun detectEdges(edgePredicate: BinaryOperation) = unsafeOperate(::Booleans) { opts -> + val bounds = opts.bounds + var buffer: Segment? = null + val result = collect(CollectOptions(bounds, false)) + .flatMap { currentSegment -> + val previous = buffer + buffer = currentSegment + val currentInterval = currentSegment.interval + + val leftEdgeInterval = at(currentInterval.start) + val rightEdgeInterval = at(currentInterval.end) + + val rightEdge = edgePredicate(currentSegment.value, null, rightEdgeInterval) + + val leftEdge = if (previous == null || previous.interval.compareEndToStart(currentInterval) == -1) { + edgePredicate(null, currentSegment.value, leftEdgeInterval) + } else { + edgePredicate(previous.value, currentSegment.value, leftEdgeInterval) + } + + listOfNotNull( + Segment(leftEdgeInterval, leftEdge).transpose(), + Segment( + Interval.between(currentInterval.start, currentInterval.end, Interval.Inclusivity.Exclusive), + false + ), + Segment(rightEdgeInterval, rightEdge).transpose() + ) + } + truncateList(coalesceList(result, Segment::valueEquals), opts) + } + + /** + * [(DOC)][changes] Returns a [Booleans] that is true whenever this profile changes, and false or gap everywhere else. + * + * This includes both continuous changes and discontinuous changes, if the profile can vary continuously. + */ + fun changes(): Booleans + // `transitions(from, to)` is a similar function that you expect to also have a declaration here, but this isn't + // feasible because `Real.transitions` takes doubles as its arguments instead of its normal payload type (LinearEquation) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt new file mode 100644 index 0000000000..be54c9663c --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt @@ -0,0 +1,63 @@ +package gov.nasa.jpl.aerie.timeline.payloads + +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.util.coalesceList + +/** + * A generic container that associates a value with an interval on a timeline. + */ +data class Segment(/***/ override val interval: Interval, /***/ val value: V): IntervalLike> { + /** + * Create a new segment on the same interval, with a new value derived from this segment. + * + * In most cases using [withNewValue] will result in simpler-looking code. + * + * @param f a function that takes `this` as argument and produces a new value. + */ + fun mapValue(f: (Segment) -> W) = Segment(interval, f(this)) + + /** + * Create a new segment with the same value, on a new interval derived from this segment. + * + * In most cases using [withNewInterval] will result in simpler-looking code. + * + * @param f a function that takes `this` as argument and produces a new interval. + */ + fun mapInterval(f: (Segment) -> Interval) = Segment(f(this), value) + + /** + * Creates a new segment with the same value on a new interval. + * + * @param i the new interval + */ + override fun withNewInterval(i: Interval) = Segment(i, value) + + /** + * Creates a new segment on the same interval with a new value. + * + * @param w the new value + */ + fun withNewValue(w: W): Segment = Segment(interval, w) + + /** + * Checks whether this segment's value equals another, using the basic equality operator. + * + * Used mostly for the [coalesceList] operation. + */ + fun valueEquals(other: Segment) = value == other.value + + /***/ + companion object { + /** + * Delegates to the constructor. + * + * Can be more convenient in Java by avoiding the `new` keyword. + */ + @JvmStatic fun of(interval: Interval, value: V) = Segment(interval, value) + } +} + +/** + * Converts a non-null segment of a maybe-null value into a maybe-null segment of a non-null value. + */ +fun Segment.transpose() = if (value == null) null else Segment(interval, value) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SegmentLists.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SegmentLists.kt new file mode 100644 index 0000000000..5fba7b4e66 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SegmentLists.kt @@ -0,0 +1,161 @@ +package gov.nasa.jpl.aerie.timeline.util + +import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.transpose + +/** + * Low level routine for performing a binary operation on a pair of segment lists. + * + * Assumes both lists are properly coalesced - NOT CHECKED. Violating this assumption is UB. + * + * The resulting segment list is defined as follows. (This is a definition of the result, not the algorithm to calculate it.) + * For each pair of segments `l` and `r` from the left and right lists (respectively): + * - let `i` be the intersection between `l.interval` and `r.interval`. + * - if `i` is not empty, let `o` be the output of `op(l.value, r.value, i)`. + * - if `o` is not `null`, the result will contain a segment with value `o` on the interval `i`. + * Additionally, for each pair of segment `s` in either list and gap in the other list: + * - let `i` be the intersection between `s.interval` and the gap. + * - if `i` is not empty, let `o` be the output of either `op(s.value, null, i)` or `op(null, s.value, i)`, depending on which list the segment came from. + * - if `o` is not `null`, the result will contain a segment with value `o` on the interval `i`. + * + * + * This routine performs a single pass down each list, with a computational complexity + * proportional to the total number of segments in both lists. + */ +fun map2SegmentLists( + left: List>, + right: List>, + op: BinaryOperation +): List> { + val result = mutableListOf>() + + var leftIndex = 0 + var rightIndex = 0 + + var leftSegment: Segment? + var rightSegment: Segment? + var remainingLeftSegment: Segment? = null + var remainingRightSegment: Segment? = null + + while ( + leftIndex < left.size || + rightIndex < right.size || + remainingLeftSegment != null || + remainingRightSegment != null + ) { + if (remainingLeftSegment != null) { + leftSegment = remainingLeftSegment + remainingLeftSegment = null + } else if (leftIndex < left.size) { + leftSegment = left[leftIndex++] + } else { + leftSegment = null + } + if (remainingRightSegment != null) { + rightSegment = remainingRightSegment + remainingRightSegment = null + } else if (rightIndex < right.size) { + rightSegment = right[rightIndex++] + } else { + rightSegment = null + } + + if (leftSegment == null) { + val resultingSegment = rightSegment!!.mapValue { op(null, it.value, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } else if (rightSegment == null) { + val resultingSegment = leftSegment.mapValue { op(it.value, null, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } else { + val startComparison = leftSegment.interval.compareStarts(rightSegment.interval) + if (startComparison == -1) { + remainingRightSegment = rightSegment + val endComparison = leftSegment.interval.compareEndToStart(rightSegment.interval) + if (endComparison < 1) { + val resultingSegment = leftSegment.mapValue { op(it.value, null, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } else { + remainingLeftSegment = leftSegment.mapInterval { + Interval.between( + rightSegment.interval.start, + it.interval.end, + rightSegment.interval.startInclusivity, + it.interval.endInclusivity + ) + } + val resultingSegment = Segment( + Interval.between( + leftSegment.interval.start, + rightSegment.interval.start, + leftSegment.interval.startInclusivity, + rightSegment.interval.startInclusivity.opposite() + ), + leftSegment.value + ).mapValue { op(it.value, null, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } + } else if (startComparison == 1) { + remainingLeftSegment = leftSegment + val endComparison = rightSegment.interval.compareEndToStart(leftSegment.interval) + if (endComparison < 1) { + val resultingSegment = rightSegment.mapValue { op(null, it.value, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } else { + remainingRightSegment = rightSegment.mapInterval { + Interval.between( + leftSegment.interval.start, + it.interval.end, + leftSegment.interval.startInclusivity, + it.interval.endInclusivity + ) + } + val resultingSegment = Segment( + Interval.between( + rightSegment.interval.start, + leftSegment.interval.start, + rightSegment.interval.startInclusivity, + leftSegment.interval.startInclusivity.opposite() + ), + rightSegment.value + ).mapValue { op(null, it.value, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } + } else { + val endComparison = leftSegment.interval.compareEnds(rightSegment.interval) + if (endComparison == -1) { + remainingRightSegment = rightSegment.mapInterval { + Interval.between( + leftSegment.interval.end, + it.interval.end, + leftSegment.interval.endInclusivity.opposite(), + it.interval.endInclusivity + ) + } + val resultingSegment = leftSegment + .mapValue { op(it.value, rightSegment.value, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } else if (endComparison == 1) { + remainingLeftSegment = leftSegment.mapInterval { + Interval.between( + rightSegment.interval.end, + it.interval.end, + rightSegment.interval.endInclusivity.opposite(), + it.interval.endInclusivity + ) + } + val resultingSegment = rightSegment + .mapValue { op(leftSegment.value, it.value, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } else { + val resultingSegment = leftSegment + .mapValue { op(it.value, rightSegment.value, it.interval) }.transpose() + if (resultingSegment != null) result.add(resultingSegment) + } + } + } + } + + return result +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt new file mode 100644 index 0000000000..30c5a780cb --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/SegmentTest.kt @@ -0,0 +1,18 @@ +package gov.nasa.jpl.aerie.timeline + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.transpose +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +class SegmentTest { + + @Test + fun transpose() { + assertEquals(Segment(at(seconds(2)), 5), Segment(at(seconds(2)), 5 as Int?).transpose()) + assertEquals(null, Segment(at(seconds(2)), null as Int?).transpose()) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOpsTest.kt new file mode 100644 index 0000000000..fd9fb62627 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOpsTest.kt @@ -0,0 +1,52 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.Duration.Companion.milliseconds +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Companion.betweenClosedOpen +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Inclusive +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class SerialOpsTest { + + // set, assignGaps, and map2Values are not tested here because they are trivial delegations to map2Serial. + // see Map2SerialTest.kt + + @Test + fun detectEdges() { + val result = Constants( + Segment(betweenClosedOpen(seconds(0), seconds(1)), "hello"), + Segment(seconds(1) .. seconds(2), "oooo"), + Segment(between(seconds(2), seconds(3), Exclusive), "aaaa"), + Segment(seconds(5) .. seconds(6), "ao") + ).detectEdges(BinaryOperation.cases( + { l, _ -> l.endsWith('o') }, + { r, _ -> r.startsWith('o') }, + { l, r, _ -> l.endsWith(r.first()) } + )) + + assertIterableEquals( + listOf( + Segment(betweenClosedOpen(seconds(0), seconds(1)), false), + Segment(at(seconds(1)), true), + Segment(between(seconds(1), seconds(3), Exclusive, Inclusive), false), + Segment(betweenClosedOpen(seconds(5), seconds(6)), false), + Segment(at(seconds(6)), true) + ), + result.collect() + ) + + // collecting on smaller bounds that start in the middle of "oooo" to make sure it does not get truncated + // and count the bounds start as the segment start. + assertIterableEquals( + listOf(Segment(between(milliseconds(1500), seconds(3)), false)), + result.collect(between(milliseconds(1500), seconds(4))) + ) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SerialTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SerialTest.kt new file mode 100644 index 0000000000..608d029656 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SerialTest.kt @@ -0,0 +1,195 @@ +package gov.nasa.jpl.aerie.timeline.util + +import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.* +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested + +class Map2SerialTest { + + @Test + fun basicCombineOrIdentity() { + val left = listOf(Segment(seconds(0) .. seconds(2), 2)) + val right = listOf(Segment(seconds(1) .. seconds(3), 3)) + + val result = map2SegmentLists( + left, right, + BinaryOperation.combineOrIdentity { l, r, _ -> l + r} + ) + + val expected = listOf( + Segment(seconds(0) ..< seconds(1), 2), + Segment(seconds(1) .. seconds(2), 5), + Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), 3), + ) + + assertIterableEquals(expected, result) + } + + @Test + fun basicCombineOrUndefined() { + val left = listOf(Segment(seconds(0) .. seconds(2), 2)) + val right = listOf(Segment(seconds(1) .. seconds(3), 3)) + + val result = map2SegmentLists( + left, right, + BinaryOperation.combineOrNull { l, r, _ -> l + r} + ) + + val expected = listOf( + Segment(seconds(1) .. seconds(2), 5) + ) + + assertIterableEquals(expected, result) + } + + @Nested + inner class SegmentAlignment { + + // Helper functions for below + val op = BinaryOperation.combineOrIdentity { l, r, _ -> l + r } + fun makeLeft(s: Long, e: Long, si: Interval.Inclusivity = Inclusive, ei: Interval.Inclusivity = Inclusive) = + listOf(Segment(between(seconds(s), seconds(e), si, ei), -1)) + + fun makeRight(s: Long, e: Long, si: Interval.Inclusivity = Inclusive, ei: Interval.Inclusivity = Inclusive) = + listOf(Segment(between(seconds(s), seconds(e), si, ei), 1)) + + @Test + fun identical() { + assertIterableEquals( + listOf(Segment(seconds(1) .. seconds(2), 0)), + map2SegmentLists(makeLeft(1, 2), makeRight(1, 2), op) + ) + } + + @Test + fun identicalExclusive() { + assertIterableEquals( + listOf(Segment(between(seconds(1), seconds(2), Exclusive, Exclusive), 0)), + map2SegmentLists(makeLeft(1, 2, Exclusive, Exclusive), makeRight(1, 2, Exclusive, Exclusive), op) + ) + } + + @Test + fun entireLeftSegmentFirst() { + assertIterableEquals( + listOf( + Segment(seconds(1) .. seconds(2), -1), + Segment(seconds(3) .. seconds(4), 1) + ), + map2SegmentLists(makeLeft(1, 2), makeRight(3, 4), op) + ) + } + + @Test + fun entireRightSegmentFirst() { + assertIterableEquals( + listOf( + Segment(seconds(1) .. seconds(2), 1), + Segment(seconds(3) .. seconds(4), -1) + ), + map2SegmentLists(makeLeft(3, 4), makeRight(1, 2), op) + ) + } + + @Test + fun leftFirstMomentOfOverlap() { + assertIterableEquals( + listOf( + Segment(between(seconds(1), seconds(2), endInclusivity = Exclusive), -1), + Segment(Interval.at(seconds(2)), 0), + Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), 1) + ), + map2SegmentLists(makeLeft(1, 2), makeRight(2, 3), op) + ) + } + + @Test + fun rightFirstMomentOfOverlap() { + assertIterableEquals( + listOf( + Segment(between(seconds(1), seconds(2), endInclusivity = Exclusive), 1), + Segment(Interval.at(seconds(2)), 0), + Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), -1) + ), + map2SegmentLists(makeLeft(2, 3), makeRight(1, 2), op) + ) + } + + @Test + fun leftFirstMomentOfNonOverlap() { + assertIterableEquals( + listOf( + Segment(Interval.at(seconds(1)), -1), + Segment(between(seconds(1), seconds(2), Exclusive, Inclusive), 0) + ), + map2SegmentLists(makeLeft(1, 2), makeRight(1, 2, Exclusive, Inclusive), op) + ) + } + + @Test + fun rightFirstMomentOfNonOverlap() { + assertIterableEquals( + listOf( + Segment(Interval.at(seconds(1)), 1), + Segment(between(seconds(1), seconds(2), Exclusive, Inclusive), 0) + ), + map2SegmentLists(makeLeft(1, 2, Exclusive, Inclusive), makeRight(1, 2), op) + ) + } + + @Test + fun leftFirstHalfNonOverlap() { + assertIterableEquals( + listOf( + Segment(seconds(1) ..< seconds(2), -1), + Segment(seconds(2) .. seconds(3), 0), + Segment(between(seconds(3), seconds(4), Exclusive, Inclusive), 1), + ), + map2SegmentLists(makeLeft(1, 3), makeRight(2, 4), op) + ) + } + + @Test + fun rightFirstHalfNonOverlap() { + assertIterableEquals( + listOf( + Segment(seconds(1) ..< seconds(2), 1), + Segment(seconds(2) .. seconds(3), 0), + Segment(between(seconds(3), seconds(4), Exclusive, Inclusive), -1), + ), + map2SegmentLists(makeLeft(2, 4), makeRight(1, 3), op) + ) + } + + @Test + fun leftContainsRight() { + assertIterableEquals( + listOf( + Segment(seconds(1) ..< seconds(2), -1), + Segment(seconds(2) .. seconds(3), 0), + Segment(between(seconds(3), seconds(4), Exclusive, Inclusive), -1), + ), + map2SegmentLists(makeLeft(1, 4), makeRight(2, 3), op) + ) + } + + @Test + fun rightContainsLeft() { + assertIterableEquals( + listOf( + Segment(seconds(1) ..< seconds(2), 1), + Segment(seconds(2) .. seconds(3), 0), + Segment(between(seconds(3), seconds(4), Exclusive, Inclusive), 1), + ), + map2SegmentLists(makeLeft(2, 3), makeRight(1, 4), op) + ) + } + } +} From 5127c6c7a72f766255f9ebf2b19e2caf2823f584 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:46:28 -0800 Subject: [PATCH 104/159] Piecewise constant profiles --- .../collections/profiles/Constants.kt | 31 ++++++++++++++++ .../jpl/aerie/timeline/ops/ConstantOps.kt | 23 ++++++++++++ .../aerie/timeline/ops/SerialConstantOps.kt | 36 ++++++++++++++++++ .../ops/coalesce/CoalesceSegmentsOp.kt | 9 +++++ .../aerie/timeline/ops/SerialConstantTest.kt | 37 +++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt new file mode 100644 index 0000000000..029e81751d --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt @@ -0,0 +1,31 @@ +package gov.nasa.jpl.aerie.timeline.collections.profiles + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.ops.SerialConstantOps +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.util.preprocessList + +/** A profile of piece-wise constant values. */ +data class Constants(private val timeline: Timeline, Constants>): + Timeline, Constants> by timeline, + CoalesceSegmentsOp>, + SerialConstantOps> +{ + constructor(v: V): this(Segment(Interval.MIN_MAX, v)) + constructor(vararg segments: Segment): this(segments.asList()) + constructor(segments: List>): this(BaseTimeline(::Constants, preprocessList(segments, Segment::valueEquals))) + + /***/ companion object { + /** + * Delegates to the constructor, for use with [gov.nasa.jpl.aerie.timeline.plan.Plan.resource]. + * + * Does not convert the serialized values because it does not know what it should be converted into. + * You will need to do that yourself using [mapValues]. + */ + @JvmStatic fun deserialize(list: List>) = Constants(list) + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt new file mode 100644 index 0000000000..6fb1072aa6 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt @@ -0,0 +1,23 @@ +package gov.nasa.jpl.aerie.timeline.ops + +/** + * Operations mixin for segment-valued timelines whose payloads + * represent constant values. + */ +interface ConstantOps>: SegmentOps { + /** + * [(DOC)][isolateEqualTo] Isolates intervals where the value is equal to a specific value. + * @see [GeneralOps.isolate] + */ + fun isolateEqualTo(value: V) = isolate { it.value == value } + + /** + * [(DOC)][splitEqualTo] Splits segments where the value is equal to a specific value. + * + * @see [split] + * + * @param value the value of the segments to split + * @param numPieces the number of pieces to split the segments into + */ + fun splitEqualTo(value: V, numPieces: Int) = split { if (it.value == value) numPieces else 1 } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt new file mode 100644 index 0000000000..5528845146 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt @@ -0,0 +1,36 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants + +/** + * Operations mixin for segment-valued timelines whose payloads + * represent constant values. + */ +interface SerialConstantOps>: SerialOps, ConstantOps { + + /** [(DOC)][equalTo] Returns a [Booleans] that is `true` when this and another profile are equal. */ + infix fun > equalTo(other: OTHER) = + map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> l == r }) + /** [(DOC)][equalTo] Returns a [Booleans] that is `true` when this equals a constant value. */ + infix fun equalTo(v: V) = equalTo(Constants(v)) + + /** [(DOC)][notEqualTo] Returns a [Booleans] that is `true` when this and another profile are not equal. */ + infix fun > notEqualTo(other: OTHER) = + map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> l != r }) + /** [(DOC)][notEqualTo] Returns a [Booleans] that is `true` when this is not equal to a constant value. */ + infix fun notEqualTo(v: V) = notEqualTo(Constants(v)) + + override fun changes() = detectEdges(BinaryOperation.combineOrNull { l, r, _-> l != r }) + + /** + * [(DOC)][transitions] Returns a [Booleans] that is `true` when this profile's value changes between + * two specific values. + */ + fun transitions(from: V, to: V) = detectEdges(BinaryOperation.cases( + { l, _ -> if (l == from) null else false }, + { r, _ -> if (r == to) null else false }, + { l, r, _ -> l == from && r == to } + )) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt new file mode 100644 index 0000000000..fb8b95f8c9 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceSegmentsOp.kt @@ -0,0 +1,9 @@ +package gov.nasa.jpl.aerie.timeline.ops.coalesce + +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.ops.GeneralOps + +/** Implements coalescing for segments. */ +interface CoalesceSegmentsOp>: GeneralOps, THIS> { + override fun shouldCoalesce() = Segment::valueEquals +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt new file mode 100644 index 0000000000..d9834f3bfe --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantTest.kt @@ -0,0 +1,37 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Inclusive +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class SerialConstantTest { + @Test + fun transitions() { + val result = Numbers( + Segment(seconds(0) .. seconds(1), 0), + Segment(seconds(2) .. seconds(3), 1), + Segment(seconds(4) .. seconds(5), 0), + Segment(seconds(5) .. seconds(6), 5), + Segment(seconds(6) .. seconds(7), 1), + Segment(seconds(7) .. seconds(8), 0), + Segment(seconds(8) .. seconds(9), 1) + ).transitions(0, 1).collect() + + assertIterableEquals( + listOf( + Segment(between(seconds(0), seconds(1), Inclusive, Exclusive), false), + Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), false), + Segment(between(seconds(4), seconds(8), Inclusive, Exclusive), false), + Segment(at(seconds(8)), true), + Segment(between(seconds(8), seconds(9), Exclusive, Inclusive), false) + ), + result + ) + } +} From b81ead1844a5fe7c594447a4eb47bfebc904e87b Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:47:38 -0800 Subject: [PATCH 105/159] Boolean profiles --- .../timeline/collections/profiles/Booleans.kt | 29 +++++ .../nasa/jpl/aerie/timeline/ops/BooleanOps.kt | 31 +++++ .../aerie/timeline/ops/SerialBooleanOps.kt | 106 ++++++++++++++++++ .../aerie/timeline/ops/SerialBooleanTest.kt | 71 ++++++++++++ 4 files changed, 237 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt new file mode 100644 index 0000000000..0bbb1e8a15 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt @@ -0,0 +1,29 @@ +package gov.nasa.jpl.aerie.timeline.collections.profiles + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.ops.SerialBooleanOps +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.util.preprocessList + +/** A profile of booleans. */ +data class Booleans(private val timeline: Timeline, Booleans>): + Timeline, Booleans> by timeline, + SerialBooleanOps, + CoalesceSegmentsOp +{ + constructor(v: Boolean): this(Segment(Interval.MIN_MAX, v)) + constructor(vararg segments: Segment): this(segments.asList()) + constructor(segments: List>): this(BaseTimeline(::Booleans, preprocessList(segments, Segment::valueEquals))) + + /***/ companion object { + /** + * Converts a list of serialized value segments into a [Booleans] profile; + * for use with [gov.nasa.jpl.aerie.timeline.plan.Plan.resource]. + */ + @JvmStatic fun deserialize(list: List>) = Booleans(list.map { it.withNewValue(it.value.asBoolean().get()) }) + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt new file mode 100644 index 0000000000..eae4eb918f --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt @@ -0,0 +1,31 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.collections.Intervals + +/** + * Operations mixin for timelines of booleans. + * + * Currently only used by [gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans], but could be used for + * parallel boolean profiles in the future. + */ +interface BooleanOps>: ConstantOps { + /** [(DOC)][not] Applies the unary NOT operation. */ + operator fun not() = mapValues { !it.value } + + /** [(DOC)][falsifyByDuration] Falsifies any `true` segments with durations outside the given interval. */ + fun falsifyByDuration(validInterval: Interval) = + mapValues { it.value && it.interval.duration() in validInterval } + + /** [(DOC)][falsifyShorterThan] Falsifies any `true` segments with durations shorter than the given duration. */ + fun falsifyShorterThan(dur: Duration) = falsifyByDuration(dur .. Duration.MAX_VALUE) + /** [(DOC)][falsifyLongerThan] Falsifies any `true` segments with durations longer than the given duration. */ + fun falsifyLongerThan(dur: Duration) = falsifyByDuration(Duration.MIN_VALUE .. dur) + + /** [(DOC)][isolateTrue] Creates an [Intervals] objects with intervals whenever this profile is `true`. */ + fun isolateTrue() = isolate { it.value } + + /** [(DOC)][splitTrue] Splits `true` segments into the given number of pieces (leaving `false` unchanged). */ + fun splitTrue(numPieces: Int) = split { if (it.value) numPieces else 1 } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt new file mode 100644 index 0000000000..1b5f56ecee --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt @@ -0,0 +1,106 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation + +/** + * Operations mixin for timelines of booleans. + */ +interface SerialBooleanOps>: SerialConstantOps, BooleanOps { + /** [(DOC)][and] Computes the AND operation between two boolean profiles. */ + infix fun > and(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + { l, _ -> if (l) null else false }, + { r, _ -> if (r) null else false }, + { l, r, _ -> l && r } + )) + + /** [(DOC)][or] Computes the OR operation between two boolean profiles. */ + infix fun > or(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + { l, _ -> if (l) true else null }, + { r, _ -> if (r) true else null }, + { l, r, _ -> l || r } + )) + + /** [(DOC)][xor] Computes the XOR operation between two boolean profiles. */ + infix fun > xor(other: SerialBooleanOps) = map2Values(other, BinaryOperation.combineOrNull { l, r, _ -> l.xor(r) }) + + /** [(DOC)][nor] the NOR operation between two boolean profiles. */ + infix fun > nor(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + { l, _ -> if (l) false else null }, + { r, _ -> if (r) false else null }, + { l, r, _ -> !(l || r) } + )) + + /** [(DOC)][nand] Computes the NAND operation between two boolean profiles. */ + infix fun > nand(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + { l, _ -> if (l) null else true }, + { r, _ -> if (r) null else true }, + { l, r, _ -> !(l && r) } + )) + + /** + * [(DOC)][shiftEdges] Shifts the rising and falling edges of a boolean profile independently of each other. + * + * This allows for segments to not just be shifted around, but stretched or squished, or even + * deleted. + * + * A rising edge is defined as the time just after a `false` segment ends - whether it meets a `true` + * segment or a gap. Similarly, a falling edge is just after a `true` segment ends. + * + * @param shiftRising duration to shift the rising edges by + * @param shiftFalling duration to shift the rising edges by + */ + fun shiftEdges(shiftRising: Duration, shiftFalling: Duration) = + unsafeMapIntervals( + { i -> + Interval.between( + Duration.min(i.start.saturatingMinus(shiftRising), i.start.saturatingMinus(shiftFalling)), + Duration.max(i.end.saturatingMinus(shiftRising), i.end.saturatingMinus(shiftFalling)), + i.startInclusivity, + i.endInclusivity + ) + }, + true + ) { t -> + if (t.value) t.interval.shiftBy(shiftRising, shiftFalling) + else t.interval.shiftBy(shiftFalling, shiftRising) + } + + /** + * [(DOC)][accumulatedTrueDuration] Creates a Real profile corresponding to the running total of time + * that this profile has spent `true`. + * + * @param unit base unit of time to count. As in, the resulting real profile will increase by + * `1` for each `unit` duration spent in the `true` state. + * + * @see gov.nasa.jpl.aerie.timeline.ops.numeric.SerialNumericOps.integrate for further explanation of [unit]. + */ + fun accumulatedTrueDuration(unit: Duration) = + mapValues(::Real) { LinearEquation(if (it.value) 1.0 else 0.0) }.integrate(unit) + + /** + * [(DOC)][rollingTrueDuration] Calculates the sum of durations of true segments in a range leading the current time. + * + * This returns a real profile that equals, at each time `t`, the duration of true segments in the interval `[t, t+range]`. + * + * Real profiles can't actually represent durations, only unitless numbers, so the result is actually calculated + * as a multiple of the provided [unit]. + * + * Because this is a serial profile, the duration of true segments in the look-ahead range can't exceed [range] itself. + * So the result is bounded by `[0, range/unit]` + * + * @param range how far into the future to look + * @param unit the time basis vector of the result; the unit of time that the result counts. + */ + fun rollingTrueDuration(range: Duration, unit: Duration) = + accumulatedTrueDuration(unit).shiftedDifference(range) + + /** [(DOC)][risingEdges] Detects when this transitions from false to true. */ + fun risingEdges() = transitions(from = false, to = true) + + /** [(DOC)][fallingEdges] Detects when this transitions from false to true. */ + fun fallingEdges() = transitions(from = true, to = false) +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanTest.kt new file mode 100644 index 0000000000..2dea3c77da --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanTest.kt @@ -0,0 +1,71 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.CollectOptions +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class SerialBooleanTest { + @Test + fun shiftEdgesBasic() { + val result = Booleans( + Segment(seconds(0) .. seconds(4), false), + Segment(seconds(4) .. seconds(8), true), + Segment(seconds(8) .. seconds(12), false) + ).shiftEdges(seconds(1), seconds(-1)).collect() + + assertIterableEquals( + listOf( + Segment(seconds(-1) ..< seconds(5), false), + Segment(seconds(5) ..< seconds(7), true), + Segment(seconds(7) .. seconds(13), false) + ), + result + ) + } + + @Test + fun shiftEdgesBoundsShift() { + val shiftRightResult = Booleans( + Segment(between(seconds(-2), seconds(0)), true), + ).shiftEdges(seconds(0), seconds(2)).collect(seconds(1) .. seconds(3)) + + assertIterableEquals( + listOf(Segment(seconds(1) .. seconds(2), true)), + shiftRightResult + ) + + val shiftLeftResult = Booleans( + Segment(seconds(2) .. seconds(4), true), + ).shiftEdges(seconds(-2), seconds(0)).collect(between(seconds(-2), seconds(1))) + + assertIterableEquals( + listOf(Segment(seconds(0) .. seconds(1), true)), + shiftLeftResult + ) + } + + @Test + fun shiftEdgesBoundsShiftNoTruncate() { + val shiftRightResult = Booleans( + Segment(between(seconds(-2), seconds(0)), true), + ).shiftEdges(seconds(0), seconds(2)).collect(CollectOptions(seconds(1) .. seconds(3), false)) + + assertIterableEquals( + listOf(Segment(between(seconds(-2), seconds(2)), true)), + shiftRightResult + ) + + val shiftLeftResult = Booleans( + Segment(seconds(2) .. seconds(4), true), + ).shiftEdges(seconds(-2), seconds(0)).collect(CollectOptions(between(seconds(-2), seconds(1)), false)) + + assertIterableEquals( + listOf(Segment(seconds(0) .. seconds(4), true)), + shiftLeftResult + ) + } +} From 1e25adf0754abea7a95878841b56cb74822375ac Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:56:43 -0800 Subject: [PATCH 106/159] General numeric profile interfaces --- .../aerie/timeline/ops/numeric/NumericOps.kt | 22 +++++++ .../timeline/ops/numeric/SerialNumericOps.kt | 60 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt new file mode 100644 index 0000000000..0716e4d1d4 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/NumericOps.kt @@ -0,0 +1,22 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.ops.SegmentOps +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation + +/** Operations for all timelines of segments that represent numbers. */ +interface NumericOps>: SegmentOps { + /** + * Used to convert individual segments of numeric profiles to linear equations. + * @suppress + */ + fun V.toLinear(): LinearEquation + + /** [(DOC)][abs] Calculates the absolute value of this profile. */ + fun abs(): THIS + + /** [(DOC)][negate] Negates this profile. */ + fun negate(): THIS + + /** @see negate */ + operator fun unaryMinus() = negate() +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt new file mode 100644 index 0000000000..8c2761b6c4 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt @@ -0,0 +1,60 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.ops.SerialOps +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation + + +/** + * Operations for profiles that represent numbers. + */ +interface SerialNumericOps>: SerialOps, NumericOps { + /** [(DOC)][toSerialLinear] Converts the profile to a linear profile, a.k.a. [Real] (no-op if it already was linear). */ + fun toSerialLinear(): Real + + /** + * [(DOC)][integrate] Calculates the integral of this profile, starting from zero. + * + * The result is scaled according to the [unit] argument. If a segment has a value of `1`, + * and `unit` is [Duration.SECOND], the integral will increase at `1` per second. + * But if (for the same segment) `unit` is [Duration.MINUTE], the integral will increase at `1` per minute + * (`1/60` per second). + * + * In fancy math terms, `unit` is the length of the time basis vector, and the result is + * contravariant with it. + * + * @param unit length of the time basis vector + */ + fun integrate(unit: Duration = Duration.SECOND) = + toSerialLinear().unsafeOperate { opts -> + val segments = collect(opts) + val result = mutableListOf>() + val baseRate = Duration.SECOND.ratioOver(unit) + var previousTime = opts.bounds.start + var acc = 0.0 + for (segment in segments) { + if (previousTime < segment.interval.start) + throw SerialLinearOps.SerialLinearOpException("Cannot integrate a linear profile that has gaps (time $previousTime") + if (!segment.value.isConstant()) + throw SerialLinearOps.SerialLinearOpException("Cannot integrate a non-piecewise-constant linear profile (time $previousTime") + val rate = segment.value.initialValue * baseRate + val nextAcc = acc + rate * segment.interval.duration().ratioOver(Duration.SECOND) + result.add(Segment(segment.interval, LinearEquation(previousTime, acc, rate))) + previousTime = segment.interval.end + acc = nextAcc + } + result + } + + /** + * [(DOC)][shiftedDifference] Calculates the difference between this, and this profile's value at [range] time in the future. + * + * If this is a function `f(t)`, the result is `f(t+range) - f(t)`. + */ + fun shiftedDifference(range: Duration): Real { + val linearized = toSerialLinear() + return linearized.shift(range.negate()).minus(linearized) + } +} From 8a5389b046cb822681ae155c168e96e935c56015 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:57:34 -0800 Subject: [PATCH 107/159] Real profiles --- .../timeline/collections/profiles/Real.kt | 51 ++++++ .../aerie/timeline/ops/numeric/LinearOps.kt | 39 ++++ .../timeline/ops/numeric/SerialLinearOps.kt | 170 ++++++++++++++++++ .../aerie/timeline/payloads/LinearEquation.kt | 166 +++++++++++++++++ .../aerie/timeline/util/LinearEquationTest.kt | 92 ++++++++++ 5 files changed, 518 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt new file mode 100644 index 0000000000..e903afdbab --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt @@ -0,0 +1,51 @@ +package gov.nasa.jpl.aerie.timeline.collections.profiles + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.ops.numeric.SerialLinearOps +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import gov.nasa.jpl.aerie.timeline.util.preprocessList +import kotlin.jvm.optionals.getOrNull + +/** A profile of [LinearEquations][LinearEquation]; a piece-wise linear real-number profile. */ +data class Real(private val timeline: Timeline, Real>): + Timeline, Real> by timeline, + SerialLinearOps, + CoalesceSegmentsOp +{ + constructor(v: Int): this(v.toDouble()) + constructor(v: Long): this(v.toDouble()) + constructor(v: Double): this(LinearEquation(v)) + constructor(eq: LinearEquation): this(Segment(Interval.MIN_MAX, eq)) + constructor(vararg segments: Segment): this(segments.asList()) + constructor(segments: List>): this(BaseTimeline(::Real, preprocessList(segments, Segment::valueEquals))) + + /***/ companion object { + /** + * Converts a list of serialized value segments into a real profile; for use with [gov.nasa.jpl.aerie.timeline.plan.Plan.resource]. + * + * Accepts either a map with the form `{initial: number, rate: number}`, or just a plain number for piecewise constant profiles. + * While plain numbers are acceptable and will be converted to a [LinearEquation] without warning, consider using [Numbers] + * to keep the precision. + */ + @JvmStatic fun deserialize(list: List>): Real { + val converted: List> = list.map { s -> + s.value.asReal().getOrNull()?.let { return@map s.withNewValue(LinearEquation(it)) } + val map = s.value.asMap().orElseThrow { RealDeserializeException("value was not a map or plain number: $s") } + val initialValue = (map["initial"] + ?: throw RealDeserializeException("initial value not found in map")) + .asReal().orElseThrow { RealDeserializeException("initial value was not a double") } + val rate = (map["rate"] ?: throw RealDeserializeException("rate not found in map")) + .asReal().orElseThrow { RealDeserializeException("rate was not a double") } + s.withNewValue(LinearEquation(s.interval.start, initialValue, rate)) + } + return Real(converted) + } + + /***/ class RealDeserializeException(message: String): Exception(message) + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt new file mode 100644 index 0000000000..a82b27ef8e --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt @@ -0,0 +1,39 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.BoundsTransformer +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation + +/** + * Operations mixin for segment-valued timelines whose payloads + * represent continuous, piecewise linear values. + * + * Currently only used for Real profiles, but in the future could be refactored for + * duration profiles or parallel real profiles. + */ +interface LinearOps>: NumericOps { + override fun negate() = mapValues { LinearEquation(it.value.initialTime, -it.value.initialValue, -it.value.rate) } + + override fun abs() = flatMapValues { it.value.abs() } + + /** + * [(DOC)][rate] Maps each segment into its derivative. + * + * The result is scaled according to the [unit] argument. If a segment increases at a rate + * of `1` per second, and `unit` is [Duration.SECOND], the derivative will be `1`. + * But if (for the same segment) `unit` is [Duration.MINUTE], the derivative will be `60`. + * + * In fancy math terms, `unit` is the length of the time basis vector, and the result is + * covariant with it. + * + * @param unit length of the time basis vector + */ + fun rate(unit: Duration = Duration.SECOND) = + if (unit == Duration.SECOND) mapValues { LinearEquation(it.value.rate) } + else mapValues { LinearEquation(it.value.rate / (Duration.SECOND ratioOver unit)) } + + override fun shift(dur: Duration) = unsafeMap(BoundsTransformer.shift(dur), false) { v -> + Segment(v.interval.shiftBy(dur), LinearEquation(v.value.initialTime.plus(dur), v.value.initialValue, v.value.rate)) + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt new file mode 100644 index 0000000000..63aa0facec --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt @@ -0,0 +1,170 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.transpose +import gov.nasa.jpl.aerie.timeline.util.truncateList +import kotlin.math.pow + +/** + * Operations mixin for segment-valued profiles whose payloads + * represent continuous, piecewise linear values. + * + * Currently only used for Real profiles, but in the future could be refactored for + * duration profiles or parallel real profiles. + */ +interface SerialLinearOps>: SerialNumericOps, LinearOps { + override fun toSerialLinear() = unsafeCast(::Real) + override fun LinearEquation.toLinear() = this + + /** + * Converts this to a primitive number profile (i.e. [Numbers]), throwing an error if this is not piece-wise constant. + * + * @param message error message to throw if this is not piece-wise constant. + */ + fun toSerialPrimitiveNumbers(message: String? = null) = mapValues(::Numbers) { + if (it.value.isConstant()) it.value.initialValue + else if (message == null) throw SerialLinearOpException("Cannot convert a non-piecewise-constant linear equation to a constant number. (at time ${it.interval.start})") + else throw SerialLinearOpException("$message (at time ${it.interval.start})") + } + + /** [(DOC)][plus] Adds this and another numeric profile. */ + operator fun > plus(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, _ -> + val shiftedRight = r.shiftInitialTime(l.initialTime) + LinearEquation(l.initialTime, l.initialValue + shiftedRight.initialValue, l.rate + r.rate) + }) + /** [(DOC)][plus] Adds a constant number to this. */ + operator fun plus(n: Number) = plus(Numbers(n)) + + /** [(DOC)][minus] Subtracts another numeric profile from this. */ + operator fun > minus(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, _ -> + val shiftedRight = r.shiftInitialTime(l.initialTime) + LinearEquation(l.initialTime, l.initialValue - shiftedRight.initialValue, l.rate - r.rate) + }) + /** [(DOC)][minus] Subtracts a constant number from this. */ + operator fun minus(n: Number) = minus(Numbers(n)) + + /** + * [(DOC)][times] Multiplies this and another numeric profile. + * + * @throws SerialLinearOpException if both profiles have non-zero rate at the same time. + */ + operator fun > times(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, i -> + if (!l.isConstant() && !r.isConstant()) throw SerialLinearOpException("Cannot multiply two linear equations that are non-constant at the same time (at time ${i.start})") + val shiftedRight = r.shiftInitialTime(l.initialTime) + val newRate = l.rate * shiftedRight.initialValue + r.rate * l.initialValue + LinearEquation(l.initialTime, l.initialValue * shiftedRight.initialValue, newRate) + }) + /** [(DOC)][times] Multiplies this by a constant number. */ + operator fun times(n: Number) = times(Numbers(n)) + + /** + * [(DOC)][div] Calculates this divided by another numeric profile. + * + * @throws SerialLinearOpException if the divisor has a non-zero rate at any time that the dividend is defined. + */ + operator fun > div(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, i -> + if (!r.isConstant()) throw SerialLinearOpException("Cannot divide by a non-piecewise-constant linear equation (at time ${i.start})") + LinearEquation(l.initialTime, l.initialValue / r.initialValue, l.rate / r.initialValue) + }) + /** [(DOC)][div] Calculates this divided by a contant number. */ + operator fun div(n: Number) = div(Numbers(n)) + + /** + * [(DOC)][pow] Calculates this raised to the power of another numeric profile. + * + * @throws SerialLinearOpException if the exponent has a non-zero rate at any time that the base is defined, + * or if the base has a non-zero rate at any time that the exponent is defined and not + * either 0 or 1. + */ + infix fun > pow(exp: SerialNumericOps) = map2Values(exp.toSerialLinear(), BinaryOperation.combineOrNull { l, r, i -> + if (!r.isConstant()) throw SerialLinearOpException("Cannot apply a non-piecewise-constant exponent (at time ${i.start}") + if (r.initialValue == 0.0) LinearEquation(1.0) + else if (r.initialValue == 1.0) l + else if (!l.isConstant()) throw SerialLinearOpException("Cannot apply an exponent to a non-piecewise-constant profile") + else LinearEquation(l.initialValue.pow(r.initialValue)) + }) + /** [(DOC)][pow] Calculates this raised to the power of a constant number. */ + infix fun pow(n: Number) = pow(Numbers(n)) + + /** [(DOC)][equalTo] Returns a [Booleans] that is true when this and another numeric profile are equal. */ + infix fun > equalTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsEqualTo) + /** [(DOC)][equalTo] Returns a [Booleans] that is true when this equals a constant number. */ + infix fun equalTo(n: Number) = equalTo(Numbers(n)) + + /** [(DOC)][notEqualTo] Returns a [Booleans] that is true when this and another numeric profile are not equal. */ + infix fun > notEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsNotEqualTo) + /** [(DOC)][notEqualTo] Returns a [Booleans] that is true when this does not equal a constant number. */ + infix fun notEqualTo(n: Number) = notEqualTo(Numbers(n)) + + /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than another numeric profile. */ + infix fun > lessThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThan) + /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than a constant number. */ + infix fun lessThan(n: Number) = lessThan(Numbers(n)) + + /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to another numeric profile. */ + infix fun > lessThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThanOrEqualTo) + /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to a constant number. */ + infix fun lessThanOrEqualTo(n: Number) = lessThanOrEqualTo(Numbers(n)) + + /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than another numeric profile. */ + infix fun > greaterThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThan) + /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a constant number. */ + infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) + + /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to another numeric profile. */ + infix fun > greaterThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThanOrEqualTo) + /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ + infix fun greaterThanOrEqualTo(n: Number) = greaterThanOrEqualTo(Numbers(n)) + + private fun > inequalityHelper(other: SerialNumericOps, f: LinearEquation.(LinearEquation) -> Booleans) = + flatMap2Values(::Booleans, other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, _ -> l.f(r) }) + + + override fun changes() = + unsafeOperate(::Booleans) { opts -> + val bounds = opts.bounds + var previous: Segment? = null + val result = collect(CollectOptions(bounds, false)).flatMap { currentSegment: Segment -> + val currentInterval = currentSegment.interval + val leftEdge = if ( + previous !== null && + previous!!.interval.compareEndToStart(currentInterval) == 0 && + currentInterval.includesStart() + ) { + previous!!.value.valueAt(currentInterval.start) == currentSegment.value.valueAt(currentInterval.start) + } else if (currentInterval.compareStarts(bounds) == 0) { + currentSegment.value.rate != 0.0 + } else { + null + } + previous = currentSegment + listOfNotNull( + Segment(Interval.at(currentInterval.start), leftEdge).transpose(), + Segment(Interval.between(currentInterval.start, currentInterval.end, Exclusive), currentSegment.value.rate != 0.0) + ) + } + truncateList(result, opts) + } + + /** + * [(DOC)][transitions] Returns a [Booleans] that is true whenever this discontinuously transitions between + * a specific pair of values, and false or gap everywhere else. + */ + fun transitions(from: Double, to: Double) = detectEdges(BinaryOperation.cases( + { l, i -> if (l.valueAt(i.start) == from) null else false }, + { r, i -> if (r.valueAt(i.start) == to) null else false }, + { l, r, i -> l.valueAt(i.start) == from && r.valueAt(i.start) == to } + )) + + /** + * An exception for linear profile operations; usually thrown in contexts that + * require one or more of the operands to be piecewise constant. + */ + class SerialLinearOpException(message: String): Exception(message) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt new file mode 100644 index 0000000000..384c2a0c9e --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt @@ -0,0 +1,166 @@ +package gov.nasa.jpl.aerie.timeline.payloads + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Inclusive +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import java.util.function.BiFunction +import kotlin.math.abs +import kotlin.math.absoluteValue + +/** A linear equation in point-slope form. */ +data class LinearEquation( + /** The time of the start point. */ + val initialTime: Duration, + /** The value of the start point. */ + val initialValue: Double, + /** The rate of change, in units per second. */ + val rate: Double +) { + /** Creates a constant linear equation at a given value. */ + constructor(constant: Double): this(Duration.ZERO, constant, 0.0) + + /** Calculates the value at a given time. */ + fun valueAt(time: Duration): Double { + val change = rate * time.ratioOver(Duration.SECOND) - rate * initialTime.ratioOver(Duration.SECOND) + return initialValue + change + } + + /** Returns an equivalent equation that is represented at a different start time. */ + fun shiftInitialTime(newInitialTime: Duration) = LinearEquation( + newInitialTime, + initialValue + newInitialTime.minus(initialTime).ratioOver(Duration.SECOND) * rate, + rate + ) + + /***/ + fun isConstant() = rate == 0.0 + + private fun intersectionPointWith(other: LinearEquation): Duration? { + if (rate == other.rate) return null + + /* + Floating point noise can cause rates to be extremely near zero, when they should + have been exactly zero. For example: `0.1 + 0.2 - 0.1 - 0.2 != 0`. + + This can cause the denominator below to be tiny, leading to long overflow later. + */ + + // If the following causes an exception, something really has gone wrong, and we don't want to catch it. + val numSeconds = (other.valueAt(initialTime) - initialValue) / (rate - other.rate) + + // Check if numSeconds is too big before putting it in a long. + return if (abs(numSeconds) > Long.MAX_VALUE.toDouble() / (Duration.SECOND / Duration.MICROSECOND)) null + else initialTime.plus(Duration.roundNearest(numSeconds, Duration.SECOND)) + } + + /** Calculates when this is less than another linear equation, as a [Booleans] object. */ + infix fun intervalsLessThan(other: LinearEquation): Booleans { + return getInequalityIntervals(other) { l: Double, r: Double -> l < r } + } + + /** Calculates when this is less than or equal to another linear equation, as a [Booleans] object. */ + infix fun intervalsLessThanOrEqualTo(other: LinearEquation): Booleans { + return getInequalityIntervals(other) { l: Double, r: Double -> l <= r } + } + + /** Calculates when this is greater than another linear equation, as a [Booleans] object. */ + infix fun intervalsGreaterThan(other: LinearEquation): Booleans { + return getInequalityIntervals(other) { l: Double, r: Double -> l > r } + } + + /** Calculates when this is greater than or equal to another linear equation, as a [Booleans] object. */ + infix fun intervalsGreaterThanOrEqualTo(other: LinearEquation): Booleans { + return getInequalityIntervals(other) { l: Double, r: Double -> l >= r } + } + + private fun getInequalityIntervals( + other: LinearEquation, + op: BiFunction + ): Booleans { + val intersection = intersectionPointWith(other) + return if (intersection === null) { + val resultEverywhere = op.apply(initialValue, other.valueAt(initialTime)) + Booleans(resultEverywhere) + } else { + val oneSecondBefore = intersection.minus(Duration.SECOND) + val oneSecondAfter = intersection.plus(Duration.SECOND) + Booleans( + Segment( + Interval.betweenClosedOpen(Duration.MIN_VALUE, intersection), + op.apply(this.valueAt(oneSecondBefore), other.valueAt(oneSecondBefore)) + ), + Segment( + Interval.at(intersection), + op.apply(this.valueAt(intersection), other.valueAt(intersection)) + ), + Segment( + Interval.between(intersection, Duration.MAX_VALUE, Exclusive, Inclusive), + op.apply(this.valueAt(oneSecondAfter), other.valueAt(oneSecondAfter)) + ) + ) + } + } + + /** Calculates when this is equal to another linear equation, as a [Booleans] object. */ + fun intervalsEqualTo(other: LinearEquation): Booleans { + val intersection = intersectionPointWith(other) + return if (intersection === null) { + Booleans(initialValue == other.valueAt(initialTime)) + } else { + Booleans( + Segment(Interval.betweenClosedOpen(Duration.MIN_VALUE, intersection), false), + Segment(Interval.at(intersection), true), + Segment(Interval.between(intersection, Duration.MAX_VALUE, Exclusive, Inclusive), false) + ) + } + } + + /** Calculates when this is not equal to another linear equation, as a [Booleans] object. */ + fun intervalsNotEqualTo(other: LinearEquation): Booleans { + return !intervalsEqualTo(other) + } + + /** Finds the time that this equation is zero, or `null` if it does not cross the axis. */ + fun findRoot() = if (rate == 0.0) null else initialTime - Duration.roundNearest(initialValue / rate, Duration.SECOND) + + /** Calculates the absolute value of this equation, as a real profile. */ + fun abs() = + if (isConstant()) Real(LinearEquation(initialTime, initialValue.absoluteValue, 0.0)) + else { + val root = findRoot()!! + Real( + Segment(Interval.betweenClosedOpen(Duration.MIN_VALUE, root), LinearEquation(root, 0.0, -rate.absoluteValue)), + Segment(Interval.between(root, Duration.MAX_VALUE), LinearEquation(root, 0.0, rate.absoluteValue)) + ) + } + + /***/ + override fun toString(): String { + return String.format( + """{ + Initial Time: %s + Initial Value: %s + Rate: %s +}""", + initialTime, + initialValue, + rate + ) + } + + /***/ + override fun equals(other: Any?) = + if (other !is LinearEquation) false + else initialValue == other.valueAt(initialTime) && rate == other.rate + + /***/ + override fun hashCode(): Int { + var result = initialTime.hashCode() + result = 31 * result + initialValue.hashCode() + result = 31 * result + rate.hashCode() + return result + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt new file mode 100644 index 0000000000..72c5c55a5b --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/LinearEquationTest.kt @@ -0,0 +1,92 @@ +package gov.nasa.jpl.aerie.timeline.util + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Companion.betweenClosedOpen +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +class LinearEquationTest { + + @Test + fun shiftInitialTime() { + val original = LinearEquation(Duration.ZERO, 1.0, 1.0) + + val shifted = original.shiftInitialTime(Duration.SECOND) + + assertEquals( + LinearEquation(Duration.SECOND, 2.0, 1.0), + shifted + ) + } + + @Test + fun intervalsLessThan() { + assertIterableEquals( + Booleans(true).collect(), + LinearEquation(1.0).intervalsLessThan(LinearEquation(2.0)).collect() + ) + + assertIterableEquals( + Booleans(false).collect(), + LinearEquation(2.0).intervalsLessThan(LinearEquation(1.0)).collect() + ) + + assertIterableEquals( + Booleans(Segment(between(Duration.ZERO, Duration.MAX_VALUE), false)).assignGaps(Booleans(true)).collect(), + LinearEquation(Duration.ZERO, 0.0, 1.0).intervalsLessThan(LinearEquation(0.0)).collect() + ) + } + + @Test + fun findRoot() { + assertNull(LinearEquation(1.0).findRoot()) + + assertEquals( + seconds(2), + LinearEquation(Duration.ZERO, 2.0, -1.0).findRoot() + ) + } + + @Test + fun abs() { + assertIterableEquals( + Real(5.0).collect(), + LinearEquation(5.0).abs().collect() + ) + + assertIterableEquals( + Real(5.0).collect(), + LinearEquation(-5.0).abs().collect() + ) + + assertIterableEquals( + Real( + Segment(betweenClosedOpen(Duration.MIN_VALUE, Duration.SECOND), LinearEquation(Duration.SECOND, 0.0, -2.0)), + Segment(between(Duration.SECOND, Duration.MAX_VALUE), LinearEquation(Duration.SECOND, 0.0, 2.0)) + ).collect(), + LinearEquation(Duration.ZERO, 2.0, -2.0).abs().collect() + ) + } + + @Test + fun equals() { + assertTrue( + LinearEquation(5.0) == LinearEquation(Duration.SECOND, 5.0, 0.0) + ) + + assertTrue( + LinearEquation(Duration.ZERO, 0.0, 2.0) == LinearEquation(Duration.SECOND, 2.0, 2.0) + ) + + assertFalse( + LinearEquation(Duration.ZERO, 0.5, 2.0) == LinearEquation(Duration.SECOND, 2.0, 2.0) + ) + } +} From bcb25fc0d0f4ad74c6efb9b944fb4d62909972b2 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:57:51 -0800 Subject: [PATCH 108/159] Primitive numbers profiles --- .../timeline/collections/profiles/Numbers.kt | 52 ++++++ .../ops/numeric/PrimitiveNumberOps.kt | 55 ++++++ .../ops/numeric/SerialPrimitiveNumberOps.kt | 163 ++++++++++++++++++ .../collections/profiles/NumbersTest.kt | 26 +++ .../ops/numeric/PrimitiveNumberOpsTest.kt | 52 ++++++ 5 files changed, 348 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt new file mode 100644 index 0000000000..90857c7e0f --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt @@ -0,0 +1,52 @@ +package gov.nasa.jpl.aerie.timeline.collections.profiles + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.ops.numeric.SerialPrimitiveNumberOps +import gov.nasa.jpl.aerie.timeline.util.preprocessList +import java.lang.ArithmeticException + +/** + * A profile of piece-wise constant numbers. + * + * Unlike [Real], this is not able to vary linearly. Instead, + * it can contain either homogeneous (and strictly-typed) collection of + * any numeric type (i.e. `Numbers` (Java) or `Numbers` (Kotlin)), + * or a heterogeneous collection of all numeric types (i.e. `Numbers`). + * + * Unfortunately Kotlin/Java is not smart enough to keep the type information during binary operations. + * So the sum of two `Numbers` objects will become `Numbers`, although no precision + * will be lost. + * + * @see Number + */ +data class Numbers(private val timeline: Timeline, Numbers>): + Timeline, Numbers> by timeline, + CoalesceSegmentsOp>, + SerialPrimitiveNumberOps> { + constructor(v: N): this(Segment(Interval.MIN_MAX, v)) + constructor(vararg segments: Segment): this(segments.asList()) + constructor(segments: List>): this(BaseTimeline(::Numbers, preprocessList(segments, Segment::valueEquals))) + + /***/ companion object { + /** + * Converts a list of serialized value segments into a [Numbers] profile; + * for use with [gov.nasa.jpl.aerie.timeline.plan.Plan.resource]. + * + * Prefers converting to longs if possible, and falls back to doubles if not. + */ + @JvmStatic fun deserialize(list: List>) = Numbers(list.map { seg -> + val bigDecimal = seg.value.asNumeric().orElseThrow { Exception("value was not numeric: $seg") } + val number: Number = try { + bigDecimal.longValueExact() + } catch (e: ArithmeticException) { + bigDecimal.toDouble() + } + seg.withNewValue(number) + }) + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt new file mode 100644 index 0000000000..90ea537773 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOps.kt @@ -0,0 +1,55 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import gov.nasa.jpl.aerie.timeline.ops.ConstantOps +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import kotlin.math.absoluteValue + +/** Operations for timelines of primitive numbers. */ +interface PrimitiveNumberOps>: NumericOps, ConstantOps { + override fun N.toLinear() = LinearEquation(this.toDouble()) + + /** [(DOC)][toDoubles] Converts all numbers in the profile to doubles. */ + fun toDoubles() = mapValues(::Numbers) { it.value.toDouble() } + /** [(DOC)][toFloats] Converts all numbers in the profile to floats. */ + fun toFloats() = mapValues(::Numbers) { it.value.toFloat() } + /** [(DOC)][toLongs] Converts all numbers in the profile to longs. */ + fun toLongs() = mapValues(::Numbers) { it.value.toLong() } + /** [(DOC)][toInts] Converts all numbers in the profile to ints. */ + fun toInts() = mapValues(::Numbers) { it.value.toInt() } + /** [(DOC)][toShorts] Converts all numbers in the profile to shorts. */ + fun toShorts() = mapValues(::Numbers) { it.value.toShort() } + /** [(DOC)][toBytes] Converts all numbers in the profile to bytes. */ + fun toBytes() = mapValues(::Numbers) { it.value.toByte() } + + @Suppress("UNCHECKED_CAST") + override fun negate() = mapValues { + when(it.value) { + is Double -> -it.value as N + is Float -> -it.value as N + is Long -> -it.value as N + is Int -> -it.value as N + is Short -> -it.value as N + is Byte -> -it.value as N + else -> throw UnreachablePrimitiveNumberException() + } + } + + @Suppress("UNCHECKED_CAST") + override fun abs() = mapValues { + when(it.value) { + is Double -> it.value.absoluteValue as N + is Float -> it.value.absoluteValue as N + is Long -> it.value.absoluteValue as N + is Int -> it.value.absoluteValue as N + + // .absoluteValue does not exist for shorts and bytes for some reason + is Short -> (if (it.value < 0) -it.value else it.value) as N + is Byte -> (if (it.value < 0) -it.value else it.value) as N + else -> throw UnreachablePrimitiveNumberException() + } + } + + /** @suppress */ + class UnreachablePrimitiveNumberException: Exception("internal error. not all numeric types were accounted for") +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt new file mode 100644 index 0000000000..3b6cf0a82e --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt @@ -0,0 +1,163 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.ops.SerialConstantOps +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import kotlin.math.pow + +/** Operations for profiles of primitive numbers. */ +interface SerialPrimitiveNumberOps>: SerialNumericOps, PrimitiveNumberOps, SerialConstantOps { + override fun toSerialLinear() = mapValues(::Real) { LinearEquation(it.value.toDouble()) } + + + /* + Due to the fact there is no superinterface for numbers that includes any arithmetic + or comparison operators, AFAICT this giant if-else statement needs to be copy-pasted + for each operation. If you can find a better way, please do. + */ + + /** [(DOC)][plus] Adds this and another primitive numeric profile. */ + operator fun > plus(other: SerialPrimitiveNumberOps) = + map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() + r.toDouble() + else if (l is Float || r is Float) l.toFloat() + r.toFloat() + else if (l is Long || r is Long) l.toLong() + r.toLong() + else if (l is Int || r is Int) l.toInt() + r.toInt() + else if (l is Short || r is Short) l.toShort() + r.toShort() + else if (l is Byte || r is Byte) l.toByte() + r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][plus] Adds this a constant number. */ + operator fun plus(n: Number) = plus(Numbers(n)) + /** [(DOC)][plus] Adds this and a linear profile. */ + operator fun > plus(other: SerialLinearOps) = other + this + + /** [(DOC)][minus] Subtracts another primitive numeric profile from this. */ + operator fun > minus(other: SerialPrimitiveNumberOps) = + map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() - r.toDouble() + else if (l is Float || r is Float) l.toFloat() - r.toFloat() + else if (l is Long || r is Long) l.toLong() - r.toLong() + else if (l is Int || r is Int) l.toInt() - r.toInt() + else if (l is Short || r is Short) l.toShort() - r.toShort() + else if (l is Byte || r is Byte) l.toByte() - r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][minus] Subtracts a constant number from this. */ + operator fun minus(n: Number) = minus(Numbers(n)) + /** [(DOC)][minus] Subtracts a linear profile from this. */ + operator fun > minus(other: SerialLinearOps) = -other + this + + /** [(DOC)][times] Multiplies this and another primitive numeric profile. */ + operator fun > times(other: SerialPrimitiveNumberOps) = + map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() * r.toDouble() + else if (l is Float || r is Float) l.toFloat() * r.toFloat() + else if (l is Long || r is Long) l.toLong() * r.toLong() + else if (l is Int || r is Int) l.toInt() * r.toInt() + else if (l is Short || r is Short) l.toShort() * r.toShort() + else if (l is Byte || r is Byte) l.toByte() * r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][times] Multiplies this by a constant number. */ + operator fun times(n: Number) = times(Numbers(n)) + /** [(DOC)][times] Multiplies this by a linear profile. */ + operator fun > times(other: SerialLinearOps) = other * this + + /** [(DOC)][div] Calculates this divided by another primitive numeric profile. */ + operator fun > div(other: SerialPrimitiveNumberOps) = + map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() / r.toDouble() + else if (l is Float || r is Float) l.toFloat() / r.toFloat() + else if (l is Long || r is Long) l.toLong() / r.toLong() + else if (l is Int || r is Int) l.toInt() / r.toInt() + else if (l is Short || r is Short) l.toShort() / r.toShort() + else if (l is Byte || r is Byte) l.toByte() / r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][div] Divides this by a constant number. */ + operator fun div(n: Number) = div(Numbers(n)) + /** [(DOC)][div] Divides this by a linear profile. */ + operator fun > div(other: SerialLinearOps) = this / other.toSerialPrimitiveNumbers("Cannot divide by a non-piecewise-constant divisor.") + + /** + * [(DOC)][pow] Calculates this raised to the power of another primitive numeric profile. + * + * Both profiles are converted to doubles first. + */ + infix fun > pow(exp: SerialPrimitiveNumberOps) = + map2Values(::Numbers, exp, BinaryOperation.combineOrNull { l, r, _ -> + l.toDouble().pow(r.toDouble()) + }) + /** [(DOC)][pow] Raises this to the power of a constant number. */ + infix fun pow(n: Number) = pow(Numbers(n)) + /** [(DOC)][pow] Raises this to the power of a linear profile. */ + infix fun > pow(other: SerialLinearOps) = this pow other.toSerialPrimitiveNumbers("Cannot apply a non-piecewise-constant exponent.") + + /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than another primitive numeric profile. */ + infix fun > lessThan(other: SerialPrimitiveNumberOps) = + map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() < r.toDouble() + else if (l is Float || r is Float) l.toFloat() < r.toFloat() + else if (l is Long || r is Long) l.toLong() < r.toLong() + else if (l is Int || r is Int) l.toInt() < r.toInt() + else if (l is Short || r is Short) l.toShort() < r.toShort() + else if (l is Byte || r is Byte) l.toByte() < r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than a constant number. */ + infix fun lessThan(n: Number) = lessThan(Numbers(n)) + /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than a linear profile. */ + infix fun > lessThan(other: SerialLinearOps) = other greaterThan this + + /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to another primitive numeric profile. */ + infix fun > lessThanOrEqualTo(other: SerialPrimitiveNumberOps) = + map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() <= r.toDouble() + else if (l is Float || r is Float) l.toFloat() <= r.toFloat() + else if (l is Long || r is Long) l.toLong() <= r.toLong() + else if (l is Int || r is Int) l.toInt() <= r.toInt() + else if (l is Short || r is Short) l.toShort() <= r.toShort() + else if (l is Byte || r is Byte) l.toByte() <= r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to a constant number. */ + infix fun lessThanOrEqual(n: Number) = lessThanOrEqualTo(Numbers(n)) + /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to a linear profile. */ + infix fun > lessThanOrEqualTo(other: SerialLinearOps) = other greaterThanOrEqualTo this + + /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than another primitive numeric profile. */ + infix fun > greaterThan(other: SerialPrimitiveNumberOps) = + map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() > r.toDouble() + else if (l is Float || r is Float) l.toFloat() > r.toFloat() + else if (l is Long || r is Long) l.toLong() > r.toLong() + else if (l is Int || r is Int) l.toInt() > r.toInt() + else if (l is Short || r is Short) l.toShort() > r.toShort() + else if (l is Byte || r is Byte) l.toByte() > r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a constant number. */ + infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) + /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a linear profile. */ + infix fun > greaterThan(other: SerialLinearOps) = other lessThan this + + /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to another primitive numeric profile. */ + infix fun > greaterThanOrEqualTo(other: SerialPrimitiveNumberOps) = + map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + if (l is Double || r is Double) l.toDouble() >= r.toDouble() + else if (l is Float || r is Float) l.toFloat() >= r.toFloat() + else if (l is Long || r is Long) l.toLong() >= r.toLong() + else if (l is Int || r is Int) l.toInt() >= r.toInt() + else if (l is Short || r is Short) l.toShort() >= r.toShort() + else if (l is Byte || r is Byte) l.toByte() >= r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + }) + /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ + infix fun greaterThanOrEqual(n: Number) = greaterThanOrEqualTo(Numbers(n)) + /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a linear profile. */ + infix fun > greaterThanOrEqualTo(other: SerialLinearOps) = other lessThanOrEqualTo this +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt new file mode 100644 index 0000000000..b799d0c996 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/NumbersTest.kt @@ -0,0 +1,26 @@ +package gov.nasa.jpl.aerie.timeline.collections.profiles + +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +class NumbersTest { + + @Test + fun plus() { + val five = Numbers(Segment(between(Duration.ZERO, seconds(1)), 5)).assignGaps(Numbers(0)) + + assertIterableEquals( + listOf( + Segment(between(Duration.ZERO, seconds(1)), 9), + Segment(between(seconds(1), seconds(2), Interval.Inclusivity.Exclusive, Interval.Inclusivity.Inclusive), 4) + ), + (five + 4).collect(between(Duration.ZERO, seconds(2))) + ) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt new file mode 100644 index 0000000000..75d6e140eb --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/PrimitiveNumberOpsTest.kt @@ -0,0 +1,52 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class PrimitiveNumberOpsTest { + @Test + fun negatePreservesType() { + val justInts: List> = (-Numbers( + Segment(at(seconds(0)), 5), + Segment(at(seconds(1)), -2), + )).collect() + + assertIterableEquals( + listOf( + Segment(at(seconds(0)), -5), + Segment(at(seconds(1)), 2), + ), + justInts + ) + + val justDoubles: List> = Numbers( + Segment(at(seconds(0)), 5.0), + Segment(at(seconds(1)), -2.0), + ).negate().collect() + + assertIterableEquals( + listOf( + Segment(at(seconds(0)), -5.0), + Segment(at(seconds(1)), 2.0), + ), + justDoubles + ) + + val both: List> = Numbers( + Segment(at(seconds(0)), 5), + Segment(at(seconds(1)), -2.0), + ).negate().collect() + + assertIterableEquals( + listOf( + Segment(at(seconds(0)), -5), + Segment(at(seconds(1)), 2.0), + ), + both + ) + } +} From e675090d6dc6605ae4c2a38582371e1bc4cee6d8 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 12:59:26 -0800 Subject: [PATCH 109/159] Implement database queries for plans and sim results --- .../aerie/timeline/plan/AeriePostgresPlan.kt | 221 ++++++++++++++++++ .../gov/nasa/jpl/aerie/timeline/plan/Plan.kt | 39 ++++ 2 files changed, 260 insertions(+) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt new file mode 100644 index 0000000000..d5467bebcb --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt @@ -0,0 +1,221 @@ +package gov.nasa.jpl.aerie.timeline.plan + +import gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Duration.Companion.minus +import gov.nasa.jpl.aerie.timeline.Duration.Companion.plus +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.* +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.activities.AnyDirective +import gov.nasa.jpl.aerie.timeline.payloads.activities.AnyInstance +import gov.nasa.jpl.aerie.timeline.payloads.activities.Directive +import gov.nasa.jpl.aerie.timeline.payloads.activities.Instance +import gov.nasa.jpl.aerie.timeline.collections.Directives +import gov.nasa.jpl.aerie.timeline.collections.Instances +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import java.io.StringReader +import java.sql.Connection +import java.sql.PreparedStatement +import java.time.Instant +import javax.json.Json +import kotlin.jvm.optionals.getOrNull + +/** A connection to Aerie's database for a particular simulation result. */ +data class AeriePostgresPlan( + /** A connection to Aerie's database. */ + val c: Connection, + /** The particular simulation dataset to query. */ + val simDatasetId: Int +): Plan { + + private val datasetId by lazy { + val statement = c.prepareStatement("select dataset_id from simulation_dataset where id = ?;") + statement.setInt(1, simDatasetId) + getSingleIntQueryResult(statement) + } + + private val simulationId by lazy { + val statement = c.prepareStatement("select simulation_id from simulation_dataset where id = ?;") + statement.setInt(1, simDatasetId) + getSingleIntQueryResult(statement) + } + + private val simulationInfo by lazy { + val statement = c.prepareStatement("select plan_id, simulation_start_time, simulation_end_time from simulation where id = ?;") + statement.setInt(1, simulationId) + val response = statement.executeQuery() + if (!response.next()) throw DatabaseError("Expected exactly one result for query, found none: $statement") + val result = object { + val planId = response.getInt(1) + val startTime = response.getTimestamp(2).toInstant() + val endTime = response.getTimestamp(3).toInstant() + } + if (response.next()) throw DatabaseError("Expected exactly one result for query, found more than one: $statement") + result + } + + private fun getSingleIntQueryResult(statement: PreparedStatement): Int { + val result = statement.executeQuery() + if (!result.next()) throw DatabaseError("Expected exactly one result for query, found none: $statement") + val int = result.getInt(1) + if (result.next()) throw DatabaseError("Expected exactly one result for query, found more than one: $statement") + return int + } + + private val planInfo by lazy { + val statement = c.prepareStatement("select start_time, duration from plan where id = ?;") + statement.setInt(1, simulationInfo.planId) + intervalStyleStatement.execute() + val response = statement.executeQuery() + if (!response.next()) throw DatabaseError("Expected exactly one result for query, found none: $statement") + val result = object { + val startTime = response.getTimestamp(1).toInstant() + val duration = Duration.parseISO8601(response.getString(2)) + val id = simulationInfo.planId + } + if (response.next()) throw DatabaseError("Expected exactly one result for query, found more than one: $statement") + result + } + + override fun totalBounds() = between(Duration.ZERO, planInfo.duration) + override fun simBounds() = between( + toRelative(simulationInfo.startTime), + toRelative(simulationInfo.endTime), + ) + + override fun toRelative(abs: Instant) = abs - planInfo.startTime + override fun toAbsolute(rel: Duration) = planInfo.startTime + rel + + private val intervalStyleStatement = c.prepareStatement("set intervalstyle = 'iso_8601';") + private val profileInfoStatement = c.prepareStatement( + "select id, duration from profile where dataset_id = ? and name = ?;" + ) + private data class ProfileInfo(val id: Int, val duration: Duration) + + private val segmentsStatement = c.prepareStatement( + "select start_offset, dynamics, is_gap from profile_segment where profile_id = ? and dataset_id = ? order by start_offset asc;" + ) + + /***/ class DatabaseError(message: String): Error(message) + + override fun > resource(name: String, ctor: (List>) -> TL): TL { + val profileInfo = getProfileInfo(name) + + segmentsStatement.clearParameters() + segmentsStatement.setInt(1, profileInfo.id) + segmentsStatement.setInt(2, datasetId) + intervalStyleStatement.execute() + val response = segmentsStatement.executeQuery() + + val result = mutableListOf>() + + var previousValue: SerializedValue? = null + var previousStart: Duration? = null + + while (response.next()) { + val thisStart = Duration.parseISO8601(response.getString(1)) + if (previousStart !== null) { + val interval = between(previousStart, thisStart, Inclusive, Exclusive) + val newSegment = Segment( + interval, + previousValue!! + ) + result.add(newSegment) + } + if (!response.getBoolean(3)) { // if not gap + val serializedValue = parseJson(response.getString(2)) + previousValue = serializedValue + previousStart = thisStart + } else { + previousValue = null + previousStart = null + } + } + if (previousStart !== null) { + val interval = between(previousStart, profileInfo.duration, Inclusive, Exclusive) + result.add( + Segment( + interval, + previousValue!! + ) + ) + } + return ctor(result) + } + + private fun getProfileInfo(name: String): ProfileInfo { + profileInfoStatement.clearParameters() + profileInfoStatement.setInt(1, datasetId) + profileInfoStatement.setString(2, name) + intervalStyleStatement.execute() + val profileResult = profileInfoStatement.executeQuery() + if (!profileResult.next()) throw DatabaseError("Profile $name not found in database") + val id = profileResult.getInt(1) + val duration = Duration.parseISO8601(profileResult.getString(2)) + if (profileResult.next()) throw DatabaseError("Multiple profiles named $name found in one simulation dataset") + return ProfileInfo(id, duration) + } + + private fun parseJson(jsonStr: String): SerializedValue = Json.createReader(StringReader(jsonStr)).use { reader -> + val requestJson = reader.readValue() + val result = SerializedValueJsonParser.serializedValueP.parse(requestJson) + return result.getSuccessOrThrow { DatabaseError(it.toString()) } + } + + private val activityInstancesStatement = c.prepareStatement( + "select start_offset, duration, attributes, activity_type_name from simulated_activity where simulation_dataset_id = ?;" + ) + override fun allActivityInstances(): Instances { + activityInstancesStatement.clearParameters() + activityInstancesStatement.setInt(1, simDatasetId) + intervalStyleStatement.execute() + val response = activityInstancesStatement.executeQuery() + val result = mutableListOf>() + while (response.next()) { + val start = Duration.parseISO8601(response.getString(1)) + val attributesString = response.getString(3) + val attributes = parseJson(attributesString) + val directiveId = attributes.asMap().getOrNull()?.get("directiveId")?.asInt()?.getOrNull() + ?: throw DatabaseError("Could not get directiveId from attributes: $attributesString") + val arguments = attributes.asMap().getOrNull()!!["arguments"]?.asMap()?.getOrNull() + ?: throw DatabaseError("Could not get arguments from attributes: $attributesString") + val computedAttributes = attributes.asMap().getOrNull()!!["computedAttributes"]?.asMap()?.getOrNull() + ?: throw DatabaseError("Could not get computed attributes from attributes: $attributesString") + result.add(Instance( + AnyInstance(arguments, computedAttributes), + response.getString(4), + directiveId, + between(start, start.plus(Duration.parseISO8601(response.getString(2)))) + )) + } + return Instances(result) + } + + private val activityDirectivesStatement = c.prepareStatement( + "select name, start_offset, type, arguments from activity_directive where plan_id = ?" + + " and start_offset > cast(? as interval) and start_offset < cast(? as interval);" + ) + override fun allActivityDirectives() = BaseTimeline(::Directives) { opts -> + activityDirectivesStatement.clearParameters() + activityDirectivesStatement.setInt(1, planInfo.id) + activityDirectivesStatement.setString(2, opts.bounds.start.toISO8601()) + activityDirectivesStatement.setString(3, opts.bounds.end.toISO8601()) + intervalStyleStatement.execute() + val response = activityDirectivesStatement.executeQuery() + val result = mutableListOf>() + while (response.next()) { + result.add(Directive( + AnyDirective( + parseJson(response.getString(4)).asMap().getOrNull()!! + ), + response.getString(1), + response.getString(3), + Duration.parseISO8601(response.getString(2)) + )) + } + result + }.specialize() +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt new file mode 100644 index 0000000000..0932050f5f --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt @@ -0,0 +1,39 @@ +package gov.nasa.jpl.aerie.timeline.plan + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.activities.AnyDirective +import gov.nasa.jpl.aerie.timeline.payloads.activities.AnyInstance +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.collections.Directives +import gov.nasa.jpl.aerie.timeline.collections.Instances +import java.time.Instant + +/** An interface for querying plan information and simulation results. */ +interface Plan { + /** Total extent of the plan's bounds, whether it was simulated on the full extent or not. */ + fun totalBounds(): Interval + + /** Bounds on which the plan was most recently simulated. */ + fun simBounds(): Interval + + /** Convert a time instant to a relative duration (relative to plan start). */ + fun toRelative(abs: Instant): Duration + /** Convert a relative duration to a time instant. */ + fun toAbsolute(rel: Duration): Instant + + /** + * Query a resource profile from the database + * + * @param ctor constructor of the profile, converting [SerializedValue] + * @param name string name of the resource + */ + fun > resource(name: String, ctor: (List>) -> TL): TL + + /** Query all activity instances. */ + fun allActivityInstances(): Instances + /** Query all activity directives. */ + fun allActivityDirectives(): Directives +} From 2ce4295ffddf1a98cf741cbd48bce09504d8ddaf Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 29 Feb 2024 13:00:32 -0800 Subject: [PATCH 110/159] Add module documentation and README --- timeline/MODULE_DOCS.md | 108 ++++++++++++++++++++++++++++++++++++++++ timeline/README.md | 13 +++++ 2 files changed, 121 insertions(+) create mode 100644 timeline/MODULE_DOCS.md create mode 100644 timeline/README.md diff --git a/timeline/MODULE_DOCS.md b/timeline/MODULE_DOCS.md new file mode 100644 index 0000000000..a35bfc3aef --- /dev/null +++ b/timeline/MODULE_DOCS.md @@ -0,0 +1,108 @@ +# Module Timeline + +This is a Kotlin library for manipulating collections of time-distributed objects on a timeline. "Time-distributed" means that +each object occupies an instant or interval of time. For example, an activity instance occupies the interval over which it +executed, and so a `Instances` collection can be used to reason about and manipulate a set of activities in aggregate. + +## Architecture + +This package is built around the `BaseTimeline` data class, which every timeline type contains and delegates to. All a `BaseTimeline` object does +is represent a "collector" function, which, when given bounds of evaluation, will produce a list of `IntervalLike` objects. All timeline operations in +this package are composed lazily, because the collection bounds can change based on the operations applied, and only the final evaluation bounds are known. + +The collections are created primarily by combining mixin interfaces from the `ops` package. All mixin interfaces are named `...Ops` or `...Op` +(i.e. `ActivityOps` or `CoalesceOp`). When implementing a new operation for a timeline type, first consider whether it is general enough to +apply to more than one type. If so, either add it to the appropriate mixin if it exists, or create a new one if it doesn't. Operations should +only be added as a class method if there's no potential for it to apply to any other type (such as `Booleans.shiftEdges`, which is highly specific to boolean profiles). + +### Time Representation + +Instants of time are represented using the `Duration` class, defined as the duration from the plan start. This +means that `Duration.ZERO` always refers to the start of the plan. You can convert these to and from `java.time.Instant` objects using utility functions on a `Plan` object, and use that to reason about time of day. + +`Interval`s represent contiguous ranges of time, which may include or exclude their end points. They may be single points or even empty. `IntervalLike` is the interface that all timeline payload objects must satisfy. + +### Timeline Types + +All timelines implement the `Timeline` interface, by delegating to a `BaseTimeline` object. The `BaseTimeline`s purpose is to hold a collector function (which evaluates the timeline and returns a list) and a constructor function (which wraps the `BaseTimeline` in a more specialized `Timeline` implementor). Users should never +need to use `BaseTimeline`s directly, except when creating new timeline types. + +Timelines can be conceptually separated into "profiles" and "everything else". + +- Profiles are timelines of *time-ordered, non-overlapping* `Segment` objects. Segments are just a general-purpose container + that associates a payload with an interval to satisfy the `IntervalLike` interface. They are also *coalesced*, + meaning that adjacent segments with equal values are combined into a single segment. Profiles are most often used to track the + evolution of a simulation resource over time. Profiles can have "gaps", or intervals where there is no segment. Conceptually, + gaps are intervals when the evolving value represented by the profile is undefined. +- All other timelines are collections of *unordered, potentially overlapping* objects, like activity instances, activity directives, or plain intervals (a timeline of intervals with no payload). + +### Collecting + +All operations are performed lazily, and only happen when the `.collect(CollectOptions)` method is called. `CollectOptions` allows you to specify: +- the bounds of evaluation (an `Interval`), outside which no data should be calculated or returned +- whether "marginal" objects (objects that are only partially contained in the bounds) should be truncated + or returned whole. + +Some operations may need to change the collect options on the timelines they are applied to. For example, in the following code: + +```kotlin +val original: Booleans = /* ... */ + +// shift the profile one hour into the future. +val shifted = original.shift(Duration.HOUR) + +val segments = shifted.collect(CollectOptions( + bounds = Interval.between(Duration.ZERO, Duration.DAY), + truncateMarginal = true +)) +``` + +When `shifted`'s collector is invoked, it will in turn invoke `original'`s collector - but not on the same bounds. +Instead, the bounds will be `Interval.between(-HOUR, DAY - HOUR)`. It is one hour earlier so that objects in `original` just before the intended bounds of `[ZERO, DAY]` are properly calculated and shifted into the bounds. + +Similarly, some operations can change whether marginal objects are truncated. `GeneralOps.filterByDuration` and `NonZeroDurationOps.split` will always perform their call the previous timeline's collector with `truncateMarginal = false`, because they need to know the full duration of all objects. (They will then truncate the marginal objects in the result if it was requested in `CollectOptions`.) Unfortunately, this is not totally fool-proof; if an +operation is applied to a profile that would have caused a segment fully outside the bounds to be coalesced (see below) +with a marginal segment, the full extent of the segment will be lost. + +### Coalescing + +Profiles are "coalesced", meaning that adjacent or overlapping segments with equal values are merged into a single segment over the union of their intervals. This is performed automatically after every operation. This can have implications for operations like `GeneralOps.filterByDuration` and `NonZeroDurationOps.split`, which care about the duration of each interval. + +Temporarily avoiding coalescing for a profile operation is possible, but not recommended or ergonomic. You can +call `myProfile.convert(::Intervals)`. The `Intervals` timeline type is the most general, and has no special mathematical properties and no specialized knowledge of the data it contains. You'd then perform the desired operation (which will likely be less ergonomic due to `Intervals`' lack of specialization) and convert it back with `.convert(::/* original type */)`. + +### Numerics + +There are two options for representing numeric profiles: `Real` and `Numbers`. `Numbers` is piece-wise constant and can contain any primitive numeric type, while `Real` is piece-wise linear and can only contain doubles. `Real` is unique because it is the only profile type so far that represents values that vary within the segment, not just between segments. + +# Package gov.nasa.jpl.aerie.timeline.collections +The officially supported timeline types. + +# Package gov.nasa.jpl.aerie.timeline.collections.profiles +Timeline types for resource profiles. + +# Package gov.nasa.jpl.aerie.timeline.ops +Operations mixins to be applied to timeline types. + +# Package gov.nasa.jpl.aerie.timeline.ops.coalesce +Operations mixins for specifying whether a timeline should be coalesced. + +# Package gov.nasa.jpl.aerie.timeline.ops.numeric +Operations mixins just for numeric types (`Real` and `Numbers`). + +# Package gov.nasa.jpl.aerie.timeline.payloads +Payload types (`IntervalLike` implementors) that can be contained in timelines. + +# Package gov.nasa.jpl.aerie.timeline.payloads.activities +Containers for representing activity directives and instants. + +Currently, there is no specialization for activity +types, and all directives and instants use `AnyDirective` and `AnyInstance`, respectively. These classes represent +arguments and computed attributes using `SerializedValue`. + +# Package gov.nasa.jpl.aerie.timeline.plan +Tools for querying simulation results, activity directives, and general information from a plan. + +# Package gov.nasa.jpl.aerie.timeline.util +Common tools used by operations and timeline constructors to sanitize and process lists. + diff --git a/timeline/README.md b/timeline/README.md new file mode 100644 index 0000000000..128439d7d1 --- /dev/null +++ b/timeline/README.md @@ -0,0 +1,13 @@ +# Timelines + +This library provides tools for querying and manipulating "timelines" from an Aerie plan or set of +simulation results. This includes things like resource profiles, activity instances, and activity directives, +but can be extended to support more kinds if needed. + +See [MODULE_DOCS.md](./MODULE_DOCS.md) for a description of the architecture and design of the library. + +- Building and testing: `./gradlew :timeline:build` +- Generating a jar for local experimentation: `./gradlew :timeline:shadowJar` + - jar will be available at `timeline/build/libs/timeline-all.jar` +- Generating documentation: `./gradlew :timeline:dokkaHtml` + - docs will be available at `timeline/build/dokka/html/index.html` From cb40fca9ddacd1d600bab7c90bd153c3958b7d06 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Mar 2024 15:38:55 -0800 Subject: [PATCH 111/159] Add non-optional binary map operations --- ...aryOperation.kt => NullBinaryOperation.kt} | 20 +-- .../nasa/jpl/aerie/timeline/ops/GeneralOps.kt | 16 ++ .../jpl/aerie/timeline/ops/ParallelOps.kt | 19 ++- .../nasa/jpl/aerie/timeline/ops/SegmentOps.kt | 16 ++ .../aerie/timeline/ops/SerialBooleanOps.kt | 12 +- .../aerie/timeline/ops/SerialConstantOps.kt | 12 +- .../nasa/jpl/aerie/timeline/ops/SerialOps.kt | 157 +---------------- .../aerie/timeline/ops/SerialSegmentOps.kt | 160 ++++++++++++++++++ .../timeline/ops/numeric/SerialLinearOps.kt | 31 ++-- .../timeline/ops/numeric/SerialNumericOps.kt | 4 +- .../ops/numeric/SerialPrimitiveNumberOps.kt | 45 +++-- .../util/{Map2SegmentLists.kt => Map2.kt} | 73 ++++++-- .../jpl/aerie/timeline/ops/ParallelOpsTest.kt | 4 +- ...rialOpsTest.kt => SerialSegmentOpsTest.kt} | 6 +- .../util/{Map2SerialTest.kt => Map2Test.kt} | 10 +- 15 files changed, 349 insertions(+), 236 deletions(-) rename timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/{BinaryOperation.kt => NullBinaryOperation.kt} (88%) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt rename timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/{Map2SegmentLists.kt => Map2.kt} (72%) rename timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/{SerialOpsTest.kt => SerialSegmentOpsTest.kt} (93%) rename timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/{Map2SerialTest.kt => Map2Test.kt} (95%) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BinaryOperation.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/NullBinaryOperation.kt similarity index 88% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BinaryOperation.kt rename to timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/NullBinaryOperation.kt index 504f9ec2b4..11fe177975 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BinaryOperation.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/NullBinaryOperation.kt @@ -11,9 +11,9 @@ package gov.nasa.jpl.aerie.timeline * Helper functions for constructing binary operations with common patterns are available * in this interface's companion object [here][Companion]. Unfortunately, Kotlin's documentation * generator Dokka doesn't like to show companion object methods inside interfaces, but all these - * methods can be called just like a static method (i.e. `BinaryOperation.combineOrNull(...)`). + * methods can be called just like a static method (i.e. `NullBinaryOperation.combineOrNull(...)`). */ -fun interface BinaryOperation { +fun interface NullBinaryOperation { /** * Calculate the operation. @@ -36,15 +36,15 @@ fun interface BinaryOperation { left: (Left & Any, Interval) -> Out, right: (Right & Any, Interval) -> Out, combine: (Left & Any, Right & Any, Interval) -> Out - ) = BinaryOperation { l, r, i -> + ) = NullBinaryOperation { l, r, i -> if (l != null && r != null) combine(l, r, i) else if (l != null) left(l, i) else if (r != null) right(r, i) else throw BinaryOperationBothNullException() } - /** A named overload of the default constructor. */ - @JvmStatic fun singleFunction(f: (Left?, Right?, Interval) -> Out) = BinaryOperation(f) + /** A named version of the default constructor. */ + @JvmStatic fun singleFunction(f: (Left?, Right?, Interval) -> Out) = NullBinaryOperation(f) /** * Constructs an operation that combines the operands in some way if they are both present, @@ -52,7 +52,7 @@ fun interface BinaryOperation { * * @param f operation to be invoked when both operands are present. */ - @JvmStatic fun combineOrNull(f: (Left & Any, Right & Any, Interval) -> Out) = BinaryOperation { l, r, i -> + @JvmStatic fun combineOrNull(f: (Left & Any, Right & Any, Interval) -> Out) = NullBinaryOperation { l, r, i -> if (l == null || r == null) null else f(l, r, i) } @@ -63,7 +63,7 @@ fun interface BinaryOperation { * * This means that both operands and the output must all be the same type. */ - @JvmStatic fun combineOrIdentity(f: (V & Any, V & Any, Interval) -> V) = BinaryOperation { l, r, i -> + @JvmStatic fun combineOrIdentity(f: (V & Any, V & Any, Interval) -> V) = NullBinaryOperation { l, r, i -> if (l != null && r != null) f(l, r, i) else l ?: r ?: throw BinaryOperationBothNullException() } @@ -85,7 +85,7 @@ fun interface BinaryOperation { @JvmStatic fun reduce( convert: (new: In & Any, Interval) -> Out, combine: (new: In & Any, acc: Out & Any, Interval) -> Out - ) = BinaryOperation { new, acc, i -> + ) = NullBinaryOperation { new, acc, i -> if (acc != null && new != null) combine(new, acc, i) else if (new != null) convert(new, i) else acc ?: throw BinaryOperationBothNullException() @@ -96,7 +96,7 @@ fun interface BinaryOperation { * * Throws [ZipOperationBothDefinedException] if both operands are present. */ - @JvmStatic fun zip() = BinaryOperation { l, r, _ -> + @JvmStatic fun zip() = NullBinaryOperation { l, r, _ -> if (l != null && r != null) throw ZipOperationBothDefinedException() else l ?: (r ?: throw BinaryOperationBothNullException()) } @@ -109,7 +109,7 @@ fun interface BinaryOperation { @JvmStatic fun convertZip( left: (Left & Any, Interval) -> Out, right: (Right & Any, Interval) -> Out, - ) = BinaryOperation { l, r, i -> + ) = NullBinaryOperation { l, r, i -> if (l != null && r != null) throw ZipOperationBothDefinedException() else if (l != null) left(l, i) else if (r != null) right(r, i) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt index c7c3f7fb56..93737e1461 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt @@ -5,6 +5,7 @@ import gov.nasa.jpl.aerie.timeline.collections.Intervals import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.util.coalesceList +import gov.nasa.jpl.aerie.timeline.util.map2ParallelLists import gov.nasa.jpl.aerie.timeline.util.sorted import gov.nasa.jpl.aerie.timeline.util.truncateList @@ -87,6 +88,13 @@ interface GeneralOps, THIS: GeneralOps>: Timeline Boolean)? + /** + * Whether the result of [collect] is guaranteed to be a sorted list. + * + * @suppress + */ + fun isAlwaysSorted(): Boolean + /** * [(DOC)][unset] Unsets everything in a given interval. Timeline objects whose intervals fully contain the rejected interval may be * split into two objects. @@ -189,6 +197,14 @@ interface GeneralOps, THIS: GeneralOps>: Timeline Interval) = unsafeMap(boundsTransformer, truncate) { v -> v.withNewInterval(f(v)) } + /** + * Performs a generalized binary operation between this and another timeline. + */ + fun , OTHER: GeneralOps, R: IntervalLike, RESULT: GeneralOps> unsafeMap2(ctor: (Timeline) -> RESULT, other: GeneralOps, op: (V, W, Interval) -> R?) = + unsafeOperate(ctor) { opts -> + map2ParallelLists(collect(opts), other.collect(opts), isAlwaysSorted(), other.isAlwaysSorted(), op) + } + /** * [(DOC)][filter] Removes or retains objects based on a predicate. * diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt index debcb510ee..175b2d87f2 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.timeline.* import gov.nasa.jpl.aerie.timeline.collections.Intervals import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceNoOp import gov.nasa.jpl.aerie.timeline.payloads.Connection import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike import gov.nasa.jpl.aerie.timeline.payloads.Segment @@ -13,9 +14,12 @@ import gov.nasa.jpl.aerie.timeline.util.truncateList /** * Operations mixin for timelines of potentially overlapping objects. * - * Opposite of [SerialOps]. + * Opposite of [SerialSegmentOps]. */ -interface ParallelOps, THIS: ParallelOps>: GeneralOps { +interface ParallelOps, THIS: ParallelOps>: GeneralOps, CoalesceNoOp { + + override fun isAlwaysSorted() = false + /** [(DOC)][merge] Combines two timelines together by overlaying them. Does not perform any transformation. */ infix fun > merge(other: GeneralOps) = unsafeOperate { opts -> collect(opts) + other.collect(opts) @@ -42,7 +46,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp * @param ctor the constructor of the result timeline * @param f a function which converts a payload object into the value of the resulting segment */ - fun > flattenIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, f: (T) -> R) = + fun > flattenIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, f: (T) -> R) = unsafeOperate(ctor) { bounds -> val result = collect(bounds).mapTo(mutableListOf()) { Segment(it.interval, f(it)) } result.sortWith { l, r -> l.interval.compareStarts(r.interval) } @@ -54,7 +58,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp * * After the payload objects are converted into segments, any overlapping segments are resolved by combining them * with a binary operation similar to a reduce operation (as in functional programming). It is recommended to - * create the binary operation using the [BinaryOperation.reduce] function. + * create the binary operation using the [NullBinaryOperation.reduce] function. * * ## Example * @@ -80,7 +84,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp * @param ctor the constructor of the result profile * @param op a binary operation for converting and combining the input objects */ - fun > reduceIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, op: BinaryOperation) = + fun > reduceIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, op: NullBinaryOperation) = unsafeOperate(ctor) { opts -> val bounds = opts.bounds var acc: List> = listOf() @@ -132,7 +136,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp fun active() = flattenIntoProfile(::Booleans) { _ -> true }.assignGaps(Booleans(false)) /** [(DOC)][countActive] Returns a [Numbers] profile that corresponds to the number of active objects at any given time. */ - fun countActive() = reduceIntoProfile(::Numbers, BinaryOperation.reduce( + fun countActive() = reduceIntoProfile(::Numbers, NullBinaryOperation.reduce( { _, _ -> 1 }, { _, acc, _ -> acc.toInt() + 1} )).assignGaps(Numbers(0)) @@ -189,7 +193,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp * @param other the other timeline to connect to * @param connectToBounds whether to connect to the end of the bounds if the other timeline ends prematurely */ - fun , OTHER: ParallelOps>connectTo(other: ParallelOps, connectToBounds: Boolean) = + fun , OTHER: ParallelOps> connectTo(other: ParallelOps, connectToBounds: Boolean) = unsafeOperate(::Intervals) { opts -> val sortedFrom = collect(opts).sortedWith { l, r -> l.interval.compareEnds(r.interval) } val sortedTo = other.collect(opts).sortedWith { l, r -> l.interval.compareStarts(r.interval) } @@ -235,4 +239,5 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp */ fun rollingDuration(range: Duration, unit: Duration) = accumulatedDuration(unit).shiftedDifference(range) + } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt index 6f8652489c..7b40c7e899 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt @@ -3,6 +3,7 @@ package gov.nasa.jpl.aerie.timeline.ops import gov.nasa.jpl.aerie.timeline.* import gov.nasa.jpl.aerie.timeline.BoundsTransformer.Companion.IDENTITY import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.util.map2ParallelLists /** * Operations mixin for timelines of segments. @@ -47,4 +48,19 @@ interface SegmentOps>: NonZeroDurationOps, RESULT: SegmentOps> flatMapValues(ctor: (Timeline, RESULT>) -> RESULT, f: (Segment) -> NESTED) = unsafeFlatMap(ctor, IDENTITY, false) { it.mapValue(f) } + + fun > map2Values(other: SegmentOps, op: (V, V, Interval) -> V?) = map2Values(ctor, other, op) + + fun , R: Any, RESULT: SegmentOps> map2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> R?) = + unsafeMap2(ctor, other) { l, r, i -> op(l.value, r.value, i)?.let { Segment(i, it) } } + + fun , NESTED: SegmentOps> flatMap2Values(other: SegmentOps, op: (V, V, Interval) -> NESTED?) = + flatMap2Values(ctor, other, op) + + fun , R: Any, NESTED: SegmentOps, RESULT: SegmentOps> flatMap2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> NESTED?) = + unsafeOperate(ctor) { opts -> + map2ParallelLists(collect(opts), other.collect(opts), isAlwaysSorted(), other.isAlwaysSorted()) { l, r, i -> + op(l.value, r.value, i)?.let { Segment(i, it) } + }.flatMap { it.value.collect(CollectOptions(it.interval, true)) } + } } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt index 1b5f56ecee..74539c7587 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.timeline.ops -import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.NullBinaryOperation import gov.nasa.jpl.aerie.timeline.Duration import gov.nasa.jpl.aerie.timeline.Interval import gov.nasa.jpl.aerie.timeline.collections.profiles.Real @@ -11,31 +11,31 @@ import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation */ interface SerialBooleanOps>: SerialConstantOps, BooleanOps { /** [(DOC)][and] Computes the AND operation between two boolean profiles. */ - infix fun > and(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + infix fun > and(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( { l, _ -> if (l) null else false }, { r, _ -> if (r) null else false }, { l, r, _ -> l && r } )) /** [(DOC)][or] Computes the OR operation between two boolean profiles. */ - infix fun > or(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + infix fun > or(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( { l, _ -> if (l) true else null }, { r, _ -> if (r) true else null }, { l, r, _ -> l || r } )) /** [(DOC)][xor] Computes the XOR operation between two boolean profiles. */ - infix fun > xor(other: SerialBooleanOps) = map2Values(other, BinaryOperation.combineOrNull { l, r, _ -> l.xor(r) }) + infix fun > xor(other: SerialBooleanOps) = map2Values(other) { l, r, _ -> l xor r } /** [(DOC)][nor] the NOR operation between two boolean profiles. */ - infix fun > nor(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + infix fun > nor(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( { l, _ -> if (l) false else null }, { r, _ -> if (r) false else null }, { l, r, _ -> !(l || r) } )) /** [(DOC)][nand] Computes the NAND operation between two boolean profiles. */ - infix fun > nand(other: SerialBooleanOps) = map2Values(other, BinaryOperation.cases( + infix fun > nand(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( { l, _ -> if (l) null else true }, { r, _ -> if (r) null else true }, { l, r, _ -> !(l && r) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt index 5528845146..168690d993 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt @@ -8,27 +8,29 @@ import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants * Operations mixin for segment-valued timelines whose payloads * represent constant values. */ -interface SerialConstantOps>: SerialOps, ConstantOps { +interface SerialConstantOps>: SerialSegmentOps, ConstantOps { /** [(DOC)][equalTo] Returns a [Booleans] that is `true` when this and another profile are equal. */ infix fun > equalTo(other: OTHER) = - map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> l == r }) + map2Values(::Booleans, other) { l, r, _ -> l == r } + /** [(DOC)][equalTo] Returns a [Booleans] that is `true` when this equals a constant value. */ infix fun equalTo(v: V) = equalTo(Constants(v)) /** [(DOC)][notEqualTo] Returns a [Booleans] that is `true` when this and another profile are not equal. */ infix fun > notEqualTo(other: OTHER) = - map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> l != r }) + map2Values(::Booleans, other) { l, r, _ -> l != r } + /** [(DOC)][notEqualTo] Returns a [Booleans] that is `true` when this is not equal to a constant value. */ infix fun notEqualTo(v: V) = notEqualTo(Constants(v)) - override fun changes() = detectEdges(BinaryOperation.combineOrNull { l, r, _-> l != r }) + override fun changes() = detectEdges(NullBinaryOperation.combineOrNull { l, r, _-> l != r }) /** * [(DOC)][transitions] Returns a [Booleans] that is `true` when this profile's value changes between * two specific values. */ - fun transitions(from: V, to: V) = detectEdges(BinaryOperation.cases( + fun transitions(from: V, to: V) = detectEdges(NullBinaryOperation.cases( { l, _ -> if (l == from) null else false }, { r, _ -> if (r == to) null else false }, { l, r, _ -> l == from && r == to } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt index 3ebcf852b7..40c749a73b 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt @@ -1,158 +1,7 @@ package gov.nasa.jpl.aerie.timeline.ops -import gov.nasa.jpl.aerie.timeline.* -import gov.nasa.jpl.aerie.timeline.Interval.Companion.at -import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans -import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants -import gov.nasa.jpl.aerie.timeline.payloads.Segment -import gov.nasa.jpl.aerie.timeline.payloads.transpose -import gov.nasa.jpl.aerie.timeline.util.coalesceList -import gov.nasa.jpl.aerie.timeline.util.map2SegmentLists -import gov.nasa.jpl.aerie.timeline.util.truncateList +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike -/** - * Operations mixin for timelines of ordered, non-overlapping segments (profiles). - * - * Opposite of [ParallelOps]. - */ -interface SerialOps>: SegmentOps { - /** Overlays two profiles on each other, asserting that they both cannot be defined at the same time. */ - infix fun > zip(other: SerialOps) = map2Values(other, BinaryOperation.zip()) - - /** [(DOC)][assignGaps] Fills in gaps in this profile with another profile. */ - // While this is logically the converse of [set], they can't delegate to each other because it would mess up the return type. - infix fun > assignGaps(other: SerialOps) = - map2Values(other, BinaryOperation.combineOrIdentity { l, _, _, -> l }) - /** [(DOC)][assignGaps] Fills in gaps in this profile with a constant value. */ - infix fun assignGaps(v: V) = assignGaps(Constants(v)) - - /** [(DOC)][set] Overwrites this profile with another. Gaps in the argument profile will be filled in with this profile. */ - infix fun > set(other: SerialOps) = map2Values(other, BinaryOperation.combineOrIdentity { _, r, _ -> r }) - - /** - * [(DOC)][map2Values] Performs a local binary operation between two profiles where the result - * is the same type as this profile. - */ - fun > map2Values(other: SerialOps, op: BinaryOperation) = map2Values(ctor, other, op) - - /** - * [(DOC)][map2Values] Performs a local binary operation between two profiles. - * - * The operation will be evaluated on each pair of segments that overlap, with their - * intersection supplied as the interval argument to the [BinaryOperation]. The result of - * the operation is inserted in the result timeline at that intersection. Additionally, - * The operation will be evaluated on each segment in the profiles that overlaps with - * a gap in the other profile, and the gap will be indicated by a `null` in that operand's - * argument in [BinaryOperation]. The operation is not called for intervals that have a gap - * in both profiles - the result will automatically have a gap there. - * - * The binary operation may return `null`, which indicates that the result profile should have - * a gap. - * - * The operation is "local", meaning that while the operation is allowed to know when it is - * being evaluated, it is not allowed to change where the result segment should be placed. - * For that, you can use [unsafeMap], or more generally, [unsafeOperate]. - * - * @param W the other operand's payload type - * @param OTHER the other operand's timeline type - * @param R the result's payload type - * @param RESULT the result's timeline type - * - * @param ctor the result timeline's constructor - * @param other the other operand timeline - * @param op a binary operation between the two payload types that produces a maybe-null result - * - * @return a coalesced profile; an instance of the return type of [ctor] - */ - fun , R: Any, RESULT: GeneralOps, RESULT>> map2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SerialOps, op: BinaryOperation) = - unsafeOperate(ctor) { bounds -> map2SegmentLists(collect(bounds), other.collect(bounds), op) } - - /** - * [(DOC)][flatMap2Values] Performs a local binary operation that produces profiles, and flattens - * it into a profile of the same type as this. - */ - fun , NESTED: SerialOps> flatMap2Values(other: SerialOps, op: BinaryOperation) = flatMap2Values(ctor, other, op) - - /** - * [(DOC)][flatMap2Values] Performs a local binary operation that produces profiles, and flattens it. - * - * Similar to [map2Values], except it expects the [BinaryOperation] to return a profile. Each nested profile - * is then collected on the interval it corresponds to, and the results are concatenated into a single profile. - * - * This is useful for binary operations where at least one of the operand segments represents a value that - * varies within the segment, such as [gov.nasa.jpl.aerie.timeline.collections.profiles.Real]. - * - * @param W the other operand's payload type - * @param OTHER the other operand's timeline type - * @param R the result's payload type - * @param NESTED the nested profile type returned by the operation before flattening - * @param RESULT the result's timeline type - * - * @param ctor the result timeline's constructor - * @param other the other operand timeline - * @param op a binary operation between the two payload types that produces a maybe-null profile - * - * @return a coalesced flattened profile; an instance of the return type of [ctor] - */ - fun , R: Any, NESTED: SerialOps, RESULT: GeneralOps, RESULT>> flatMap2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SerialOps, op: BinaryOperation) = - unsafeOperate(ctor) { opts -> - map2SegmentLists(collect(opts), other.collect(opts), op) - .flatMap { it.value.collect(CollectOptions(it.interval, true)) } - } - - /** - * [(DOC)][detectEdges] Uses a [BinaryOperation] as a predicate to highlight edges between segments. - * - * The result is a [Booleans] profile, which is `false` inside segments, a gap when - * this profile is a gap, and possibly `true`, `false`, or a gap on segment edges according - * to the result of the predicate. - * - * The predicate will be called at the edge between two adjacent segments, where the values of - * the left and right segments are the left and right operands of the predicate, respectively. - * The predicate will also be call at any edge where a segment meets a gap, in which case the appropriate operand - * will be `null`. - * - * @param edgePredicate a binary operation between operands of type [V] that produces a boolean or `null` - * @return a [Booleans] object that contains `true` on the edges indicated by the predicate - */ - fun detectEdges(edgePredicate: BinaryOperation) = unsafeOperate(::Booleans) { opts -> - val bounds = opts.bounds - var buffer: Segment? = null - val result = collect(CollectOptions(bounds, false)) - .flatMap { currentSegment -> - val previous = buffer - buffer = currentSegment - val currentInterval = currentSegment.interval - - val leftEdgeInterval = at(currentInterval.start) - val rightEdgeInterval = at(currentInterval.end) - - val rightEdge = edgePredicate(currentSegment.value, null, rightEdgeInterval) - - val leftEdge = if (previous == null || previous.interval.compareEndToStart(currentInterval) == -1) { - edgePredicate(null, currentSegment.value, leftEdgeInterval) - } else { - edgePredicate(previous.value, currentSegment.value, leftEdgeInterval) - } - - listOfNotNull( - Segment(leftEdgeInterval, leftEdge).transpose(), - Segment( - Interval.between(currentInterval.start, currentInterval.end, Interval.Inclusivity.Exclusive), - false - ), - Segment(rightEdgeInterval, rightEdge).transpose() - ) - } - truncateList(coalesceList(result, Segment::valueEquals), opts) - } - - /** - * [(DOC)][changes] Returns a [Booleans] that is true whenever this profile changes, and false or gap everywhere else. - * - * This includes both continuous changes and discontinuous changes, if the profile can vary continuously. - */ - fun changes(): Booleans - // `transitions(from, to)` is a similar function that you expect to also have a declaration here, but this isn't - // feasible because `Real.transitions` takes doubles as its arguments instead of its normal payload type (LinearEquation) +interface SerialOps, THIS: SerialOps>: GeneralOps { + override fun isAlwaysSorted() = true } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt new file mode 100644 index 0000000000..283671f024 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt @@ -0,0 +1,160 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans +import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import gov.nasa.jpl.aerie.timeline.payloads.transpose +import gov.nasa.jpl.aerie.timeline.util.coalesceList +import gov.nasa.jpl.aerie.timeline.util.map2SegmentLists +import gov.nasa.jpl.aerie.timeline.util.truncateList + +/** + * Operations mixin for timelines of ordered, non-overlapping segments (profiles). + * + * Opposite of [ParallelOps]. + */ +interface SerialSegmentOps>: SerialOps, THIS>, SegmentOps, CoalesceSegmentsOp { + /** Overlays two profiles on each other, asserting that they both cannot be defined at the same time. */ + infix fun > zip(other: SerialSegmentOps) = map2OptionalValues(other, NullBinaryOperation.zip()) + + /** [(DOC)][assignGaps] Fills in gaps in this profile with another profile. */ + // While this is logically the converse of [set], they can't delegate to each other because it would mess up the return type. + infix fun > assignGaps(other: SerialSegmentOps) = + map2OptionalValues(other, NullBinaryOperation.combineOrIdentity { l, _, _, -> l }) + /** [(DOC)][assignGaps] Fills in gaps in this profile with a constant value. */ + infix fun assignGaps(v: V) = assignGaps(Constants(v)) + + /** [(DOC)][set] Overwrites this profile with another. Gaps in the argument profile will be filled in with this profile. */ + infix fun > set(other: SerialSegmentOps) = map2OptionalValues(other, NullBinaryOperation.combineOrIdentity { _, r, _ -> r }) + + /** + * [(DOC)][map2OptionalValues] Performs a local binary operation between two profiles where the result + * is the same type as this profile. + */ + fun > map2OptionalValues(other: SerialSegmentOps, op: NullBinaryOperation) = map2OptionalValues(ctor, other, op) + + /** + * [(DOC)][map2OptionalValues] Performs a local binary operation between two profiles, with special treatment + * of gaps. + * + * The operation will be evaluated on each pair of segments that overlap, with their + * intersection supplied as the interval argument to the [NullBinaryOperation]. The result of + * the operation is inserted in the result timeline at that intersection. Additionally, + * The operation will be evaluated on each segment in the profiles that overlaps with + * a gap in the other profile, and the gap will be indicated by a `null` in that operand's + * argument in [NullBinaryOperation]. The operation is not called for intervals that have a gap + * in both profiles - the result will automatically have a gap there. + * + * The binary operation may return `null`, which indicates that the result profile should have + * a gap. + * + * The operation is "local", meaning that while the operation is allowed to know when it is + * being evaluated, it is not allowed to change where the result segment should be placed. + * For that, you can use [unsafeMap], or more generally, [unsafeOperate]. + * + * @param W the other operand's payload type + * @param OTHER the other operand's timeline type + * @param R the result's payload type + * @param RESULT the result's timeline type + * + * @param ctor the result timeline's constructor + * @param other the other operand timeline + * @param op a binary operation between the two payload types that produces a maybe-null result + * + * @return a coalesced profile; an instance of the return type of [ctor] + */ + fun , R: Any, RESULT: GeneralOps, RESULT>> map2OptionalValues(ctor: (Timeline, RESULT>) -> RESULT, other: SerialSegmentOps, op: NullBinaryOperation) = + unsafeOperate(ctor) { bounds -> map2SegmentLists(collect(bounds), other.collect(bounds), op) } + + /** + * [(DOC)][flatMap2OptionalValues] Performs a local binary operation that produces profiles, and flattens + * it into a profile of the same type as this. + */ + fun , NESTED: SerialSegmentOps> flatMap2OptionalValues(other: SerialSegmentOps, op: NullBinaryOperation) = flatMap2OptionalValues(ctor, other, op) + + /** + * [(DOC)][flatMap2OptionalValues] Performs a local binary operation that produces profiles, and flattens it. + * + * Similar to [map2OptionalValues], except it expects the [NullBinaryOperation] to return a profile. Each nested profile + * is then collected on the interval it corresponds to, and the results are concatenated into a single profile. + * + * This is useful for binary operations where at least one of the operand segments represents a value that + * varies within the segment, such as [gov.nasa.jpl.aerie.timeline.collections.profiles.Real]. + * + * @param W the other operand's payload type + * @param OTHER the other operand's timeline type + * @param R the result's payload type + * @param NESTED the nested profile type returned by the operation before flattening + * @param RESULT the result's timeline type + * + * @param ctor the result timeline's constructor + * @param other the other operand timeline + * @param op a binary operation between the two payload types that produces a maybe-null profile + * + * @return a coalesced flattened profile; an instance of the return type of [ctor] + */ + fun , R: Any, NESTED: SerialSegmentOps, RESULT: GeneralOps, RESULT>> flatMap2OptionalValues(ctor: (Timeline, RESULT>) -> RESULT, other: SerialSegmentOps, op: NullBinaryOperation) = + unsafeOperate(ctor) { opts -> + map2SegmentLists(collect(opts), other.collect(opts), op) + .flatMap { it.value.collect(CollectOptions(it.interval, true)) } + } + + /** + * [(DOC)][detectEdges] Uses a [NullBinaryOperation] as a predicate to highlight edges between segments. + * + * The result is a [Booleans] profile, which is `false` inside segments, a gap when + * this profile is a gap, and possibly `true`, `false`, or a gap on segment edges according + * to the result of the predicate. + * + * The predicate will be called at the edge between two adjacent segments, where the values of + * the left and right segments are the left and right operands of the predicate, respectively. + * The predicate will also be call at any edge where a segment meets a gap, in which case the appropriate operand + * will be `null`. + * + * @param edgePredicate a binary operation between operands of type [V] that produces a boolean or `null` + * @return a [Booleans] object that contains `true` on the edges indicated by the predicate + */ + fun detectEdges(edgePredicate: NullBinaryOperation) = unsafeOperate(::Booleans) { opts -> + val bounds = opts.bounds + var buffer: Segment? = null + val result = collect(CollectOptions(bounds, false)) + .flatMap { currentSegment -> + val previous = buffer + buffer = currentSegment + val currentInterval = currentSegment.interval + + val leftEdgeInterval = at(currentInterval.start) + val rightEdgeInterval = at(currentInterval.end) + + val rightEdge = edgePredicate(currentSegment.value, null, rightEdgeInterval) + + val leftEdge = if (previous == null || previous.interval.compareEndToStart(currentInterval) == -1) { + edgePredicate(null, currentSegment.value, leftEdgeInterval) + } else { + edgePredicate(previous.value, currentSegment.value, leftEdgeInterval) + } + + listOfNotNull( + Segment(leftEdgeInterval, leftEdge).transpose(), + Segment( + Interval.between(currentInterval.start, currentInterval.end, Interval.Inclusivity.Exclusive), + false + ), + Segment(rightEdgeInterval, rightEdge).transpose() + ) + } + truncateList(coalesceList(result, Segment::valueEquals), opts) + } + + /** + * [(DOC)][changes] Returns a [Booleans] that is true whenever this profile changes, and false or gap everywhere else. + * + * This includes both continuous changes and discontinuous changes, if the profile can vary continuously. + */ + fun changes(): Booleans + // `transitions(from, to)` is a similar function that you expect to also have a declaration here, but this isn't + // feasible because `Real.transitions` takes doubles as its arguments instead of its normal payload type (LinearEquation) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt index 63aa0facec..b29117e9c7 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt @@ -34,18 +34,20 @@ interface SerialLinearOps>: SerialNumericOps> plus(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, _ -> + operator fun > plus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> val shiftedRight = r.shiftInitialTime(l.initialTime) LinearEquation(l.initialTime, l.initialValue + shiftedRight.initialValue, l.rate + r.rate) - }) + } + /** [(DOC)][plus] Adds a constant number to this. */ operator fun plus(n: Number) = plus(Numbers(n)) /** [(DOC)][minus] Subtracts another numeric profile from this. */ - operator fun > minus(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, _ -> + operator fun > minus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> val shiftedRight = r.shiftInitialTime(l.initialTime) LinearEquation(l.initialTime, l.initialValue - shiftedRight.initialValue, l.rate - r.rate) - }) + } + /** [(DOC)][minus] Subtracts a constant number from this. */ operator fun minus(n: Number) = minus(Numbers(n)) @@ -54,12 +56,13 @@ interface SerialLinearOps>: SerialNumericOps> times(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, i -> + operator fun > times(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> if (!l.isConstant() && !r.isConstant()) throw SerialLinearOpException("Cannot multiply two linear equations that are non-constant at the same time (at time ${i.start})") val shiftedRight = r.shiftInitialTime(l.initialTime) val newRate = l.rate * shiftedRight.initialValue + r.rate * l.initialValue LinearEquation(l.initialTime, l.initialValue * shiftedRight.initialValue, newRate) - }) + } + /** [(DOC)][times] Multiplies this by a constant number. */ operator fun times(n: Number) = times(Numbers(n)) @@ -68,11 +71,12 @@ interface SerialLinearOps>: SerialNumericOps> div(other: SerialNumericOps) = map2Values(other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, i -> + operator fun > div(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> if (!r.isConstant()) throw SerialLinearOpException("Cannot divide by a non-piecewise-constant linear equation (at time ${i.start})") LinearEquation(l.initialTime, l.initialValue / r.initialValue, l.rate / r.initialValue) - }) - /** [(DOC)][div] Calculates this divided by a contant number. */ + } + + /** [(DOC)][div] Calculates this divided by a constant number. */ operator fun div(n: Number) = div(Numbers(n)) /** @@ -82,13 +86,14 @@ interface SerialLinearOps>: SerialNumericOps> pow(exp: SerialNumericOps) = map2Values(exp.toSerialLinear(), BinaryOperation.combineOrNull { l, r, i -> + infix fun > pow(exp: SerialNumericOps) = map2Values(exp.toSerialLinear()) { l, r, i -> if (!r.isConstant()) throw SerialLinearOpException("Cannot apply a non-piecewise-constant exponent (at time ${i.start}") if (r.initialValue == 0.0) LinearEquation(1.0) else if (r.initialValue == 1.0) l else if (!l.isConstant()) throw SerialLinearOpException("Cannot apply an exponent to a non-piecewise-constant profile") else LinearEquation(l.initialValue.pow(r.initialValue)) - }) + } + /** [(DOC)][pow] Calculates this raised to the power of a constant number. */ infix fun pow(n: Number) = pow(Numbers(n)) @@ -123,7 +128,7 @@ interface SerialLinearOps>: SerialNumericOps> inequalityHelper(other: SerialNumericOps, f: LinearEquation.(LinearEquation) -> Booleans) = - flatMap2Values(::Booleans, other.toSerialLinear(), BinaryOperation.combineOrNull { l, r, _ -> l.f(r) }) + flatMap2Values(::Booleans, other.toSerialLinear()) { l, r, _ -> l.f(r) } override fun changes() = @@ -156,7 +161,7 @@ interface SerialLinearOps>: SerialNumericOps if (l.valueAt(i.start) == from) null else false }, { r, i -> if (r.valueAt(i.start) == to) null else false }, { l, r, i -> l.valueAt(i.start) == from && r.valueAt(i.start) == to } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt index 8c2761b6c4..e3bf348e0c 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt @@ -3,14 +3,14 @@ package gov.nasa.jpl.aerie.timeline.ops.numeric import gov.nasa.jpl.aerie.timeline.Duration import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.collections.profiles.Real -import gov.nasa.jpl.aerie.timeline.ops.SerialOps +import gov.nasa.jpl.aerie.timeline.ops.SerialSegmentOps import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation /** * Operations for profiles that represent numbers. */ -interface SerialNumericOps>: SerialOps, NumericOps { +interface SerialNumericOps>: SerialSegmentOps, NumericOps { /** [(DOC)][toSerialLinear] Converts the profile to a linear profile, a.k.a. [Real] (no-op if it already was linear). */ fun toSerialLinear(): Real diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt index 3b6cf0a82e..70a91dd9eb 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt @@ -21,7 +21,7 @@ interface SerialPrimitiveNumberOps> plus(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Numbers, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() + r.toDouble() else if (l is Float || r is Float) l.toFloat() + r.toFloat() else if (l is Long || r is Long) l.toLong() + r.toLong() @@ -29,7 +29,8 @@ interface SerialPrimitiveNumberOps> minus(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Numbers, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() - r.toDouble() else if (l is Float || r is Float) l.toFloat() - r.toFloat() else if (l is Long || r is Long) l.toLong() - r.toLong() @@ -45,7 +46,8 @@ interface SerialPrimitiveNumberOps> times(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Numbers, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() * r.toDouble() else if (l is Float || r is Float) l.toFloat() * r.toFloat() else if (l is Long || r is Long) l.toLong() * r.toLong() @@ -61,7 +63,8 @@ interface SerialPrimitiveNumberOps> div(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Numbers, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() / r.toDouble() else if (l is Float || r is Float) l.toFloat() / r.toFloat() else if (l is Long || r is Long) l.toLong() / r.toLong() @@ -77,7 +80,8 @@ interface SerialPrimitiveNumberOps> pow(exp: SerialPrimitiveNumberOps) = - map2Values(::Numbers, exp, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Numbers, exp) { l, r, _ -> l.toDouble().pow(r.toDouble()) - }) + } + /** [(DOC)][pow] Raises this to the power of a constant number. */ infix fun pow(n: Number) = pow(Numbers(n)) /** [(DOC)][pow] Raises this to the power of a linear profile. */ @@ -99,7 +104,7 @@ interface SerialPrimitiveNumberOps> lessThan(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Booleans, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() < r.toDouble() else if (l is Float || r is Float) l.toFloat() < r.toFloat() else if (l is Long || r is Long) l.toLong() < r.toLong() @@ -107,7 +112,8 @@ interface SerialPrimitiveNumberOps> lessThanOrEqualTo(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Booleans, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() <= r.toDouble() else if (l is Float || r is Float) l.toFloat() <= r.toFloat() else if (l is Long || r is Long) l.toLong() <= r.toLong() @@ -123,7 +129,8 @@ interface SerialPrimitiveNumberOps> greaterThan(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Booleans, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() > r.toDouble() else if (l is Float || r is Float) l.toFloat() > r.toFloat() else if (l is Long || r is Long) l.toLong() > r.toLong() @@ -139,7 +146,8 @@ interface SerialPrimitiveNumberOps r.toShort() else if (l is Byte || r is Byte) l.toByte() > r.toByte() else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - }) + } + /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a constant number. */ infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a linear profile. */ @@ -147,7 +155,7 @@ interface SerialPrimitiveNumberOps> greaterThanOrEqualTo(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other, BinaryOperation.combineOrNull { l, r, _ -> + map2Values(::Booleans, other) { l, r, _ -> if (l is Double || r is Double) l.toDouble() >= r.toDouble() else if (l is Float || r is Float) l.toFloat() >= r.toFloat() else if (l is Long || r is Long) l.toLong() >= r.toLong() @@ -155,7 +163,8 @@ interface SerialPrimitiveNumberOps= r.toShort() else if (l is Byte || r is Byte) l.toByte() >= r.toByte() else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - }) + } + /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ infix fun greaterThanOrEqual(n: Number) = greaterThanOrEqualTo(Numbers(n)) /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a linear profile. */ diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SegmentLists.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt similarity index 72% rename from timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SegmentLists.kt rename to timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt index 5fba7b4e66..32c3afcb23 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SegmentLists.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt @@ -1,7 +1,8 @@ package gov.nasa.jpl.aerie.timeline.util -import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.NullBinaryOperation import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.payloads.transpose @@ -24,20 +25,20 @@ import gov.nasa.jpl.aerie.timeline.payloads.transpose * This routine performs a single pass down each list, with a computational complexity * proportional to the total number of segments in both lists. */ -fun map2SegmentLists( - left: List>, - right: List>, - op: BinaryOperation -): List> { - val result = mutableListOf>() +fun map2SegmentLists( + left: List>, + right: List>, + op: NullBinaryOperation +): List> { + val result = mutableListOf>() var leftIndex = 0 var rightIndex = 0 - var leftSegment: Segment? - var rightSegment: Segment? - var remainingLeftSegment: Segment? = null - var remainingRightSegment: Segment? = null + var leftSegment: Segment? + var rightSegment: Segment? + var remainingLeftSegment: Segment? = null + var remainingRightSegment: Segment? = null while ( leftIndex < left.size || @@ -159,3 +160,53 @@ fun map2SegmentLists( return result } + +/** + * Low-level routine for performing a binary operation on a pair of parallel lists. + * + * The result is defined as follows: for every object `l` in [left] and every object `r` + * in [right], if their intervals overlap on an intersection `i`, the operation will be + * invoked with `(l, r, i)`. If the output of the operation is not `null`, it will be + * included in the resulting timeline. + * + * Always sorts both lists before performing the operation. + * + * @param left the left operand list + * @param right the right operand list + * @param op a binary operation between the payload types of the operand lists, which also + * accepts an intersection interval + */ +fun , RIGHT: IntervalLike, OUT: IntervalLike> map2ParallelLists( + left: List, + right: List, + isLeftSorted: Boolean, + isRightSorted: Boolean, + op: (LEFT, RIGHT, Interval) -> OUT?, +): List { + val leftSorted = if (isLeftSorted) left else left.sorted() + val rightSorted = if (isRightSorted) right else right.sorted() + + var rightIndex = 0 + var rightLookaheadIndex: Int + + val result = mutableListOf() + for (leftObj in leftSorted) { + while (rightSorted[rightIndex].interval.compareEndToStart(leftObj.interval) == -1 && rightIndex < right.size) { + rightIndex++ + } + + if (rightIndex == right.size) break + + rightLookaheadIndex = rightIndex + while (leftObj.interval.compareEndToStart(rightSorted[rightLookaheadIndex].interval) != -1) { + val rightObj = right[rightLookaheadIndex] + val intersection = leftObj.interval intersection rightObj.interval + + if (!intersection.isEmpty()) op(leftObj, rightObj, intersection)?.let { result.add(it) } + + rightLookaheadIndex++ + if (rightLookaheadIndex == right.size) break + } + } + return result +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt index 7284b8d400..73309f16fb 100644 --- a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOpsTest.kt @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.timeline.ops -import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.NullBinaryOperation import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds import gov.nasa.jpl.aerie.timeline.Interval.Companion.at import gov.nasa.jpl.aerie.timeline.Interval.Companion.between @@ -39,7 +39,7 @@ class ParallelOpsTest { Segment(seconds(0) .. seconds(3), 2), Segment(seconds(4) .. seconds(6), 5), Segment(seconds(2) .. seconds(5), 3) - ).reduceIntoProfile(::Numbers, BinaryOperation.reduce( + ).reduceIntoProfile(::Numbers, NullBinaryOperation.reduce( { seg, _ -> seg.value }, { seg, acc, _ -> seg.value + acc } )).collect() diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOpsTest.kt similarity index 93% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOpsTest.kt rename to timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOpsTest.kt index fd9fb62627..2ebb511132 100644 --- a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOpsTest.kt +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOpsTest.kt @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.timeline.ops -import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.NullBinaryOperation import gov.nasa.jpl.aerie.timeline.Duration.Companion.milliseconds import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds import gov.nasa.jpl.aerie.timeline.Interval.Companion.at @@ -13,7 +13,7 @@ import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants import org.junit.jupiter.api.Assertions.assertIterableEquals import org.junit.jupiter.api.Test -class SerialOpsTest { +class SerialSegmentOpsTest { // set, assignGaps, and map2Values are not tested here because they are trivial delegations to map2Serial. // see Map2SerialTest.kt @@ -25,7 +25,7 @@ class SerialOpsTest { Segment(seconds(1) .. seconds(2), "oooo"), Segment(between(seconds(2), seconds(3), Exclusive), "aaaa"), Segment(seconds(5) .. seconds(6), "ao") - ).detectEdges(BinaryOperation.cases( + ).detectEdges(NullBinaryOperation.cases( { l, _ -> l.endsWith('o') }, { r, _ -> r.startsWith('o') }, { l, r, _ -> l.endsWith(r.first()) } diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SerialTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2Test.kt similarity index 95% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SerialTest.kt rename to timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2Test.kt index 608d029656..9b5cf21882 100644 --- a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2SerialTest.kt +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2Test.kt @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.timeline.util -import gov.nasa.jpl.aerie.timeline.BinaryOperation +import gov.nasa.jpl.aerie.timeline.NullBinaryOperation import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds import gov.nasa.jpl.aerie.timeline.Interval import gov.nasa.jpl.aerie.timeline.Interval.Companion.between @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested -class Map2SerialTest { +class Map2Test { @Test fun basicCombineOrIdentity() { @@ -20,7 +20,7 @@ class Map2SerialTest { val result = map2SegmentLists( left, right, - BinaryOperation.combineOrIdentity { l, r, _ -> l + r} + NullBinaryOperation.combineOrIdentity { l, r, _ -> l + r} ) val expected = listOf( @@ -39,7 +39,7 @@ class Map2SerialTest { val result = map2SegmentLists( left, right, - BinaryOperation.combineOrNull { l, r, _ -> l + r} + NullBinaryOperation.combineOrNull { l, r, _ -> l + r} ) val expected = listOf( @@ -53,7 +53,7 @@ class Map2SerialTest { inner class SegmentAlignment { // Helper functions for below - val op = BinaryOperation.combineOrIdentity { l, r, _ -> l + r } + val op = NullBinaryOperation.combineOrIdentity { l, r, _ -> l + r } fun makeLeft(s: Long, e: Long, si: Interval.Inclusivity = Inclusive, ei: Interval.Inclusivity = Inclusive) = listOf(Segment(between(seconds(s), seconds(e), si, ei), -1)) From 9293e3dc4d9981c08b739884f628171e7aa8586e Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Mar 2024 16:08:44 -0800 Subject: [PATCH 112/159] Query activity IDs and fix computed attributes --- .../aerie/timeline/payloads/activities/AnyInstance.kt | 2 +- .../aerie/timeline/payloads/activities/Directive.kt | 5 ++++- .../aerie/timeline/payloads/activities/Instance.kt | 5 ++++- .../nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt | 11 +++++++---- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt index e2a3343c28..98d8fb9b95 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt @@ -5,5 +5,5 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue /** A general-purpose container for representing the arguments and computed attributes of any type of activity instance. */ data class AnyInstance( /***/ val arguments: Map, - /***/ val computedAttributes: Map + /***/ val computedAttributes: SerializedValue ) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt index 9291f7ea4b..721d9e93d7 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt @@ -11,6 +11,9 @@ data class Directive( /** The name of this specific directive. */ val name: String, + /** The directive id. */ + val id: Long, + override val type: String, override val startTime: Duration ): Activity> { @@ -18,7 +21,7 @@ data class Directive( get() = Interval.at(startTime) override fun withNewInterval(i: Interval): Directive { - if (i.isPoint()) return Directive(inner, name, type, i.start) + if (i.isPoint()) return Directive(inner, name, id, type, i.start) else throw Exception("Cannot change directive time to a non-instantaneous interval.") } } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt index cbcf62de7e..8b608c17d9 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt @@ -9,6 +9,9 @@ data class Instance( val inner: A, override val type: String, + /** The instance id. */ + val id: Long, + /** * The maybe-null id of the directive associated with this instance. * @@ -20,5 +23,5 @@ data class Instance( override val startTime: Duration get() = interval.start - override fun withNewInterval(i: Interval) = Instance(inner, type, directiveId, i) + override fun withNewInterval(i: Interval) = Instance(inner, type, id, directiveId, i) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt index d5467bebcb..171c30f523 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.timeline.BaseTimeline import gov.nasa.jpl.aerie.timeline.Duration import gov.nasa.jpl.aerie.timeline.Duration.Companion.minus import gov.nasa.jpl.aerie.timeline.Duration.Companion.plus +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at import gov.nasa.jpl.aerie.timeline.Interval.Companion.between import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.* import gov.nasa.jpl.aerie.timeline.payloads.Segment @@ -166,7 +167,7 @@ data class AeriePostgresPlan( } private val activityInstancesStatement = c.prepareStatement( - "select start_offset, duration, attributes, activity_type_name from simulated_activity where simulation_dataset_id = ?;" + "select start_offset, duration, attributes, activity_type_name, id from simulated_activity where simulation_dataset_id = ?;" ) override fun allActivityInstances(): Instances { activityInstancesStatement.clearParameters() @@ -176,17 +177,18 @@ data class AeriePostgresPlan( val result = mutableListOf>() while (response.next()) { val start = Duration.parseISO8601(response.getString(1)) + val id = response.getLong(5) val attributesString = response.getString(3) val attributes = parseJson(attributesString) val directiveId = attributes.asMap().getOrNull()?.get("directiveId")?.asInt()?.getOrNull() - ?: throw DatabaseError("Could not get directiveId from attributes: $attributesString") val arguments = attributes.asMap().getOrNull()!!["arguments"]?.asMap()?.getOrNull() ?: throw DatabaseError("Could not get arguments from attributes: $attributesString") - val computedAttributes = attributes.asMap().getOrNull()!!["computedAttributes"]?.asMap()?.getOrNull() + val computedAttributes = attributes.asMap().getOrNull()!!["computedAttributes"] ?: throw DatabaseError("Could not get computed attributes from attributes: $attributesString") result.add(Instance( AnyInstance(arguments, computedAttributes), response.getString(4), + id, directiveId, between(start, start.plus(Duration.parseISO8601(response.getString(2)))) )) @@ -195,7 +197,7 @@ data class AeriePostgresPlan( } private val activityDirectivesStatement = c.prepareStatement( - "select name, start_offset, type, arguments from activity_directive where plan_id = ?" + + "select name, start_offset, type, arguments, id from activity_directive where plan_id = ?" + " and start_offset > cast(? as interval) and start_offset < cast(? as interval);" ) override fun allActivityDirectives() = BaseTimeline(::Directives) { opts -> @@ -212,6 +214,7 @@ data class AeriePostgresPlan( parseJson(response.getString(4)).asMap().getOrNull()!! ), response.getString(1), + response.getLong(5), response.getString(3), Duration.parseISO8601(response.getString(2)) )) From 388ac3f0aa5b266e99b95a50286df10b83ecb85c Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Mar 2024 16:09:16 -0800 Subject: [PATCH 113/159] Allow filtering activities by type --- .../kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt index 18afe148de..fb5314106e 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ActivityOps.kt @@ -5,4 +5,10 @@ import gov.nasa.jpl.aerie.timeline.payloads.activities.Activity /** * Operations mixin for timelines of activities. */ -interface ActivityOps, THIS: ActivityOps>: GeneralOps +interface ActivityOps, THIS: ActivityOps>: ParallelOps { + /** Filters out all activities except those of a given vararg list of types. */ + fun filterByType(vararg types: String) = filterByType(types.asList()) + + /** Filters out all activities except those of a given list of types. */ + fun filterByType(types: List) = filter { it.type in types } +} From da53a11785c1cedef3128ff3269ecfca42663970 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Mar 2024 16:11:05 -0800 Subject: [PATCH 114/159] Simplify reduce number of applied mixins --- .../gov/nasa/jpl/aerie/timeline/collections/Directives.kt | 4 +--- .../gov/nasa/jpl/aerie/timeline/collections/Instances.kt | 4 +--- .../gov/nasa/jpl/aerie/timeline/collections/Intervals.kt | 3 +-- .../nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt | 3 +-- .../nasa/jpl/aerie/timeline/collections/profiles/Constants.kt | 1 - .../nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt | 1 - .../gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt | 3 +-- 7 files changed, 5 insertions(+), 14 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt index 063be169e1..b1192308f9 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt @@ -15,9 +15,7 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList */ data class Directives(private val timeline: Timeline, Directives>): Timeline, Directives> by timeline, - ParallelOps, Directives>, - DirectiveOps>, - CoalesceNoOp, Directives> + DirectiveOps> { constructor(vararg directives: Directive): this(directives.asList()) constructor(directives: List>): this(BaseTimeline(::Directives, preprocessList(directives))) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt index 1ad4e559cc..f434e73cde 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt @@ -14,9 +14,7 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList */ data class Instances(private val timeline: Timeline, Instances>): Timeline, Instances> by timeline, - ParallelOps, Instances>, - InstanceOps>, - CoalesceNoOp, Instances> + InstanceOps> { constructor(vararg instances: Instance): this(instances.asList()) constructor(instances: List>): this(BaseTimeline(::Instances, preprocessList(instances))) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt index 5efc811a61..18a0d698ba 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt @@ -12,8 +12,7 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList data class Intervals>(private val timeline: Timeline>): Timeline> by timeline, ParallelOps>, - NonZeroDurationOps>, - CoalesceNoOp> + NonZeroDurationOps> { constructor(vararg intervals: T): this(intervals.asList()) constructor(intervals: List): this(BaseTimeline(::Intervals, preprocessList(intervals))) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt index 0bbb1e8a15..f206355f81 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt @@ -12,8 +12,7 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList /** A profile of booleans. */ data class Booleans(private val timeline: Timeline, Booleans>): Timeline, Booleans> by timeline, - SerialBooleanOps, - CoalesceSegmentsOp + SerialBooleanOps { constructor(v: Boolean): this(Segment(Interval.MIN_MAX, v)) constructor(vararg segments: Segment): this(segments.asList()) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt index 029e81751d..4a56661570 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Constants.kt @@ -12,7 +12,6 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList /** A profile of piece-wise constant values. */ data class Constants(private val timeline: Timeline, Constants>): Timeline, Constants> by timeline, - CoalesceSegmentsOp>, SerialConstantOps> { constructor(v: V): this(Segment(Interval.MIN_MAX, v)) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt index 90857c7e0f..e1ffad3dc2 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt @@ -26,7 +26,6 @@ import java.lang.ArithmeticException */ data class Numbers(private val timeline: Timeline, Numbers>): Timeline, Numbers> by timeline, - CoalesceSegmentsOp>, SerialPrimitiveNumberOps> { constructor(v: N): this(Segment(Interval.MIN_MAX, v)) constructor(vararg segments: Segment): this(segments.asList()) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt index e903afdbab..117dbd8dd5 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt @@ -14,8 +14,7 @@ import kotlin.jvm.optionals.getOrNull /** A profile of [LinearEquations][LinearEquation]; a piece-wise linear real-number profile. */ data class Real(private val timeline: Timeline, Real>): Timeline, Real> by timeline, - SerialLinearOps, - CoalesceSegmentsOp + SerialLinearOps { constructor(v: Int): this(v.toDouble()) constructor(v: Long): this(v.toDouble()) From 99a8705ee966087599ca44a347169f1948efe38b Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Mar 2024 16:11:58 -0800 Subject: [PATCH 115/159] Implement Windows; a serial coalescing interval timeline --- .../jpl/aerie/timeline/collections/Windows.kt | 20 ++++++++++++ .../nasa/jpl/aerie/timeline/ops/BooleanOps.kt | 5 ++- .../jpl/aerie/timeline/ops/ConstantOps.kt | 6 ++++ .../nasa/jpl/aerie/timeline/ops/GeneralOps.kt | 31 +++++++++++++++++-- .../aerie/timeline/ops/SerialIntervalOps.kt | 30 ++++++++++++++++++ .../ops/coalesce/CoalesceIntervalsOp.kt | 10 ++++++ 6 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt create mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt new file mode 100644 index 0000000000..17ee6ee9d8 --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt @@ -0,0 +1,20 @@ +package gov.nasa.jpl.aerie.timeline.collections + +import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Timeline +import gov.nasa.jpl.aerie.timeline.ops.NonZeroDurationOps +import gov.nasa.jpl.aerie.timeline.ops.SerialIntervalOps +import gov.nasa.jpl.aerie.timeline.ops.SerialSegmentOps +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceIntervalsOp +import gov.nasa.jpl.aerie.timeline.util.preprocessList + +/** A coalescing timeline of [Intervals][Interval] with no extra data. */ +data class Windows(private val timeline: Timeline): + Timeline by timeline, + SerialIntervalOps, + NonZeroDurationOps +{ + constructor(vararg intervals: Interval): this(intervals.asList()) + constructor(intervals: List): this(BaseTimeline(::Windows, preprocessList(intervals))) +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt index eae4eb918f..e71ffa4bba 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/BooleanOps.kt @@ -23,9 +23,12 @@ interface BooleanOps>: ConstantOps { /** [(DOC)][falsifyLongerThan] Falsifies any `true` segments with durations longer than the given duration. */ fun falsifyLongerThan(dur: Duration) = falsifyByDuration(Duration.MIN_VALUE .. dur) - /** [(DOC)][isolateTrue] Creates an [Intervals] objects with intervals whenever this profile is `true`. */ + /** [(DOC)][isolateTrue] Creates an [Intervals] object with intervals whenever this profile is `true`. */ fun isolateTrue() = isolate { it.value } + /** [(DOC)][highlightTrue] Creates an [Windows] object that highlights whenever this profile is `true`. */ + fun highlightTrue() = highlight { it.value } + /** [(DOC)][splitTrue] Splits `true` segments into the given number of pieces (leaving `false` unchanged). */ fun splitTrue(numPieces: Int) = split { if (it.value) numPieces else 1 } } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt index 6fb1072aa6..64103ae35d 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ConstantOps.kt @@ -11,6 +11,12 @@ interface ConstantOps>: SegmentOps { */ fun isolateEqualTo(value: V) = isolate { it.value == value } + /** + * [(DOC)][highlightEqualTo] Highlights intervals where the value is equal to a specific value. + * @see [GeneralOps.highlight] + */ + fun highlightEqualTo(value: V) = highlight { it.value == value } + /** * [(DOC)][splitEqualTo] Splits segments where the value is equal to a specific value. * diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt index 93737e1461..5c09c62b5c 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt @@ -2,6 +2,7 @@ package gov.nasa.jpl.aerie.timeline.ops import gov.nasa.jpl.aerie.timeline.* import gov.nasa.jpl.aerie.timeline.collections.Intervals +import gov.nasa.jpl.aerie.timeline.collections.Windows import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.util.coalesceList @@ -119,12 +120,24 @@ interface GeneralOps, THIS: GeneralOps>: Timeline Boolean) = filter(false, f).unsafeCast(::Intervals) + /** + * [(DOC)][highlight] Similar to [filter], but produces a coalesced [Windows] object + * that highlights everything that satisfies the predicate. + * + * @param f a predicate that decides if a given payload object's interval should be included in the result. + */ + fun highlight(f: (V) -> Boolean) = + unsafeMap(::Intervals, BoundsTransformer.IDENTITY, false) { + if (f(it)) it.interval + else Interval.EMPTY + }.convert(::Windows) + /** [(DOC)][unsafeMap] **UNSAFE!** A simpler version of [unsafeMap] for operations that don't change the timeline type. */ fun unsafeMap(boundsTransformer: BoundsTransformer, truncate: Boolean, f: (V) -> V) = unsafeMap(ctor, boundsTransformer, truncate, f) @@ -238,6 +251,20 @@ interface GeneralOps, THIS: GeneralOps>: Timeline> filterByWindows(windows: SerialIntervalOps, truncateMarginal: Boolean = true) = + if (truncateMarginal) { + unsafeMap2(ctor, windows) { l, _, i -> l.withNewInterval(i) } + } else { + unsafeMap2(::Intervals, windows) { l, _, _ -> l }.unsafeOperate { collect(it).distinct() }.unsafeCast(ctor) + } + /** [(DOC)][shift] Uniformly shifts the entire timeline in time (positive shifts toward the future). */ fun shift(dur: Duration) = unsafeMapIntervals(BoundsTransformer.shift(dur), false) { it.interval.shiftBy(dur) } } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt new file mode 100644 index 0000000000..0f8b6030ae --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt @@ -0,0 +1,30 @@ +package gov.nasa.jpl.aerie.timeline.ops + +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.collections.Intervals +import gov.nasa.jpl.aerie.timeline.collections.Windows +import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceIntervalsOp +import gov.nasa.jpl.aerie.timeline.util.sorted + +/** Operations for coalescing intervals. */ +interface SerialIntervalOps>: SerialOps, CoalesceIntervalsOp { + + /** [(DOC)][union] Calculates the union of this and another [Windows]. */ + infix fun > union(other: SerialIntervalOps) = unsafeOperate { opts -> + val combined = collect(opts) + other.collect(opts) + combined.sorted() + } + + /** [(DOC)][intersection] Calculates the intersection of this and another [Windows]. */ + infix fun > intersection(other: SerialIntervalOps) = + unsafeMap2(::Windows, other) { _, _, i -> i } + + /** [(DOC)][complement] Calculates the complement; i.e. highlights everything that is not highlighted in this timeline. */ + fun complement() = unsafeOperate { opts -> + val result = mutableListOf(opts.bounds) + for (interval in collect(opts)) { + result += result.removeLast() - interval + } + result + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt new file mode 100644 index 0000000000..f3e6bacb4c --- /dev/null +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/coalesce/CoalesceIntervalsOp.kt @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.timeline.ops.coalesce + +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +import gov.nasa.jpl.aerie.timeline.ops.GeneralOps + +/** A coalesce operation for intervals, which always coalesce. */ +interface CoalesceIntervalsOp>: GeneralOps { + override fun shouldCoalesce() = { _: Interval, _: Interval -> true } +} From 811494a452e5daf8f3d55cf54ee8ed6dd221b536 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Fri, 1 Mar 2024 17:11:17 -0800 Subject: [PATCH 116/159] Remove unnecessary operations mixins --- .../aerie/timeline/collections/Directives.kt | 6 +- .../aerie/timeline/collections/Instances.kt | 2 +- .../jpl/aerie/timeline/collections/Windows.kt | 26 ++- .../timeline/collections/profiles/Booleans.kt | 106 ++++++++++- .../timeline/collections/profiles/Numbers.kt | 171 ++++++++++++++++- .../timeline/collections/profiles/Real.kt | 168 ++++++++++++++++- .../jpl/aerie/timeline/ops/DirectiveOps.kt | 10 - .../nasa/jpl/aerie/timeline/ops/GeneralOps.kt | 2 +- .../jpl/aerie/timeline/ops/InstanceOps.kt | 10 - .../nasa/jpl/aerie/timeline/ops/SegmentOps.kt | 50 +++++ .../aerie/timeline/ops/SerialBooleanOps.kt | 106 ----------- .../aerie/timeline/ops/SerialIntervalOps.kt | 30 --- .../nasa/jpl/aerie/timeline/ops/SerialOps.kt | 1 + .../aerie/timeline/ops/SerialSegmentOps.kt | 7 +- .../timeline/ops/numeric/SerialLinearOps.kt | 175 ------------------ .../timeline/ops/numeric/SerialNumericOps.kt | 4 +- .../ops/numeric/SerialPrimitiveNumberOps.kt | 172 ----------------- 17 files changed, 515 insertions(+), 531 deletions(-) delete mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt delete mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt delete mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt delete mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt delete mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt delete mode 100644 timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt index b1192308f9..18fd1255dd 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt @@ -2,10 +2,8 @@ package gov.nasa.jpl.aerie.timeline.collections import gov.nasa.jpl.aerie.timeline.BaseTimeline import gov.nasa.jpl.aerie.timeline.payloads.activities.Directive -import gov.nasa.jpl.aerie.timeline.ops.DirectiveOps -import gov.nasa.jpl.aerie.timeline.ops.ParallelOps import gov.nasa.jpl.aerie.timeline.Timeline -import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceNoOp +import gov.nasa.jpl.aerie.timeline.ops.ActivityOps import gov.nasa.jpl.aerie.timeline.util.preprocessList /** @@ -15,7 +13,7 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList */ data class Directives(private val timeline: Timeline, Directives>): Timeline, Directives> by timeline, - DirectiveOps> + ActivityOps, Directives> { constructor(vararg directives: Directive): this(directives.asList()) constructor(directives: List>): this(BaseTimeline(::Directives, preprocessList(directives))) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt index f434e73cde..9797220416 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt @@ -14,7 +14,7 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList */ data class Instances(private val timeline: Timeline, Instances>): Timeline, Instances> by timeline, - InstanceOps> + ActivityOps, Instances> { constructor(vararg instances: Instance): this(instances.asList()) constructor(instances: List>): this(BaseTimeline(::Instances, preprocessList(instances))) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt index 17ee6ee9d8..3cd165eba9 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt @@ -4,17 +4,37 @@ import gov.nasa.jpl.aerie.timeline.BaseTimeline import gov.nasa.jpl.aerie.timeline.Interval import gov.nasa.jpl.aerie.timeline.Timeline import gov.nasa.jpl.aerie.timeline.ops.NonZeroDurationOps -import gov.nasa.jpl.aerie.timeline.ops.SerialIntervalOps -import gov.nasa.jpl.aerie.timeline.ops.SerialSegmentOps +import gov.nasa.jpl.aerie.timeline.ops.SerialOps import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceIntervalsOp import gov.nasa.jpl.aerie.timeline.util.preprocessList +import gov.nasa.jpl.aerie.timeline.util.sorted /** A coalescing timeline of [Intervals][Interval] with no extra data. */ data class Windows(private val timeline: Timeline): Timeline by timeline, - SerialIntervalOps, + SerialOps, + CoalesceIntervalsOp, NonZeroDurationOps { constructor(vararg intervals: Interval): this(intervals.asList()) constructor(intervals: List): this(BaseTimeline(::Windows, preprocessList(intervals))) + + /** Calculates the union of this and another [Windows]. */ + infix fun union(other: Windows) = unsafeOperate { opts -> + val combined = collect(opts) + other.collect(opts) + combined.sorted() + } + + /** Calculates the intersection of this and another [Windows]. */ + infix fun intersection(other: Windows) = + unsafeMap2(::Windows, other) { _, _, i -> i } + + /** Calculates the complement; i.e. highlights everything that is not highlighted in this timeline. */ + fun complement() = unsafeOperate { opts -> + val result = mutableListOf(opts.bounds) + for (interval in collect(opts)) { + result += result.removeLast() - interval + } + result + } } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt index f206355f81..f8f48b777f 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Booleans.kt @@ -1,23 +1,117 @@ package gov.nasa.jpl.aerie.timeline.collections.profiles import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.ops.BooleanOps import gov.nasa.jpl.aerie.timeline.payloads.Segment -import gov.nasa.jpl.aerie.timeline.BaseTimeline -import gov.nasa.jpl.aerie.timeline.Interval -import gov.nasa.jpl.aerie.timeline.Timeline -import gov.nasa.jpl.aerie.timeline.ops.SerialBooleanOps -import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.ops.SerialConstantOps +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation import gov.nasa.jpl.aerie.timeline.util.preprocessList /** A profile of booleans. */ data class Booleans(private val timeline: Timeline, Booleans>): Timeline, Booleans> by timeline, - SerialBooleanOps + SerialConstantOps, + BooleanOps { constructor(v: Boolean): this(Segment(Interval.MIN_MAX, v)) constructor(vararg segments: Segment): this(segments.asList()) constructor(segments: List>): this(BaseTimeline(::Booleans, preprocessList(segments, Segment::valueEquals))) + /** Computes the AND operation between two boolean profiles. */ + infix fun and(other: Booleans) = map2OptionalValues(other, NullBinaryOperation.cases( + { l, _ -> if (l) null else false }, + { r, _ -> if (r) null else false }, + { l, r, _ -> l && r } + )) + + /** Computes the OR operation between two boolean profiles. */ + infix fun or(other: Booleans) = map2OptionalValues(other, NullBinaryOperation.cases( + { l, _ -> if (l) true else null }, + { r, _ -> if (r) true else null }, + { l, r, _ -> l || r } + )) + + /** Computes the XOR operation between two boolean profiles. */ + infix fun xor(other: Booleans) = map2Values(other) { l, r, _ -> l xor r } + + /** Computes the NOR operation between two boolean profiles. */ + infix fun nor(other: Booleans) = map2OptionalValues(other, NullBinaryOperation.cases( + { l, _ -> if (l) false else null }, + { r, _ -> if (r) false else null }, + { l, r, _ -> !(l || r) } + )) + + /** Computes the NAND operation between two boolean profiles. */ + infix fun nand(other: Booleans) = map2OptionalValues(other, NullBinaryOperation.cases( + { l, _ -> if (l) null else true }, + { r, _ -> if (r) null else true }, + { l, r, _ -> !(l && r) } + )) + + /** + * Shifts the rising and falling edges of a boolean profile independently of each other. + * + * This allows for segments to not just be shifted around, but stretched or squished, or even + * deleted. + * + * A rising edge is defined as the time just after a `false` segment ends - whether it meets a `true` + * segment or a gap. Similarly, a falling edge is just after a `true` segment ends. + * + * @param shiftRising duration to shift the rising edges by + * @param shiftFalling duration to shift the rising edges by + */ + fun shiftEdges(shiftRising: Duration, shiftFalling: Duration) = + unsafeMapIntervals( + { i -> + Interval.between( + Duration.min(i.start.saturatingMinus(shiftRising), i.start.saturatingMinus(shiftFalling)), + Duration.max(i.end.saturatingMinus(shiftRising), i.end.saturatingMinus(shiftFalling)), + i.startInclusivity, + i.endInclusivity + ) + }, + true + ) { t -> + if (t.value) t.interval.shiftBy(shiftRising, shiftFalling) + else t.interval.shiftBy(shiftFalling, shiftRising) + } + + /** + * Creates a Real profile corresponding to the running total of time + * that this profile has spent `true`. + * + * @param unit base unit of time to count. As in, the resulting real profile will increase by + * `1` for each `unit` duration spent in the `true` state. + * + * @see gov.nasa.jpl.aerie.timeline.ops.numeric.SerialNumericOps.integrate for further explanation of [unit]. + */ + fun accumulatedTrueDuration(unit: Duration) = + mapValues(::Real) { LinearEquation(if (it.value) 1.0 else 0.0) }.integrate(unit) + + /** + * Calculates the sum of durations of true segments in a range leading the current time. + * + * This returns a real profile that equals, at each time `t`, the duration of true segments in the interval `[t, t+range]`. + * + * Real profiles can't actually represent durations, only unitless numbers, so the result is actually calculated + * as a multiple of the provided [unit]. + * + * Because this is a serial profile, the duration of true segments in the look-ahead range can't exceed [range] itself. + * So the result is bounded by `[0, range/unit]` + * + * @param range how far into the future to look + * @param unit the time basis vector of the result; the unit of time that the result counts. + */ + fun rollingTrueDuration(range: Duration, unit: Duration) = + accumulatedTrueDuration(unit).shiftedDifference(range) + + /** Detects when this transitions from false to true. */ + fun risingEdges() = transitions(from = false, to = true) + + /** Detects when this transitions from false to true. */ + fun fallingEdges() = transitions(from = true, to = false) + /***/ companion object { /** * Converts a list of serialized value segments into a [Booleans] profile; diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt index e1ffad3dc2..558efce64b 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt @@ -5,10 +5,13 @@ import gov.nasa.jpl.aerie.timeline.BaseTimeline import gov.nasa.jpl.aerie.timeline.Interval import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.Timeline -import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp -import gov.nasa.jpl.aerie.timeline.ops.numeric.SerialPrimitiveNumberOps +import gov.nasa.jpl.aerie.timeline.ops.SerialConstantOps +import gov.nasa.jpl.aerie.timeline.ops.numeric.PrimitiveNumberOps +import gov.nasa.jpl.aerie.timeline.ops.numeric.SerialNumericOps +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation import gov.nasa.jpl.aerie.timeline.util.preprocessList import java.lang.ArithmeticException +import kotlin.math.pow /** * A profile of piece-wise constant numbers. @@ -26,11 +29,173 @@ import java.lang.ArithmeticException */ data class Numbers(private val timeline: Timeline, Numbers>): Timeline, Numbers> by timeline, - SerialPrimitiveNumberOps> { + SerialNumericOps>, + PrimitiveNumberOps>, + SerialConstantOps> +{ constructor(v: N): this(Segment(Interval.MIN_MAX, v)) constructor(vararg segments: Segment): this(segments.asList()) constructor(segments: List>): this(BaseTimeline(::Numbers, preprocessList(segments, Segment::valueEquals))) + override fun toSerialLinear() = mapValues(::Real) { LinearEquation(it.value.toDouble()) } + + /* + Due to the fact there is no superinterface for numbers that includes any arithmetic + or comparison operators, AFAICT this giant if-else statement needs to be copy-pasted + for each operation. If you can find a better way, please do. + */ + + /** Adds this and another primitive numeric profile. */ + operator fun plus(other: Numbers<*>) = + map2Values(::Numbers, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() + r.toDouble() + else if (l is Float || r is Float) l.toFloat() + r.toFloat() + else if (l is Long || r is Long) l.toLong() + r.toLong() + else if (l is Int || r is Int) l.toInt() + r.toInt() + else if (l is Short || r is Short) l.toShort() + r.toShort() + else if (l is Byte || r is Byte) l.toByte() + r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Adds this a constant number. */ + operator fun plus(n: Number) = plus(Numbers(n)) + /** Adds this and a linear profile. */ + operator fun plus(other: Real) = other + this + + /** Subtracts another primitive numeric profile from this. */ + operator fun minus(other: Numbers<*>) = + map2Values(::Numbers, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() - r.toDouble() + else if (l is Float || r is Float) l.toFloat() - r.toFloat() + else if (l is Long || r is Long) l.toLong() - r.toLong() + else if (l is Int || r is Int) l.toInt() - r.toInt() + else if (l is Short || r is Short) l.toShort() - r.toShort() + else if (l is Byte || r is Byte) l.toByte() - r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Subtracts a constant number from this. */ + operator fun minus(n: Number) = minus(Numbers(n)) + /** Subtracts a linear profile from this. */ + operator fun minus(other: Real) = -other + this + + /** Multiplies this and another primitive numeric profile. */ + operator fun times(other: Numbers<*>) = + map2Values(::Numbers, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() * r.toDouble() + else if (l is Float || r is Float) l.toFloat() * r.toFloat() + else if (l is Long || r is Long) l.toLong() * r.toLong() + else if (l is Int || r is Int) l.toInt() * r.toInt() + else if (l is Short || r is Short) l.toShort() * r.toShort() + else if (l is Byte || r is Byte) l.toByte() * r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Multiplies this by a constant number. */ + operator fun times(n: Number) = times(Numbers(n)) + /** Multiplies this by a linear profile. */ + operator fun times(other: Real) = other * this + + /** Calculates this divided by another primitive numeric profile. */ + operator fun div(other: Numbers<*>) = + map2Values(::Numbers, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() / r.toDouble() + else if (l is Float || r is Float) l.toFloat() / r.toFloat() + else if (l is Long || r is Long) l.toLong() / r.toLong() + else if (l is Int || r is Int) l.toInt() / r.toInt() + else if (l is Short || r is Short) l.toShort() / r.toShort() + else if (l is Byte || r is Byte) l.toByte() / r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Divides this by a constant number. */ + operator fun div(n: Number) = div(Numbers(n)) + /** Divides this by a linear profile. */ + operator fun div(other: Real) = this / other.toSerialPrimitiveNumbers("Cannot divide by a non-piecewise-constant divisor.") + + /** + * Calculates this raised to the power of another primitive numeric profile. + * + * Both profiles are converted to doubles first. + */ + infix fun pow(exp: Numbers<*>) = + map2Values(::Numbers, exp) { l, r, _ -> + l.toDouble().pow(r.toDouble()) + } + + /** Raises this to the power of a constant number. */ + infix fun pow(n: Number) = pow(Numbers(n)) + /** Raises this to the power of a linear profile. */ + infix fun pow(other: Real) = this pow other.toSerialPrimitiveNumbers("Cannot apply a non-piecewise-constant exponent.") + + /** Returns a [Booleans] that is true when this is less than another primitive numeric profile. */ + infix fun lessThan(other: Numbers<*>) = + map2Values(::Booleans, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() < r.toDouble() + else if (l is Float || r is Float) l.toFloat() < r.toFloat() + else if (l is Long || r is Long) l.toLong() < r.toLong() + else if (l is Int || r is Int) l.toInt() < r.toInt() + else if (l is Short || r is Short) l.toShort() < r.toShort() + else if (l is Byte || r is Byte) l.toByte() < r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Returns a [Booleans] that is true when this is less than a constant number. */ + infix fun lessThan(n: Number) = lessThan(Numbers(n)) + /** Returns a [Booleans] that is true when this is less than a linear profile. */ + infix fun lessThan(other: Real) = other greaterThan this + + /** Returns a [Booleans] that is true when this is less than or equal to another primitive numeric profile. */ + infix fun lessThanOrEqualTo(other: Numbers<*>) = + map2Values(::Booleans, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() <= r.toDouble() + else if (l is Float || r is Float) l.toFloat() <= r.toFloat() + else if (l is Long || r is Long) l.toLong() <= r.toLong() + else if (l is Int || r is Int) l.toInt() <= r.toInt() + else if (l is Short || r is Short) l.toShort() <= r.toShort() + else if (l is Byte || r is Byte) l.toByte() <= r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Returns a [Booleans] that is true when this is less than or equal to a constant number. */ + infix fun lessThanOrEqual(n: Number) = lessThanOrEqualTo(Numbers(n)) + /** Returns a [Booleans] that is true when this is less than or equal to a linear profile. */ + infix fun lessThanOrEqualTo(other: Real) = other greaterThanOrEqualTo this + + /** Returns a [Booleans] that is true when this is greater than another primitive numeric profile. */ + infix fun greaterThan(other: Numbers<*>) = + map2Values(::Booleans, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() > r.toDouble() + else if (l is Float || r is Float) l.toFloat() > r.toFloat() + else if (l is Long || r is Long) l.toLong() > r.toLong() + else if (l is Int || r is Int) l.toInt() > r.toInt() + else if (l is Short || r is Short) l.toShort() > r.toShort() + else if (l is Byte || r is Byte) l.toByte() > r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Returns a [Booleans] that is true when this is greater than a constant number. */ + infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) + /** Returns a [Booleans] that is true when this is greater than a linear profile. */ + infix fun greaterThan(other: Real) = other lessThan this + + /** Returns a [Booleans] that is true when this is greater than or equal to another primitive numeric profile. */ + infix fun greaterThanOrEqualTo(other: Numbers<*>) = + map2Values(::Booleans, other) { l, r, _ -> + if (l is Double || r is Double) l.toDouble() >= r.toDouble() + else if (l is Float || r is Float) l.toFloat() >= r.toFloat() + else if (l is Long || r is Long) l.toLong() >= r.toLong() + else if (l is Int || r is Int) l.toInt() >= r.toInt() + else if (l is Short || r is Short) l.toShort() >= r.toShort() + else if (l is Byte || r is Byte) l.toByte() >= r.toByte() + else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() + } + + /** Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ + infix fun greaterThanOrEqual(n: Number) = greaterThanOrEqualTo(Numbers(n)) + /** Returns a [Booleans] that is true when this is greater than or equal to a linear profile. */ + infix fun greaterThanOrEqualTo(other: Real) = other lessThanOrEqualTo this + /***/ companion object { /** * Converts a list of serialized value segments into a [Numbers] profile; diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt index 117dbd8dd5..d671b79676 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt @@ -1,20 +1,22 @@ package gov.nasa.jpl.aerie.timeline.collections.profiles import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.* +import gov.nasa.jpl.aerie.timeline.ops.numeric.LinearOps import gov.nasa.jpl.aerie.timeline.payloads.Segment -import gov.nasa.jpl.aerie.timeline.BaseTimeline -import gov.nasa.jpl.aerie.timeline.Interval -import gov.nasa.jpl.aerie.timeline.Timeline -import gov.nasa.jpl.aerie.timeline.ops.numeric.SerialLinearOps -import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceSegmentsOp +import gov.nasa.jpl.aerie.timeline.ops.numeric.SerialNumericOps import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import gov.nasa.jpl.aerie.timeline.payloads.transpose import gov.nasa.jpl.aerie.timeline.util.preprocessList +import gov.nasa.jpl.aerie.timeline.util.truncateList import kotlin.jvm.optionals.getOrNull +import kotlin.math.pow /** A profile of [LinearEquations][LinearEquation]; a piece-wise linear real-number profile. */ data class Real(private val timeline: Timeline, Real>): Timeline, Real> by timeline, - SerialLinearOps + SerialNumericOps, + LinearOps { constructor(v: Int): this(v.toDouble()) constructor(v: Long): this(v.toDouble()) @@ -23,6 +25,160 @@ data class Real(private val timeline: Timeline, Real>): constructor(vararg segments: Segment): this(segments.asList()) constructor(segments: List>): this(BaseTimeline(::Real, preprocessList(segments, Segment::valueEquals))) + override fun toSerialLinear() = unsafeCast(::Real) + override fun LinearEquation.toLinear() = this + + /** + * Converts this to a primitive number profile (i.e. [Numbers]), throwing an error if this is not piece-wise constant. + * + * @param message error message to throw if this is not piece-wise constant. + */ + fun toSerialPrimitiveNumbers(message: String? = null) = mapValues(::Numbers) { + if (it.value.isConstant()) it.value.initialValue + else if (message == null) throw RealOpException("Cannot convert a non-piecewise-constant linear equation to a constant number. (at time ${it.interval.start})") + else throw RealOpException("$message (at time ${it.interval.start})") + } + + /** Adds this and another numeric profile. */ + operator fun > plus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> + val shiftedRight = r.shiftInitialTime(l.initialTime) + LinearEquation(l.initialTime, l.initialValue + shiftedRight.initialValue, l.rate + r.rate) + } + + /** Adds a constant number to this. */ + operator fun plus(n: Number) = plus(Numbers(n)) + + /** Subtracts another numeric profile from this. */ + operator fun > minus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> + val shiftedRight = r.shiftInitialTime(l.initialTime) + LinearEquation(l.initialTime, l.initialValue - shiftedRight.initialValue, l.rate - r.rate) + } + + /** Subtracts a constant number from this. */ + operator fun minus(n: Number) = minus(Numbers(n)) + + /** + * Multiplies this and another numeric profile. + * + * @throws RealOpException if both profiles have non-zero rate at the same time. + */ + operator fun > times(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> + if (!l.isConstant() && !r.isConstant()) throw RealOpException("Cannot multiply two linear equations that are non-constant at the same time (at time ${i.start})") + val shiftedRight = r.shiftInitialTime(l.initialTime) + val newRate = l.rate * shiftedRight.initialValue + r.rate * l.initialValue + LinearEquation(l.initialTime, l.initialValue * shiftedRight.initialValue, newRate) + } + + /** Multiplies this by a constant number. */ + operator fun times(n: Number) = times(Numbers(n)) + + /** + * Calculates this divided by another numeric profile. + * + * @throws RealOpException if the divisor has a non-zero rate at any time that the dividend is defined. + */ + operator fun > div(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> + if (!r.isConstant()) throw RealOpException("Cannot divide by a non-piecewise-constant linear equation (at time ${i.start})") + LinearEquation(l.initialTime, l.initialValue / r.initialValue, l.rate / r.initialValue) + } + + /** Calculates this divided by a constant number. */ + operator fun div(n: Number) = div(Numbers(n)) + + /** + * Calculates this raised to the power of another numeric profile. + * + * @throws RealOpException if the exponent has a non-zero rate at any time that the base is defined, + * or if the base has a non-zero rate at any time that the exponent is defined and not + * either 0 or 1. + */ + infix fun > pow(exp: SerialNumericOps) = map2Values(exp.toSerialLinear()) { l, r, i -> + if (!r.isConstant()) throw RealOpException("Cannot apply a non-piecewise-constant exponent (at time ${i.start}") + if (r.initialValue == 0.0) LinearEquation(1.0) + else if (r.initialValue == 1.0) l + else if (!l.isConstant()) throw RealOpException("Cannot apply an exponent to a non-piecewise-constant profile") + else LinearEquation(l.initialValue.pow(r.initialValue)) + } + + /** Calculates this raised to the power of a constant number. */ + infix fun pow(n: Number) = pow(Numbers(n)) + + /** Returns a [Booleans] that is true when this and another numeric profile are equal. */ + infix fun > equalTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsEqualTo) + /** Returns a [Booleans] that is true when this equals a constant number. */ + infix fun equalTo(n: Number) = equalTo(Numbers(n)) + + /** Returns a [Booleans] that is true when this and another numeric profile are not equal. */ + infix fun > notEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsNotEqualTo) + /** Returns a [Booleans] that is true when this does not equal a constant number. */ + infix fun notEqualTo(n: Number) = notEqualTo(Numbers(n)) + + /** Returns a [Booleans] that is true when this is less than another numeric profile. */ + infix fun > lessThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThan) + /** Returns a [Booleans] that is true when this is less than a constant number. */ + infix fun lessThan(n: Number) = lessThan(Numbers(n)) + + /** Returns a [Booleans] that is true when this is less than or equal to another numeric profile. */ + infix fun > lessThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThanOrEqualTo) + /** Returns a [Booleans] that is true when this is less than or equal to a constant number. */ + infix fun lessThanOrEqualTo(n: Number) = lessThanOrEqualTo(Numbers(n)) + + /** Returns a [Booleans] that is true when this is greater than another numeric profile. */ + infix fun > greaterThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThan) + /** Returns a [Booleans] that is true when this is greater than a constant number. */ + infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) + + /** Returns a [Booleans] that is true when this is greater than or equal to another numeric profile. */ + infix fun > greaterThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThanOrEqualTo) + /** Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ + infix fun greaterThanOrEqualTo(n: Number) = greaterThanOrEqualTo(Numbers(n)) + + private fun > inequalityHelper(other: SerialNumericOps, f: LinearEquation.(LinearEquation) -> Booleans) = + flatMap2Values(::Booleans, other.toSerialLinear()) { l, r, _ -> l.f(r) } + + + override fun changes() = + unsafeOperate(::Booleans) { opts -> + val bounds = opts.bounds + var previous: Segment? = null + val result = collect(CollectOptions(bounds, false)).flatMap { currentSegment: Segment -> + val currentInterval = currentSegment.interval + val leftEdge = if ( + previous !== null && + previous!!.interval.compareEndToStart(currentInterval) == 0 && + currentInterval.includesStart() + ) { + previous!!.value.valueAt(currentInterval.start) == currentSegment.value.valueAt(currentInterval.start) + } else if (currentInterval.compareStarts(bounds) == 0) { + currentSegment.value.rate != 0.0 + } else { + null + } + previous = currentSegment + listOfNotNull( + Segment(Interval.at(currentInterval.start), leftEdge).transpose(), + Segment(Interval.between(currentInterval.start, currentInterval.end, Interval.Inclusivity.Exclusive), currentSegment.value.rate != 0.0) + ) + } + truncateList(result, opts) + } + + /** + * Returns a [Booleans] that is true whenever this discontinuously transitions between + * a specific pair of values, and false or gap everywhere else. + */ + fun transitions(from: Double, to: Double) = detectEdges(NullBinaryOperation.cases( + { l, i -> if (l.valueAt(i.start) == from) null else false }, + { r, i -> if (r.valueAt(i.start) == to) null else false }, + { l, r, i -> l.valueAt(i.start) == from && r.valueAt(i.start) == to } + )) + + /** + * An exception for linear profile operations; usually thrown in contexts that + * require one or more of the operands to be piecewise constant. + */ + class RealOpException(message: String): Exception(message) + /***/ companion object { /** * Converts a list of serialized value segments into a real profile; for use with [gov.nasa.jpl.aerie.timeline.plan.Plan.resource]. diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt deleted file mode 100644 index 8dde663ea4..0000000000 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/DirectiveOps.kt +++ /dev/null @@ -1,10 +0,0 @@ -package gov.nasa.jpl.aerie.timeline.ops - -import gov.nasa.jpl.aerie.timeline.payloads.activities.Directive - -/** - * Operations mixin for timelines of activity directives. - * - * Used for both generic directives, and specific directive types from the mission model. - */ -interface DirectiveOps>: ActivityOps, THIS> diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt index 5c09c62b5c..03afa059f4 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt @@ -258,7 +258,7 @@ interface GeneralOps, THIS: GeneralOps>: Timeline> filterByWindows(windows: SerialIntervalOps, truncateMarginal: Boolean = true) = + fun filterByWindows(windows: Windows, truncateMarginal: Boolean = true) = if (truncateMarginal) { unsafeMap2(ctor, windows) { l, _, i -> l.withNewInterval(i) } } else { diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt deleted file mode 100644 index 2734cf8ddb..0000000000 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/InstanceOps.kt +++ /dev/null @@ -1,10 +0,0 @@ -package gov.nasa.jpl.aerie.timeline.ops - -import gov.nasa.jpl.aerie.timeline.payloads.activities.Instance - -/** - * Operations mixin for timelines of activity instances. - * - * Used for both generic instances, and specific instance types from the mission model. - */ -interface InstanceOps>: ActivityOps, THIS>, NonZeroDurationOps, THIS> diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt index 7b40c7e899..df43ddb60e 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt @@ -49,14 +49,64 @@ interface SegmentOps>: NonZeroDurationOps, RESULT: SegmentOps> flatMapValues(ctor: (Timeline, RESULT>) -> RESULT, f: (Segment) -> NESTED) = unsafeFlatMap(ctor, IDENTITY, false) { it.mapValue(f) } + /** [(DOC)][map2Values] A simplified version of [map2Values] for operations that don't change the timeline type. */ fun > map2Values(other: SegmentOps, op: (V, V, Interval) -> V?) = map2Values(ctor, other, op) + /** + * [(DOC)][map2Values] Performs a local binary operation between two segment-valued timelines. + * + * The operation will be evaluated on each pair of segments that overlap, with their + * intersection supplied as the interval argument to the [operation][op]. The result of + * the operation is inserted in the result timeline at that intersection. + * + * The binary operation may return `null`, which indicates that the result profile should have + * a gap. + * + * The operation is "local", meaning that while the operation is allowed to know when it is + * being evaluated, it is not allowed to change where the result segment should be placed. + * For that, consider using [unsafeMap2] or (ideally) shifting the results in a separate operation. + * + * @param W the other operand's payload type + * @param OTHER the other operand's timeline type + * @param R the result's payload type + * @param RESULT the result's timeline type + * + * @param ctor the result timeline's constructor + * @param other the other operand timeline + * @param op a binary operation between the two payload types that produces a maybe-null result + * + * @return a new timeline of segments + */ fun , R: Any, RESULT: SegmentOps> map2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> R?) = unsafeMap2(ctor, other) { l, r, i -> op(l.value, r.value, i)?.let { Segment(i, it) } } + /** [(DOC)][flatMap2Values] A simpler version of [flatMap2Values] for operations that don't change the timeline type. */ fun , NESTED: SegmentOps> flatMap2Values(other: SegmentOps, op: (V, V, Interval) -> NESTED?) = flatMap2Values(ctor, other, op) + /** + * [(DOC)][flatMap2Values] Performs a local binary operation that produces profiles, and flattens it. + * + * Similar to [map2Values], except it expects the operation to return a profile. Each nested profile + * is then collected on the interval it corresponds to, and the results are concatenated into a single profile. + * + * This is useful for binary operations where at least one of the operand segments represents a value that + * varies within the segment, such as [gov.nasa.jpl.aerie.timeline.collections.profiles.Real]. + * + * @param W the other operand's payload type + * @param OTHER the other operand's timeline type + * @param R the result's payload type + * @param NESTED the nested profile type returned by the operation before flattening + * @param RESULT the result's timeline type + * + * @param ctor the result timeline's constructor + * @param other the other operand timeline + * @param op a binary operation between the two payload types that produces a maybe-null profile + * + * @return a coalesced flattened profile; an instance of the return type of [ctor] + * + * @see map2Values + */ fun , R: Any, NESTED: SegmentOps, RESULT: SegmentOps> flatMap2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> NESTED?) = unsafeOperate(ctor) { opts -> map2ParallelLists(collect(opts), other.collect(opts), isAlwaysSorted(), other.isAlwaysSorted()) { l, r, i -> diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt deleted file mode 100644 index 74539c7587..0000000000 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanOps.kt +++ /dev/null @@ -1,106 +0,0 @@ -package gov.nasa.jpl.aerie.timeline.ops - -import gov.nasa.jpl.aerie.timeline.NullBinaryOperation -import gov.nasa.jpl.aerie.timeline.Duration -import gov.nasa.jpl.aerie.timeline.Interval -import gov.nasa.jpl.aerie.timeline.collections.profiles.Real -import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation - -/** - * Operations mixin for timelines of booleans. - */ -interface SerialBooleanOps>: SerialConstantOps, BooleanOps { - /** [(DOC)][and] Computes the AND operation between two boolean profiles. */ - infix fun > and(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( - { l, _ -> if (l) null else false }, - { r, _ -> if (r) null else false }, - { l, r, _ -> l && r } - )) - - /** [(DOC)][or] Computes the OR operation between two boolean profiles. */ - infix fun > or(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( - { l, _ -> if (l) true else null }, - { r, _ -> if (r) true else null }, - { l, r, _ -> l || r } - )) - - /** [(DOC)][xor] Computes the XOR operation between two boolean profiles. */ - infix fun > xor(other: SerialBooleanOps) = map2Values(other) { l, r, _ -> l xor r } - - /** [(DOC)][nor] the NOR operation between two boolean profiles. */ - infix fun > nor(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( - { l, _ -> if (l) false else null }, - { r, _ -> if (r) false else null }, - { l, r, _ -> !(l || r) } - )) - - /** [(DOC)][nand] Computes the NAND operation between two boolean profiles. */ - infix fun > nand(other: SerialBooleanOps) = map2OptionalValues(other, NullBinaryOperation.cases( - { l, _ -> if (l) null else true }, - { r, _ -> if (r) null else true }, - { l, r, _ -> !(l && r) } - )) - - /** - * [(DOC)][shiftEdges] Shifts the rising and falling edges of a boolean profile independently of each other. - * - * This allows for segments to not just be shifted around, but stretched or squished, or even - * deleted. - * - * A rising edge is defined as the time just after a `false` segment ends - whether it meets a `true` - * segment or a gap. Similarly, a falling edge is just after a `true` segment ends. - * - * @param shiftRising duration to shift the rising edges by - * @param shiftFalling duration to shift the rising edges by - */ - fun shiftEdges(shiftRising: Duration, shiftFalling: Duration) = - unsafeMapIntervals( - { i -> - Interval.between( - Duration.min(i.start.saturatingMinus(shiftRising), i.start.saturatingMinus(shiftFalling)), - Duration.max(i.end.saturatingMinus(shiftRising), i.end.saturatingMinus(shiftFalling)), - i.startInclusivity, - i.endInclusivity - ) - }, - true - ) { t -> - if (t.value) t.interval.shiftBy(shiftRising, shiftFalling) - else t.interval.shiftBy(shiftFalling, shiftRising) - } - - /** - * [(DOC)][accumulatedTrueDuration] Creates a Real profile corresponding to the running total of time - * that this profile has spent `true`. - * - * @param unit base unit of time to count. As in, the resulting real profile will increase by - * `1` for each `unit` duration spent in the `true` state. - * - * @see gov.nasa.jpl.aerie.timeline.ops.numeric.SerialNumericOps.integrate for further explanation of [unit]. - */ - fun accumulatedTrueDuration(unit: Duration) = - mapValues(::Real) { LinearEquation(if (it.value) 1.0 else 0.0) }.integrate(unit) - - /** - * [(DOC)][rollingTrueDuration] Calculates the sum of durations of true segments in a range leading the current time. - * - * This returns a real profile that equals, at each time `t`, the duration of true segments in the interval `[t, t+range]`. - * - * Real profiles can't actually represent durations, only unitless numbers, so the result is actually calculated - * as a multiple of the provided [unit]. - * - * Because this is a serial profile, the duration of true segments in the look-ahead range can't exceed [range] itself. - * So the result is bounded by `[0, range/unit]` - * - * @param range how far into the future to look - * @param unit the time basis vector of the result; the unit of time that the result counts. - */ - fun rollingTrueDuration(range: Duration, unit: Duration) = - accumulatedTrueDuration(unit).shiftedDifference(range) - - /** [(DOC)][risingEdges] Detects when this transitions from false to true. */ - fun risingEdges() = transitions(from = false, to = true) - - /** [(DOC)][fallingEdges] Detects when this transitions from false to true. */ - fun fallingEdges() = transitions(from = true, to = false) -} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt deleted file mode 100644 index 0f8b6030ae..0000000000 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialIntervalOps.kt +++ /dev/null @@ -1,30 +0,0 @@ -package gov.nasa.jpl.aerie.timeline.ops - -import gov.nasa.jpl.aerie.timeline.* -import gov.nasa.jpl.aerie.timeline.collections.Intervals -import gov.nasa.jpl.aerie.timeline.collections.Windows -import gov.nasa.jpl.aerie.timeline.ops.coalesce.CoalesceIntervalsOp -import gov.nasa.jpl.aerie.timeline.util.sorted - -/** Operations for coalescing intervals. */ -interface SerialIntervalOps>: SerialOps, CoalesceIntervalsOp { - - /** [(DOC)][union] Calculates the union of this and another [Windows]. */ - infix fun > union(other: SerialIntervalOps) = unsafeOperate { opts -> - val combined = collect(opts) + other.collect(opts) - combined.sorted() - } - - /** [(DOC)][intersection] Calculates the intersection of this and another [Windows]. */ - infix fun > intersection(other: SerialIntervalOps) = - unsafeMap2(::Windows, other) { _, _, i -> i } - - /** [(DOC)][complement] Calculates the complement; i.e. highlights everything that is not highlighted in this timeline. */ - fun complement() = unsafeOperate { opts -> - val result = mutableListOf(opts.bounds) - for (interval in collect(opts)) { - result += result.removeLast() - interval - } - result - } -} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt index 40c749a73b..43f9c7ce7a 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialOps.kt @@ -2,6 +2,7 @@ package gov.nasa.jpl.aerie.timeline.ops import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike +/** An operations mixin for timelines whose payload objects are ordered and non-overlapping. */ interface SerialOps, THIS: SerialOps>: GeneralOps { override fun isAlwaysSorted() = true } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt index 283671f024..7be5e2e4d6 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt @@ -53,7 +53,7 @@ interface SerialSegmentOps>: SerialOps< * * The operation is "local", meaning that while the operation is allowed to know when it is * being evaluated, it is not allowed to change where the result segment should be placed. - * For that, you can use [unsafeMap], or more generally, [unsafeOperate]. + * For that, try to shift the results in a separate operation. * * @param W the other operand's payload type * @param OTHER the other operand's timeline type @@ -76,7 +76,8 @@ interface SerialSegmentOps>: SerialOps< fun , NESTED: SerialSegmentOps> flatMap2OptionalValues(other: SerialSegmentOps, op: NullBinaryOperation) = flatMap2OptionalValues(ctor, other, op) /** - * [(DOC)][flatMap2OptionalValues] Performs a local binary operation that produces profiles, and flattens it. + * [(DOC)][flatMap2OptionalValues] Performs a local binary operation that produces profiles, and flattens it, with + * special treatment of gaps. * * Similar to [map2OptionalValues], except it expects the [NullBinaryOperation] to return a profile. Each nested profile * is then collected on the interval it corresponds to, and the results are concatenated into a single profile. @@ -95,6 +96,8 @@ interface SerialSegmentOps>: SerialOps< * @param op a binary operation between the two payload types that produces a maybe-null profile * * @return a coalesced flattened profile; an instance of the return type of [ctor] + * + * @see map2OptionalValues */ fun , R: Any, NESTED: SerialSegmentOps, RESULT: GeneralOps, RESULT>> flatMap2OptionalValues(ctor: (Timeline, RESULT>) -> RESULT, other: SerialSegmentOps, op: NullBinaryOperation) = unsafeOperate(ctor) { opts -> diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt deleted file mode 100644 index b29117e9c7..0000000000 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialLinearOps.kt +++ /dev/null @@ -1,175 +0,0 @@ -package gov.nasa.jpl.aerie.timeline.ops.numeric - -import gov.nasa.jpl.aerie.timeline.* -import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive -import gov.nasa.jpl.aerie.timeline.collections.profiles.Real -import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans -import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers -import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation -import gov.nasa.jpl.aerie.timeline.payloads.Segment -import gov.nasa.jpl.aerie.timeline.payloads.transpose -import gov.nasa.jpl.aerie.timeline.util.truncateList -import kotlin.math.pow - -/** - * Operations mixin for segment-valued profiles whose payloads - * represent continuous, piecewise linear values. - * - * Currently only used for Real profiles, but in the future could be refactored for - * duration profiles or parallel real profiles. - */ -interface SerialLinearOps>: SerialNumericOps, LinearOps { - override fun toSerialLinear() = unsafeCast(::Real) - override fun LinearEquation.toLinear() = this - - /** - * Converts this to a primitive number profile (i.e. [Numbers]), throwing an error if this is not piece-wise constant. - * - * @param message error message to throw if this is not piece-wise constant. - */ - fun toSerialPrimitiveNumbers(message: String? = null) = mapValues(::Numbers) { - if (it.value.isConstant()) it.value.initialValue - else if (message == null) throw SerialLinearOpException("Cannot convert a non-piecewise-constant linear equation to a constant number. (at time ${it.interval.start})") - else throw SerialLinearOpException("$message (at time ${it.interval.start})") - } - - /** [(DOC)][plus] Adds this and another numeric profile. */ - operator fun > plus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> - val shiftedRight = r.shiftInitialTime(l.initialTime) - LinearEquation(l.initialTime, l.initialValue + shiftedRight.initialValue, l.rate + r.rate) - } - - /** [(DOC)][plus] Adds a constant number to this. */ - operator fun plus(n: Number) = plus(Numbers(n)) - - /** [(DOC)][minus] Subtracts another numeric profile from this. */ - operator fun > minus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> - val shiftedRight = r.shiftInitialTime(l.initialTime) - LinearEquation(l.initialTime, l.initialValue - shiftedRight.initialValue, l.rate - r.rate) - } - - /** [(DOC)][minus] Subtracts a constant number from this. */ - operator fun minus(n: Number) = minus(Numbers(n)) - - /** - * [(DOC)][times] Multiplies this and another numeric profile. - * - * @throws SerialLinearOpException if both profiles have non-zero rate at the same time. - */ - operator fun > times(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> - if (!l.isConstant() && !r.isConstant()) throw SerialLinearOpException("Cannot multiply two linear equations that are non-constant at the same time (at time ${i.start})") - val shiftedRight = r.shiftInitialTime(l.initialTime) - val newRate = l.rate * shiftedRight.initialValue + r.rate * l.initialValue - LinearEquation(l.initialTime, l.initialValue * shiftedRight.initialValue, newRate) - } - - /** [(DOC)][times] Multiplies this by a constant number. */ - operator fun times(n: Number) = times(Numbers(n)) - - /** - * [(DOC)][div] Calculates this divided by another numeric profile. - * - * @throws SerialLinearOpException if the divisor has a non-zero rate at any time that the dividend is defined. - */ - operator fun > div(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> - if (!r.isConstant()) throw SerialLinearOpException("Cannot divide by a non-piecewise-constant linear equation (at time ${i.start})") - LinearEquation(l.initialTime, l.initialValue / r.initialValue, l.rate / r.initialValue) - } - - /** [(DOC)][div] Calculates this divided by a constant number. */ - operator fun div(n: Number) = div(Numbers(n)) - - /** - * [(DOC)][pow] Calculates this raised to the power of another numeric profile. - * - * @throws SerialLinearOpException if the exponent has a non-zero rate at any time that the base is defined, - * or if the base has a non-zero rate at any time that the exponent is defined and not - * either 0 or 1. - */ - infix fun > pow(exp: SerialNumericOps) = map2Values(exp.toSerialLinear()) { l, r, i -> - if (!r.isConstant()) throw SerialLinearOpException("Cannot apply a non-piecewise-constant exponent (at time ${i.start}") - if (r.initialValue == 0.0) LinearEquation(1.0) - else if (r.initialValue == 1.0) l - else if (!l.isConstant()) throw SerialLinearOpException("Cannot apply an exponent to a non-piecewise-constant profile") - else LinearEquation(l.initialValue.pow(r.initialValue)) - } - - /** [(DOC)][pow] Calculates this raised to the power of a constant number. */ - infix fun pow(n: Number) = pow(Numbers(n)) - - /** [(DOC)][equalTo] Returns a [Booleans] that is true when this and another numeric profile are equal. */ - infix fun > equalTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsEqualTo) - /** [(DOC)][equalTo] Returns a [Booleans] that is true when this equals a constant number. */ - infix fun equalTo(n: Number) = equalTo(Numbers(n)) - - /** [(DOC)][notEqualTo] Returns a [Booleans] that is true when this and another numeric profile are not equal. */ - infix fun > notEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsNotEqualTo) - /** [(DOC)][notEqualTo] Returns a [Booleans] that is true when this does not equal a constant number. */ - infix fun notEqualTo(n: Number) = notEqualTo(Numbers(n)) - - /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than another numeric profile. */ - infix fun > lessThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThan) - /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than a constant number. */ - infix fun lessThan(n: Number) = lessThan(Numbers(n)) - - /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to another numeric profile. */ - infix fun > lessThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThanOrEqualTo) - /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to a constant number. */ - infix fun lessThanOrEqualTo(n: Number) = lessThanOrEqualTo(Numbers(n)) - - /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than another numeric profile. */ - infix fun > greaterThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThan) - /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a constant number. */ - infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) - - /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to another numeric profile. */ - infix fun > greaterThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThanOrEqualTo) - /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ - infix fun greaterThanOrEqualTo(n: Number) = greaterThanOrEqualTo(Numbers(n)) - - private fun > inequalityHelper(other: SerialNumericOps, f: LinearEquation.(LinearEquation) -> Booleans) = - flatMap2Values(::Booleans, other.toSerialLinear()) { l, r, _ -> l.f(r) } - - - override fun changes() = - unsafeOperate(::Booleans) { opts -> - val bounds = opts.bounds - var previous: Segment? = null - val result = collect(CollectOptions(bounds, false)).flatMap { currentSegment: Segment -> - val currentInterval = currentSegment.interval - val leftEdge = if ( - previous !== null && - previous!!.interval.compareEndToStart(currentInterval) == 0 && - currentInterval.includesStart() - ) { - previous!!.value.valueAt(currentInterval.start) == currentSegment.value.valueAt(currentInterval.start) - } else if (currentInterval.compareStarts(bounds) == 0) { - currentSegment.value.rate != 0.0 - } else { - null - } - previous = currentSegment - listOfNotNull( - Segment(Interval.at(currentInterval.start), leftEdge).transpose(), - Segment(Interval.between(currentInterval.start, currentInterval.end, Exclusive), currentSegment.value.rate != 0.0) - ) - } - truncateList(result, opts) - } - - /** - * [(DOC)][transitions] Returns a [Booleans] that is true whenever this discontinuously transitions between - * a specific pair of values, and false or gap everywhere else. - */ - fun transitions(from: Double, to: Double) = detectEdges(NullBinaryOperation.cases( - { l, i -> if (l.valueAt(i.start) == from) null else false }, - { r, i -> if (r.valueAt(i.start) == to) null else false }, - { l, r, i -> l.valueAt(i.start) == from && r.valueAt(i.start) == to } - )) - - /** - * An exception for linear profile operations; usually thrown in contexts that - * require one or more of the operands to be piecewise constant. - */ - class SerialLinearOpException(message: String): Exception(message) -} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt index e3bf348e0c..beec3d69e6 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt @@ -36,9 +36,9 @@ interface SerialNumericOps>: SerialSegme var acc = 0.0 for (segment in segments) { if (previousTime < segment.interval.start) - throw SerialLinearOps.SerialLinearOpException("Cannot integrate a linear profile that has gaps (time $previousTime") + throw Real.RealOpException("Cannot integrate a linear profile that has gaps (time $previousTime") if (!segment.value.isConstant()) - throw SerialLinearOps.SerialLinearOpException("Cannot integrate a non-piecewise-constant linear profile (time $previousTime") + throw Real.RealOpException("Cannot integrate a non-piecewise-constant linear profile (time $previousTime") val rate = segment.value.initialValue * baseRate val nextAcc = acc + rate * segment.interval.duration().ratioOver(Duration.SECOND) result.add(Segment(segment.interval, LinearEquation(previousTime, acc, rate))) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt deleted file mode 100644 index 70a91dd9eb..0000000000 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialPrimitiveNumberOps.kt +++ /dev/null @@ -1,172 +0,0 @@ -package gov.nasa.jpl.aerie.timeline.ops.numeric - -import gov.nasa.jpl.aerie.timeline.* -import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers -import gov.nasa.jpl.aerie.timeline.collections.profiles.Real -import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans -import gov.nasa.jpl.aerie.timeline.ops.SerialConstantOps -import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation -import kotlin.math.pow - -/** Operations for profiles of primitive numbers. */ -interface SerialPrimitiveNumberOps>: SerialNumericOps, PrimitiveNumberOps, SerialConstantOps { - override fun toSerialLinear() = mapValues(::Real) { LinearEquation(it.value.toDouble()) } - - - /* - Due to the fact there is no superinterface for numbers that includes any arithmetic - or comparison operators, AFAICT this giant if-else statement needs to be copy-pasted - for each operation. If you can find a better way, please do. - */ - - /** [(DOC)][plus] Adds this and another primitive numeric profile. */ - operator fun > plus(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() + r.toDouble() - else if (l is Float || r is Float) l.toFloat() + r.toFloat() - else if (l is Long || r is Long) l.toLong() + r.toLong() - else if (l is Int || r is Int) l.toInt() + r.toInt() - else if (l is Short || r is Short) l.toShort() + r.toShort() - else if (l is Byte || r is Byte) l.toByte() + r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][plus] Adds this a constant number. */ - operator fun plus(n: Number) = plus(Numbers(n)) - /** [(DOC)][plus] Adds this and a linear profile. */ - operator fun > plus(other: SerialLinearOps) = other + this - - /** [(DOC)][minus] Subtracts another primitive numeric profile from this. */ - operator fun > minus(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() - r.toDouble() - else if (l is Float || r is Float) l.toFloat() - r.toFloat() - else if (l is Long || r is Long) l.toLong() - r.toLong() - else if (l is Int || r is Int) l.toInt() - r.toInt() - else if (l is Short || r is Short) l.toShort() - r.toShort() - else if (l is Byte || r is Byte) l.toByte() - r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][minus] Subtracts a constant number from this. */ - operator fun minus(n: Number) = minus(Numbers(n)) - /** [(DOC)][minus] Subtracts a linear profile from this. */ - operator fun > minus(other: SerialLinearOps) = -other + this - - /** [(DOC)][times] Multiplies this and another primitive numeric profile. */ - operator fun > times(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() * r.toDouble() - else if (l is Float || r is Float) l.toFloat() * r.toFloat() - else if (l is Long || r is Long) l.toLong() * r.toLong() - else if (l is Int || r is Int) l.toInt() * r.toInt() - else if (l is Short || r is Short) l.toShort() * r.toShort() - else if (l is Byte || r is Byte) l.toByte() * r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][times] Multiplies this by a constant number. */ - operator fun times(n: Number) = times(Numbers(n)) - /** [(DOC)][times] Multiplies this by a linear profile. */ - operator fun > times(other: SerialLinearOps) = other * this - - /** [(DOC)][div] Calculates this divided by another primitive numeric profile. */ - operator fun > div(other: SerialPrimitiveNumberOps) = - map2Values(::Numbers, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() / r.toDouble() - else if (l is Float || r is Float) l.toFloat() / r.toFloat() - else if (l is Long || r is Long) l.toLong() / r.toLong() - else if (l is Int || r is Int) l.toInt() / r.toInt() - else if (l is Short || r is Short) l.toShort() / r.toShort() - else if (l is Byte || r is Byte) l.toByte() / r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][div] Divides this by a constant number. */ - operator fun div(n: Number) = div(Numbers(n)) - /** [(DOC)][div] Divides this by a linear profile. */ - operator fun > div(other: SerialLinearOps) = this / other.toSerialPrimitiveNumbers("Cannot divide by a non-piecewise-constant divisor.") - - /** - * [(DOC)][pow] Calculates this raised to the power of another primitive numeric profile. - * - * Both profiles are converted to doubles first. - */ - infix fun > pow(exp: SerialPrimitiveNumberOps) = - map2Values(::Numbers, exp) { l, r, _ -> - l.toDouble().pow(r.toDouble()) - } - - /** [(DOC)][pow] Raises this to the power of a constant number. */ - infix fun pow(n: Number) = pow(Numbers(n)) - /** [(DOC)][pow] Raises this to the power of a linear profile. */ - infix fun > pow(other: SerialLinearOps) = this pow other.toSerialPrimitiveNumbers("Cannot apply a non-piecewise-constant exponent.") - - /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than another primitive numeric profile. */ - infix fun > lessThan(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() < r.toDouble() - else if (l is Float || r is Float) l.toFloat() < r.toFloat() - else if (l is Long || r is Long) l.toLong() < r.toLong() - else if (l is Int || r is Int) l.toInt() < r.toInt() - else if (l is Short || r is Short) l.toShort() < r.toShort() - else if (l is Byte || r is Byte) l.toByte() < r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than a constant number. */ - infix fun lessThan(n: Number) = lessThan(Numbers(n)) - /** [(DOC)][lessThan] Returns a [Booleans] that is true when this is less than a linear profile. */ - infix fun > lessThan(other: SerialLinearOps) = other greaterThan this - - /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to another primitive numeric profile. */ - infix fun > lessThanOrEqualTo(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() <= r.toDouble() - else if (l is Float || r is Float) l.toFloat() <= r.toFloat() - else if (l is Long || r is Long) l.toLong() <= r.toLong() - else if (l is Int || r is Int) l.toInt() <= r.toInt() - else if (l is Short || r is Short) l.toShort() <= r.toShort() - else if (l is Byte || r is Byte) l.toByte() <= r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to a constant number. */ - infix fun lessThanOrEqual(n: Number) = lessThanOrEqualTo(Numbers(n)) - /** [(DOC)][lessThanOrEqualTo] Returns a [Booleans] that is true when this is less than or equal to a linear profile. */ - infix fun > lessThanOrEqualTo(other: SerialLinearOps) = other greaterThanOrEqualTo this - - /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than another primitive numeric profile. */ - infix fun > greaterThan(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() > r.toDouble() - else if (l is Float || r is Float) l.toFloat() > r.toFloat() - else if (l is Long || r is Long) l.toLong() > r.toLong() - else if (l is Int || r is Int) l.toInt() > r.toInt() - else if (l is Short || r is Short) l.toShort() > r.toShort() - else if (l is Byte || r is Byte) l.toByte() > r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a constant number. */ - infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) - /** [(DOC)][greaterThan] Returns a [Booleans] that is true when this is greater than a linear profile. */ - infix fun > greaterThan(other: SerialLinearOps) = other lessThan this - - /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to another primitive numeric profile. */ - infix fun > greaterThanOrEqualTo(other: SerialPrimitiveNumberOps) = - map2Values(::Booleans, other) { l, r, _ -> - if (l is Double || r is Double) l.toDouble() >= r.toDouble() - else if (l is Float || r is Float) l.toFloat() >= r.toFloat() - else if (l is Long || r is Long) l.toLong() >= r.toLong() - else if (l is Int || r is Int) l.toInt() >= r.toInt() - else if (l is Short || r is Short) l.toShort() >= r.toShort() - else if (l is Byte || r is Byte) l.toByte() >= r.toByte() - else throw PrimitiveNumberOps.UnreachablePrimitiveNumberException() - } - - /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ - infix fun greaterThanOrEqual(n: Number) = greaterThanOrEqualTo(Numbers(n)) - /** [(DOC)][greaterThanOrEqualTo] Returns a [Booleans] that is true when this is greater than or equal to a linear profile. */ - infix fun > greaterThanOrEqualTo(other: SerialLinearOps) = other lessThanOrEqualTo this -} From 073e124742135432336768acee07e0de0b33401b Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 01:10:48 -0800 Subject: [PATCH 117/159] Add NonZeroDurationOps to instances --- .../kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt index 9797220416..4e0c373b68 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt @@ -14,6 +14,7 @@ import gov.nasa.jpl.aerie.timeline.util.preprocessList */ data class Instances(private val timeline: Timeline, Instances>): Timeline, Instances> by timeline, + NonZeroDurationOps, Instances>, ActivityOps, Instances> { constructor(vararg instances: Instance): this(instances.asList()) From cd974fd593f427ef974c3a390abfeb5e27a729be Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 11:05:19 -0800 Subject: [PATCH 118/159] Add recursive IntervalLike generic requirement --- .../kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt | 2 +- .../gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt index 02f3c7647e..b6e120e1ec 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/IntervalLike.kt @@ -7,7 +7,7 @@ import gov.nasa.jpl.aerie.timeline.Interval * * For example, profile segments, activity instances, and plain intervals are all interval-like. */ -interface IntervalLike { +interface IntervalLike> { /** The interval this occupies on the timeline. */ val interval: Interval diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt index 73e62e6347..69a4a85868 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Activity.kt @@ -4,7 +4,7 @@ import gov.nasa.jpl.aerie.timeline.Duration import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike /** Unifying interface for activity instances and directives. */ -interface Activity: IntervalLike { +interface Activity>: IntervalLike { /** String type name of the activity. */ val type: String From d89f45b42f7868dcf4b83a6dc582bc66e2718558 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 15:42:01 -0800 Subject: [PATCH 119/159] Make preprocessList coalesce argument explicit --- .../gov/nasa/jpl/aerie/timeline/collections/Directives.kt | 2 +- .../kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt | 2 +- .../kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt | 2 +- .../kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt | 2 +- .../main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt index 18fd1255dd..05ae361d5f 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Directives.kt @@ -16,5 +16,5 @@ data class Directives(private val timeline: Timeline, Direc ActivityOps, Directives> { constructor(vararg directives: Directive): this(directives.asList()) - constructor(directives: List>): this(BaseTimeline(::Directives, preprocessList(directives))) + constructor(directives: List>): this(BaseTimeline(::Directives, preprocessList(directives, null))) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt index 4e0c373b68..4779f413be 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Instances.kt @@ -18,5 +18,5 @@ data class Instances(private val timeline: Timeline, Instanc ActivityOps, Instances> { constructor(vararg instances: Instance): this(instances.asList()) - constructor(instances: List>): this(BaseTimeline(::Instances, preprocessList(instances))) + constructor(instances: List>): this(BaseTimeline(::Instances, preprocessList(instances, null))) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt index 18a0d698ba..a0dba2cd53 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Intervals.kt @@ -15,5 +15,5 @@ data class Intervals>(private val timeline: Timeline> { constructor(vararg intervals: T): this(intervals.asList()) - constructor(intervals: List): this(BaseTimeline(::Intervals, preprocessList(intervals))) + constructor(intervals: List): this(BaseTimeline(::Intervals, preprocessList(intervals, null))) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt index 3cd165eba9..4b03e84b52 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/Windows.kt @@ -17,7 +17,7 @@ data class Windows(private val timeline: Timeline): NonZeroDurationOps { constructor(vararg intervals: Interval): this(intervals.asList()) - constructor(intervals: List): this(BaseTimeline(::Windows, preprocessList(intervals))) + constructor(intervals: List): this(BaseTimeline(::Windows, preprocessList(intervals) { true })) /** Calculates the union of this and another [Windows]. */ infix fun union(other: Windows) = unsafeOperate { opts -> diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt index f345397d0b..189f7f2967 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/ListUtils.kt @@ -17,7 +17,7 @@ import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike * * @return a collect closure that produces a sorted, possibly coalesced, and bounded list */ -fun > preprocessList(list: List, shouldCoalesce: (V.(V) -> Boolean)? = null): (CollectOptions) -> List { +fun > preprocessList(list: List, shouldCoalesce: (V.(V) -> Boolean)?): (CollectOptions) -> List { val sorted = list.sorted() val coalesced = maybeCoalesce(sorted, shouldCoalesce) return listCollector(coalesced) From 3f679fcd3784346f56deea8528bcaa3aed7038ac Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 15:43:51 -0800 Subject: [PATCH 120/159] Remove unnecessary generic arguments --- .../timeline/collections/profiles/Real.kt | 24 +++++++++---------- .../nasa/jpl/aerie/timeline/ops/GeneralOps.kt | 5 ++-- .../jpl/aerie/timeline/ops/ParallelOps.kt | 6 ++--- .../nasa/jpl/aerie/timeline/ops/SegmentOps.kt | 16 +++++-------- .../aerie/timeline/ops/SerialConstantOps.kt | 4 ++-- .../aerie/timeline/ops/SerialSegmentOps.kt | 17 ++++++------- 6 files changed, 32 insertions(+), 40 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt index d671b79676..922d533e7f 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt @@ -40,7 +40,7 @@ data class Real(private val timeline: Timeline, Real>): } /** Adds this and another numeric profile. */ - operator fun > plus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> + operator fun plus(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, _ -> val shiftedRight = r.shiftInitialTime(l.initialTime) LinearEquation(l.initialTime, l.initialValue + shiftedRight.initialValue, l.rate + r.rate) } @@ -49,7 +49,7 @@ data class Real(private val timeline: Timeline, Real>): operator fun plus(n: Number) = plus(Numbers(n)) /** Subtracts another numeric profile from this. */ - operator fun > minus(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, _ -> + operator fun minus(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, _ -> val shiftedRight = r.shiftInitialTime(l.initialTime) LinearEquation(l.initialTime, l.initialValue - shiftedRight.initialValue, l.rate - r.rate) } @@ -62,7 +62,7 @@ data class Real(private val timeline: Timeline, Real>): * * @throws RealOpException if both profiles have non-zero rate at the same time. */ - operator fun > times(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> + operator fun times(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, i -> if (!l.isConstant() && !r.isConstant()) throw RealOpException("Cannot multiply two linear equations that are non-constant at the same time (at time ${i.start})") val shiftedRight = r.shiftInitialTime(l.initialTime) val newRate = l.rate * shiftedRight.initialValue + r.rate * l.initialValue @@ -77,7 +77,7 @@ data class Real(private val timeline: Timeline, Real>): * * @throws RealOpException if the divisor has a non-zero rate at any time that the dividend is defined. */ - operator fun > div(other: SerialNumericOps) = map2Values(other.toSerialLinear()) { l, r, i -> + operator fun div(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, i -> if (!r.isConstant()) throw RealOpException("Cannot divide by a non-piecewise-constant linear equation (at time ${i.start})") LinearEquation(l.initialTime, l.initialValue / r.initialValue, l.rate / r.initialValue) } @@ -92,7 +92,7 @@ data class Real(private val timeline: Timeline, Real>): * or if the base has a non-zero rate at any time that the exponent is defined and not * either 0 or 1. */ - infix fun > pow(exp: SerialNumericOps) = map2Values(exp.toSerialLinear()) { l, r, i -> + infix fun pow(exp: SerialNumericOps<*, *>) = map2Values(exp.toSerialLinear()) { l, r, i -> if (!r.isConstant()) throw RealOpException("Cannot apply a non-piecewise-constant exponent (at time ${i.start}") if (r.initialValue == 0.0) LinearEquation(1.0) else if (r.initialValue == 1.0) l @@ -104,36 +104,36 @@ data class Real(private val timeline: Timeline, Real>): infix fun pow(n: Number) = pow(Numbers(n)) /** Returns a [Booleans] that is true when this and another numeric profile are equal. */ - infix fun > equalTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsEqualTo) + infix fun equalTo(other: SerialNumericOps<*, *>) = inequalityHelper(other, LinearEquation::intervalsEqualTo) /** Returns a [Booleans] that is true when this equals a constant number. */ infix fun equalTo(n: Number) = equalTo(Numbers(n)) /** Returns a [Booleans] that is true when this and another numeric profile are not equal. */ - infix fun > notEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsNotEqualTo) + infix fun notEqualTo(other: SerialNumericOps<*, *>) = inequalityHelper(other, LinearEquation::intervalsNotEqualTo) /** Returns a [Booleans] that is true when this does not equal a constant number. */ infix fun notEqualTo(n: Number) = notEqualTo(Numbers(n)) /** Returns a [Booleans] that is true when this is less than another numeric profile. */ - infix fun > lessThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThan) + infix fun lessThan(other: SerialNumericOps<*, *>) = inequalityHelper(other, LinearEquation::intervalsLessThan) /** Returns a [Booleans] that is true when this is less than a constant number. */ infix fun lessThan(n: Number) = lessThan(Numbers(n)) /** Returns a [Booleans] that is true when this is less than or equal to another numeric profile. */ - infix fun > lessThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsLessThanOrEqualTo) + infix fun lessThanOrEqualTo(other: SerialNumericOps<*, *>) = inequalityHelper(other, LinearEquation::intervalsLessThanOrEqualTo) /** Returns a [Booleans] that is true when this is less than or equal to a constant number. */ infix fun lessThanOrEqualTo(n: Number) = lessThanOrEqualTo(Numbers(n)) /** Returns a [Booleans] that is true when this is greater than another numeric profile. */ - infix fun > greaterThan(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThan) + infix fun greaterThan(other: SerialNumericOps<*, *>) = inequalityHelper(other, LinearEquation::intervalsGreaterThan) /** Returns a [Booleans] that is true when this is greater than a constant number. */ infix fun greaterThan(n: Number) = greaterThan(Numbers(n)) /** Returns a [Booleans] that is true when this is greater than or equal to another numeric profile. */ - infix fun > greaterThanOrEqualTo(other: SerialNumericOps) = inequalityHelper(other, LinearEquation::intervalsGreaterThanOrEqualTo) + infix fun greaterThanOrEqualTo(other: SerialNumericOps<*, *>) = inequalityHelper(other, LinearEquation::intervalsGreaterThanOrEqualTo) /** Returns a [Booleans] that is true when this is greater than or equal to a constant number. */ infix fun greaterThanOrEqualTo(n: Number) = greaterThanOrEqualTo(Numbers(n)) - private fun > inequalityHelper(other: SerialNumericOps, f: LinearEquation.(LinearEquation) -> Booleans) = + private fun inequalityHelper(other: SerialNumericOps<*, *>, f: LinearEquation.(LinearEquation) -> Booleans) = flatMap2Values(::Booleans, other.toSerialLinear()) { l, r, _ -> l.f(r) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt index 03afa059f4..1928dc3ed5 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/GeneralOps.kt @@ -180,7 +180,6 @@ interface GeneralOps, THIS: GeneralOps>: Timeline, THIS: GeneralOps>: Timeline, NESTED: GeneralOps, RESULT: GeneralOps> unsafeFlatMap(ctor: (Timeline) -> RESULT, boundsTransformer: BoundsTransformer, truncate: Boolean, f: (V) -> Segment) = + fun , RESULT: GeneralOps> unsafeFlatMap(ctor: (Timeline) -> RESULT, boundsTransformer: BoundsTransformer, truncate: Boolean, f: (V) -> Segment>) = unsafeOperate(ctor) { opts -> val mapped = collect(opts.transformBounds(boundsTransformer)).flatMap { val nested = f(it) @@ -213,7 +212,7 @@ interface GeneralOps, THIS: GeneralOps>: Timeline, OTHER: GeneralOps, R: IntervalLike, RESULT: GeneralOps> unsafeMap2(ctor: (Timeline) -> RESULT, other: GeneralOps, op: (V, W, Interval) -> R?) = + fun , R: IntervalLike, RESULT: GeneralOps> unsafeMap2(ctor: (Timeline) -> RESULT, other: GeneralOps, op: (V, W, Interval) -> R?) = unsafeOperate(ctor) { opts -> map2ParallelLists(collect(opts), other.collect(opts), isAlwaysSorted(), other.isAlwaysSorted(), op) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt index 175b2d87f2..381f5b2a07 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/ParallelOps.kt @@ -21,7 +21,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp override fun isAlwaysSorted() = false /** [(DOC)][merge] Combines two timelines together by overlaying them. Does not perform any transformation. */ - infix fun > merge(other: GeneralOps) = unsafeOperate { opts -> + infix fun merge(other: GeneralOps) = unsafeOperate { opts -> collect(opts) + other.collect(opts) } @@ -34,7 +34,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp * Not currently usable because no parallel profile types are implemented. * @suppress */ - fun , RESULT>> mapIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, f: (T) -> V) = + fun , RESULT>> mapIntoProfile(ctor: (Timeline, RESULT>) -> RESULT, f: (T) -> R) = unsafeMap(ctor, BoundsTransformer.IDENTITY, false) { Segment(it.interval, f(it)) } /** @@ -193,7 +193,7 @@ interface ParallelOps, THIS: ParallelOps>: GeneralOp * @param other the other timeline to connect to * @param connectToBounds whether to connect to the end of the bounds if the other timeline ends prematurely */ - fun , OTHER: ParallelOps> connectTo(other: ParallelOps, connectToBounds: Boolean) = + fun > connectTo(other: ParallelOps, connectToBounds: Boolean) = unsafeOperate(::Intervals) { opts -> val sortedFrom = collect(opts).sortedWith { l, r -> l.interval.compareEnds(r.interval) } val sortedTo = other.collect(opts).sortedWith { l, r -> l.interval.compareStarts(r.interval) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt index df43ddb60e..36a99cf1e3 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SegmentOps.kt @@ -30,7 +30,7 @@ interface SegmentOps>: NonZeroDurationOps> flatMapValues(f: (Segment) -> NESTED) = + fun flatMapValues(f: (Segment) -> SegmentOps) = unsafeFlatMap(ctor, IDENTITY, false) { it.mapValue(f) } /** @@ -40,17 +40,16 @@ interface SegmentOps>: NonZeroDurationOps, RESULT: SegmentOps> flatMapValues(ctor: (Timeline, RESULT>) -> RESULT, f: (Segment) -> NESTED) = + fun > flatMapValues(ctor: (Timeline, RESULT>) -> RESULT, f: (Segment) -> SegmentOps) = unsafeFlatMap(ctor, IDENTITY, false) { it.mapValue(f) } /** [(DOC)][map2Values] A simplified version of [map2Values] for operations that don't change the timeline type. */ - fun > map2Values(other: SegmentOps, op: (V, V, Interval) -> V?) = map2Values(ctor, other, op) + fun map2Values(other: SegmentOps, op: (V, V, Interval) -> V?) = map2Values(ctor, other, op) /** * [(DOC)][map2Values] Performs a local binary operation between two segment-valued timelines. @@ -67,7 +66,6 @@ interface SegmentOps>: NonZeroDurationOps>: NonZeroDurationOps, R: Any, RESULT: SegmentOps> map2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> R?) = + fun > map2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> R?) = unsafeMap2(ctor, other) { l, r, i -> op(l.value, r.value, i)?.let { Segment(i, it) } } /** [(DOC)][flatMap2Values] A simpler version of [flatMap2Values] for operations that don't change the timeline type. */ - fun , NESTED: SegmentOps> flatMap2Values(other: SegmentOps, op: (V, V, Interval) -> NESTED?) = + fun flatMap2Values(other: SegmentOps, op: (V, V, Interval) -> SegmentOps?) = flatMap2Values(ctor, other, op) /** @@ -94,9 +92,7 @@ interface SegmentOps>: NonZeroDurationOps>: NonZeroDurationOps, R: Any, NESTED: SegmentOps, RESULT: SegmentOps> flatMap2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> NESTED?) = + fun > flatMap2Values(ctor: (Timeline, RESULT>) -> RESULT, other: SegmentOps, op: (V, W, Interval) -> SegmentOps?) = unsafeOperate(ctor) { opts -> map2ParallelLists(collect(opts), other.collect(opts), isAlwaysSorted(), other.isAlwaysSorted()) { l, r, i -> op(l.value, r.value, i)?.let { Segment(i, it) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt index 168690d993..ca7eafd315 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialConstantOps.kt @@ -11,14 +11,14 @@ import gov.nasa.jpl.aerie.timeline.collections.profiles.Constants interface SerialConstantOps>: SerialSegmentOps, ConstantOps { /** [(DOC)][equalTo] Returns a [Booleans] that is `true` when this and another profile are equal. */ - infix fun > equalTo(other: OTHER) = + infix fun equalTo(other: SerialConstantOps) = map2Values(::Booleans, other) { l, r, _ -> l == r } /** [(DOC)][equalTo] Returns a [Booleans] that is `true` when this equals a constant value. */ infix fun equalTo(v: V) = equalTo(Constants(v)) /** [(DOC)][notEqualTo] Returns a [Booleans] that is `true` when this and another profile are not equal. */ - infix fun > notEqualTo(other: OTHER) = + infix fun notEqualTo(other: SerialConstantOps) = map2Values(::Booleans, other) { l, r, _ -> l != r } /** [(DOC)][notEqualTo] Returns a [Booleans] that is `true` when this is not equal to a constant value. */ diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt index 7be5e2e4d6..a19641dc3a 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialSegmentOps.kt @@ -18,23 +18,23 @@ import gov.nasa.jpl.aerie.timeline.util.truncateList */ interface SerialSegmentOps>: SerialOps, THIS>, SegmentOps, CoalesceSegmentsOp { /** Overlays two profiles on each other, asserting that they both cannot be defined at the same time. */ - infix fun > zip(other: SerialSegmentOps) = map2OptionalValues(other, NullBinaryOperation.zip()) + infix fun zip(other: SerialSegmentOps) = map2OptionalValues(other, NullBinaryOperation.zip()) /** [(DOC)][assignGaps] Fills in gaps in this profile with another profile. */ // While this is logically the converse of [set], they can't delegate to each other because it would mess up the return type. - infix fun > assignGaps(other: SerialSegmentOps) = + infix fun assignGaps(other: SerialSegmentOps) = map2OptionalValues(other, NullBinaryOperation.combineOrIdentity { l, _, _, -> l }) /** [(DOC)][assignGaps] Fills in gaps in this profile with a constant value. */ infix fun assignGaps(v: V) = assignGaps(Constants(v)) /** [(DOC)][set] Overwrites this profile with another. Gaps in the argument profile will be filled in with this profile. */ - infix fun > set(other: SerialSegmentOps) = map2OptionalValues(other, NullBinaryOperation.combineOrIdentity { _, r, _ -> r }) + infix fun set(other: SerialSegmentOps) = map2OptionalValues(other, NullBinaryOperation.combineOrIdentity { _, r, _ -> r }) /** * [(DOC)][map2OptionalValues] Performs a local binary operation between two profiles where the result * is the same type as this profile. */ - fun > map2OptionalValues(other: SerialSegmentOps, op: NullBinaryOperation) = map2OptionalValues(ctor, other, op) + fun map2OptionalValues(other: SerialSegmentOps, op: NullBinaryOperation) = map2OptionalValues(ctor, other, op) /** * [(DOC)][map2OptionalValues] Performs a local binary operation between two profiles, with special treatment @@ -56,7 +56,6 @@ interface SerialSegmentOps>: SerialOps< * For that, try to shift the results in a separate operation. * * @param W the other operand's payload type - * @param OTHER the other operand's timeline type * @param R the result's payload type * @param RESULT the result's timeline type * @@ -66,14 +65,14 @@ interface SerialSegmentOps>: SerialOps< * * @return a coalesced profile; an instance of the return type of [ctor] */ - fun , R: Any, RESULT: GeneralOps, RESULT>> map2OptionalValues(ctor: (Timeline, RESULT>) -> RESULT, other: SerialSegmentOps, op: NullBinaryOperation) = + fun , RESULT>> map2OptionalValues(ctor: (Timeline, RESULT>) -> RESULT, other: SerialSegmentOps, op: NullBinaryOperation) = unsafeOperate(ctor) { bounds -> map2SegmentLists(collect(bounds), other.collect(bounds), op) } /** * [(DOC)][flatMap2OptionalValues] Performs a local binary operation that produces profiles, and flattens * it into a profile of the same type as this. */ - fun , NESTED: SerialSegmentOps> flatMap2OptionalValues(other: SerialSegmentOps, op: NullBinaryOperation) = flatMap2OptionalValues(ctor, other, op) + fun flatMap2OptionalValues(other: SerialSegmentOps, op: NullBinaryOperation?>) = flatMap2OptionalValues(ctor, other, op) /** * [(DOC)][flatMap2OptionalValues] Performs a local binary operation that produces profiles, and flattens it, with @@ -86,9 +85,7 @@ interface SerialSegmentOps>: SerialOps< * varies within the segment, such as [gov.nasa.jpl.aerie.timeline.collections.profiles.Real]. * * @param W the other operand's payload type - * @param OTHER the other operand's timeline type * @param R the result's payload type - * @param NESTED the nested profile type returned by the operation before flattening * @param RESULT the result's timeline type * * @param ctor the result timeline's constructor @@ -99,7 +96,7 @@ interface SerialSegmentOps>: SerialOps< * * @see map2OptionalValues */ - fun , R: Any, NESTED: SerialSegmentOps, RESULT: GeneralOps, RESULT>> flatMap2OptionalValues(ctor: (Timeline, RESULT>) -> RESULT, other: SerialSegmentOps, op: NullBinaryOperation) = + fun , RESULT>> flatMap2OptionalValues(ctor: (Timeline, RESULT>) -> RESULT, other: SerialSegmentOps, op: NullBinaryOperation?>) = unsafeOperate(ctor) { opts -> map2SegmentLists(collect(opts), other.collect(opts), op) .flatMap { it.value.collect(CollectOptions(it.interval, true)) } From d76c5f923555eac3052fbdcff554fc820a8f9d1b Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 15:44:23 -0800 Subject: [PATCH 121/159] Refactor return types for numeric operations --- .../timeline/collections/profiles/Numbers.kt | 9 ++++++++- .../aerie/timeline/collections/profiles/Real.kt | 6 ++++-- .../jpl/aerie/timeline/ops/numeric/LinearOps.kt | 5 +++-- .../timeline/ops/numeric/SerialNumericOps.kt | 16 +++++++--------- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt index 558efce64b..24d3b233de 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt @@ -2,6 +2,7 @@ package gov.nasa.jpl.aerie.timeline.collections.profiles import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue import gov.nasa.jpl.aerie.timeline.BaseTimeline +import gov.nasa.jpl.aerie.timeline.Duration import gov.nasa.jpl.aerie.timeline.Interval import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.Timeline @@ -17,7 +18,7 @@ import kotlin.math.pow * A profile of piece-wise constant numbers. * * Unlike [Real], this is not able to vary linearly. Instead, - * it can contain either homogeneous (and strictly-typed) collection of + * it can contain either a homogeneous (and strictly-typed) collection of * any numeric type (i.e. `Numbers` (Java) or `Numbers` (Kotlin)), * or a heterogeneous collection of all numeric types (i.e. `Numbers`). * @@ -38,6 +39,7 @@ data class Numbers(private val timeline: Timeline, Numbers constructor(segments: List>): this(BaseTimeline(::Numbers, preprocessList(segments, Segment::valueEquals))) override fun toSerialLinear() = mapValues(::Real) { LinearEquation(it.value.toDouble()) } + override fun toSerialPrimitiveNumbers(message: String?) = this /* Due to the fact there is no superinterface for numbers that includes any arithmetic @@ -196,6 +198,11 @@ data class Numbers(private val timeline: Timeline, Numbers /** Returns a [Booleans] that is true when this is greater than or equal to a linear profile. */ infix fun greaterThanOrEqualTo(other: Real) = other lessThanOrEqualTo this + // This unchecked cast is OK because the difference between two primitives of different type + // will never be a third type. + @Suppress("UNCHECKED_CAST") + override fun shiftedDifference(range: Duration) = shift(range.negate()).minus(this) as Numbers + /***/ companion object { /** * Converts a list of serialized value segments into a [Numbers] profile; diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt index 922d533e7f..6e0e164224 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt @@ -25,7 +25,7 @@ data class Real(private val timeline: Timeline, Real>): constructor(vararg segments: Segment): this(segments.asList()) constructor(segments: List>): this(BaseTimeline(::Real, preprocessList(segments, Segment::valueEquals))) - override fun toSerialLinear() = unsafeCast(::Real) + override fun toSerialLinear() = this override fun LinearEquation.toLinear() = this /** @@ -33,7 +33,7 @@ data class Real(private val timeline: Timeline, Real>): * * @param message error message to throw if this is not piece-wise constant. */ - fun toSerialPrimitiveNumbers(message: String? = null) = mapValues(::Numbers) { + override fun toSerialPrimitiveNumbers(message: String?) = mapValues(::Numbers) { if (it.value.isConstant()) it.value.initialValue else if (message == null) throw RealOpException("Cannot convert a non-piecewise-constant linear equation to a constant number. (at time ${it.interval.start})") else throw RealOpException("$message (at time ${it.interval.start})") @@ -173,6 +173,8 @@ data class Real(private val timeline: Timeline, Real>): { l, r, i -> l.valueAt(i.start) == from && r.valueAt(i.start) == to } )) + override fun shiftedDifference(range: Duration) = shift(range.negate()).minus(this) + /** * An exception for linear profile operations; usually thrown in contexts that * require one or more of the operands to be piecewise constant. diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt index a82b27ef8e..390924f070 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOps.kt @@ -2,6 +2,7 @@ package gov.nasa.jpl.aerie.timeline.ops.numeric import gov.nasa.jpl.aerie.timeline.BoundsTransformer import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation @@ -30,8 +31,8 @@ interface LinearOps>: NumericOps { * @param unit length of the time basis vector */ fun rate(unit: Duration = Duration.SECOND) = - if (unit == Duration.SECOND) mapValues { LinearEquation(it.value.rate) } - else mapValues { LinearEquation(it.value.rate / (Duration.SECOND ratioOver unit)) } + if (unit == Duration.SECOND) mapValues(::Numbers) { it.value.rate } + else mapValues(::Numbers) { it.value.rate / (Duration.SECOND ratioOver unit) } override fun shift(dur: Duration) = unsafeMap(BoundsTransformer.shift(dur), false) { v -> Segment(v.interval.shiftBy(dur), LinearEquation(v.value.initialTime.plus(dur), v.value.initialValue, v.value.rate)) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt index beec3d69e6..0b840f482a 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.timeline.ops.numeric import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers import gov.nasa.jpl.aerie.timeline.payloads.Segment import gov.nasa.jpl.aerie.timeline.collections.profiles.Real import gov.nasa.jpl.aerie.timeline.ops.SerialSegmentOps @@ -13,6 +14,7 @@ import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation interface SerialNumericOps>: SerialSegmentOps, NumericOps { /** [(DOC)][toSerialLinear] Converts the profile to a linear profile, a.k.a. [Real] (no-op if it already was linear). */ fun toSerialLinear(): Real + fun toSerialPrimitiveNumbers(message: String? = null): Numbers<*> /** * [(DOC)][integrate] Calculates the integral of this profile, starting from zero. @@ -28,7 +30,7 @@ interface SerialNumericOps>: SerialSegme * @param unit length of the time basis vector */ fun integrate(unit: Duration = Duration.SECOND) = - toSerialLinear().unsafeOperate { opts -> + toSerialPrimitiveNumbers("Cannot integrate a non-piecewise-constant linear profile.").unsafeOperate(::Real) { opts -> val segments = collect(opts) val result = mutableListOf>() val baseRate = Duration.SECOND.ratioOver(unit) @@ -37,9 +39,7 @@ interface SerialNumericOps>: SerialSegme for (segment in segments) { if (previousTime < segment.interval.start) throw Real.RealOpException("Cannot integrate a linear profile that has gaps (time $previousTime") - if (!segment.value.isConstant()) - throw Real.RealOpException("Cannot integrate a non-piecewise-constant linear profile (time $previousTime") - val rate = segment.value.initialValue * baseRate + val rate = segment.value.toDouble() * baseRate val nextAcc = acc + rate * segment.interval.duration().ratioOver(Duration.SECOND) result.add(Segment(segment.interval, LinearEquation(previousTime, acc, rate))) previousTime = segment.interval.end @@ -49,12 +49,10 @@ interface SerialNumericOps>: SerialSegme } /** - * [(DOC)][shiftedDifference] Calculates the difference between this, and this profile's value at [range] time in the future. + * [(DOC)][shiftedDifference] Calculates the difference between this profile's value at [range] time in the future, + * and this profile at the present. * * If this is a function `f(t)`, the result is `f(t+range) - f(t)`. */ - fun shiftedDifference(range: Duration): Real { - val linearized = toSerialLinear() - return linearized.shift(range.negate()).minus(linearized) - } + fun shiftedDifference(range: Duration): THIS } From 99f3b9e4f6867c2a437c546204f511c731087a21 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 15:46:08 -0800 Subject: [PATCH 122/159] More unit tests --- .../gov/nasa/jpl/aerie/timeline/util/Map2.kt | 2 +- .../aerie/timeline/collections/WindowsTest.kt | 130 ++++++++++++++ .../profiles/BooleansTest.kt} | 5 +- .../timeline/collections/profiles/RealTest.kt | 22 +++ .../timeline/ops/numeric/LinearOpsTest.kt | 22 +++ .../ops/numeric/SerialNumericOpsTest.kt | 96 ++++++++++ .../nasa/jpl/aerie/timeline/util/Map2Test.kt | 164 ++++++++++-------- 7 files changed, 366 insertions(+), 75 deletions(-) create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt rename timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/{ops/SerialBooleanTest.kt => collections/profiles/BooleansTest.kt} (94%) create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt create mode 100644 timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt index 32c3afcb23..adc8f1e17d 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/util/Map2.kt @@ -191,7 +191,7 @@ fun , RIGHT: IntervalLike, OUT: IntervalLike() for (leftObj in leftSorted) { - while (rightSorted[rightIndex].interval.compareEndToStart(leftObj.interval) == -1 && rightIndex < right.size) { + while (rightIndex < right.size && rightSorted[rightIndex].interval.compareEndToStart(leftObj.interval) == -1) { rightIndex++ } diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt new file mode 100644 index 0000000000..ccec6dd57f --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/WindowsTest.kt @@ -0,0 +1,130 @@ +package gov.nasa.jpl.aerie.timeline.collections + +import gov.nasa.jpl.aerie.timeline.CollectOptions +import gov.nasa.jpl.aerie.timeline.Duration +import gov.nasa.jpl.aerie.timeline.Duration.Companion.milliseconds +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval.Companion.at +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.Interval.Inclusivity.Exclusive +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class WindowsTest { + @Test + fun constructorCoalesce() { + val result = Windows( + seconds(2) .. seconds(3), + seconds(0) ..< seconds(2), + at(seconds(5)), + seconds(4) .. seconds(6) + ).collect() + + assertIterableEquals( + listOf( + seconds(0) .. seconds(3), + seconds(4) .. seconds(6) + ), + result + ) + } + + @Test + fun complement() { + val windows = Windows( + seconds(1) .. seconds(3), + seconds(5) ..< seconds(8) + ).complement() + + // testing how complement works on different bounds + + assertIterableEquals( + listOf( + Duration.MIN_VALUE ..< seconds(1), + between(seconds(3), seconds(5), Exclusive), + seconds(8) .. Duration.MAX_VALUE + ), + windows.collect() + ) + + assertIterableEquals( + listOf( + seconds(0) ..< seconds(1), + between(seconds(3), seconds(5), Exclusive), + seconds(8) .. seconds(10) + ), + windows.collect(seconds(0) .. seconds(10)) + ) + + assertIterableEquals( + listOf(between(seconds(3), seconds(5), Exclusive)), + windows.collect(seconds(2) .. seconds(7)) + ) + assertIterableEquals( + listOf(between(seconds(3), seconds(5), Exclusive)), + windows.collect(CollectOptions(seconds(2) .. seconds(7), false)) + ) + + assertIterableEquals( + listOf( + seconds(4) ..< seconds(5), + seconds(8) .. seconds(10) + ), + windows.collect(seconds(4) .. seconds(10)) + ) + } + + @Test + fun union() { + val w1 = Windows( + seconds(0) ..< seconds(1), + at(milliseconds(3500)), + seconds(5) .. seconds(7), + seconds(8) .. seconds(10) + ) + + val w2 = Windows( + seconds(1) .. seconds(2), + seconds(3) .. seconds(4), + seconds(6) .. seconds(9) + ) + + val result = (w1 union w2).collect() + + assertIterableEquals( + listOf( + seconds(0) .. seconds(2), + seconds(3) .. seconds(4), + seconds(5) .. seconds(10) + ), + result + ) + } + + @Test + fun intersection() { + val w1 = Windows( + seconds(1) .. seconds(2), + seconds(5) ..< seconds(7) + ) + + val w2 = Windows( + seconds(0) ..< seconds(1), + seconds(2) .. seconds(3), + at(seconds(5)), + seconds(6) .. seconds(8) + ) + + val result = (w1 intersection w2).collect() + + assertIterableEquals( + listOf( + at(seconds(2)), + at(seconds(5)), + seconds(6) ..< seconds(7) + ), + result + ) + } + +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/BooleansTest.kt similarity index 94% rename from timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanTest.kt rename to timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/BooleansTest.kt index 2dea3c77da..fdf23be27d 100644 --- a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/SerialBooleanTest.kt +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/BooleansTest.kt @@ -1,14 +1,13 @@ -package gov.nasa.jpl.aerie.timeline.ops +package gov.nasa.jpl.aerie.timeline.collections.profiles import gov.nasa.jpl.aerie.timeline.CollectOptions import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds import gov.nasa.jpl.aerie.timeline.Interval.Companion.between import gov.nasa.jpl.aerie.timeline.payloads.Segment -import gov.nasa.jpl.aerie.timeline.collections.profiles.Booleans import org.junit.jupiter.api.Assertions.assertIterableEquals import org.junit.jupiter.api.Test -class SerialBooleanTest { +class BooleansTest { @Test fun shiftEdgesBasic() { val result = Booleans( diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt new file mode 100644 index 0000000000..8ba121c39f --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/RealTest.kt @@ -0,0 +1,22 @@ +package gov.nasa.jpl.aerie.timeline.collections.profiles + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class RealTest { + @Test + fun plusShiftsInitialTime() { + val result = (Real( + Segment(seconds(0)..seconds(2), LinearEquation(seconds(0), 1.0, 1.0)) + ) + Real( + Segment(seconds(1)..seconds(3), LinearEquation(seconds(-2), -1.0, 3.0)) + )).collect() + assertIterableEquals( + listOf(Segment(seconds(1)..seconds(2), LinearEquation(seconds(1), 10.0, 4.0))), + result + ) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt new file mode 100644 index 0000000000..ccc7a46ef0 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/LinearOpsTest.kt @@ -0,0 +1,22 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test + +class LinearOpsTest { + @Test + fun shift() { + val result = Real( + Segment(seconds(0)..seconds(1), LinearEquation(seconds(0), 1.0, 2.0)) + ).shift(seconds(5)).collect() + + assertIterableEquals( + listOf(Segment(seconds(5)..seconds(6), LinearEquation(seconds(5), 1.0, 2.0))), + result + ) + } +} diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt new file mode 100644 index 0000000000..3f8e9196f4 --- /dev/null +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt @@ -0,0 +1,96 @@ +package gov.nasa.jpl.aerie.timeline.ops.numeric + +import gov.nasa.jpl.aerie.timeline.Duration.Companion.seconds +import gov.nasa.jpl.aerie.timeline.Interval +import gov.nasa.jpl.aerie.timeline.Interval.Companion.between +import gov.nasa.jpl.aerie.timeline.collections.profiles.Numbers +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation +import gov.nasa.jpl.aerie.timeline.payloads.Segment +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class SerialNumericOpsTest { + @Test + fun toSerialLinear() { + val real = Real( + Segment(seconds(0)..seconds(1), LinearEquation(4.0)), + Segment(seconds(3)..seconds(4), LinearEquation(5.0)), + ) + val numbers = Numbers( + Segment(seconds(0)..seconds(1), 4.0), + Segment(seconds(3)..seconds(4), 5L), + ) + + assertIterableEquals( + real.collect(), + real.toSerialLinear().collect() + ) + assertIterableEquals( + real.collect(), + numbers.toSerialLinear().collect() + ) + } + + @Test + fun toSerialPrimitiveNumbers() { + val real = Real( + Segment(seconds(0)..seconds(1), LinearEquation(4.0)), + Segment(seconds(3)..seconds(4), LinearEquation(5.0)), + ) + val numbers = Numbers( + Segment(seconds(0)..seconds(1), 4.0), + Segment(seconds(3)..seconds(4), 5L), + ) + + assertIterableEquals( + numbers.collect(), + numbers.toSerialPrimitiveNumbers().collect() + ) + assertIterableEquals( + numbers.toDoubles().collect(), + real.toSerialPrimitiveNumbers().collect() + ) + + assertThrows { + Real( + Segment(seconds(0)..seconds(1), LinearEquation(seconds(0), 1.0, 1.0)) + ).toSerialPrimitiveNumbers().collect() + } + } + + @Test + fun integrate() { + val numbers = Numbers( + Segment(seconds(0).. l + r} - ) - - val expected = listOf( - Segment(seconds(0) ..< seconds(1), 2), - Segment(seconds(1) .. seconds(2), 5), - Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), 3), - ) - - assertIterableEquals(expected, result) - } + @Nested + inner class Map2SegmentLists { - @Test - fun basicCombineOrUndefined() { - val left = listOf(Segment(seconds(0) .. seconds(2), 2)) - val right = listOf(Segment(seconds(1) .. seconds(3), 3)) + @Test + fun basicCombineOrIdentity() { + val left = listOf(Segment(seconds(0)..seconds(2), 2)) + val right = listOf(Segment(seconds(1)..seconds(3), 3)) - val result = map2SegmentLists( - left, right, - NullBinaryOperation.combineOrNull { l, r, _ -> l + r} - ) + val result = map2SegmentLists( + left, right, + NullBinaryOperation.combineOrIdentity { l, r, _ -> l + r } + ) - val expected = listOf( - Segment(seconds(1) .. seconds(2), 5) - ) + val expected = listOf( + Segment(seconds(0).. l + r } + ) + + val expected = listOf( + Segment(seconds(1)..seconds(2), 5) + ) + + assertIterableEquals(expected, result) + } } @Nested inner class SegmentAlignment { - // Helper functions for below - val op = NullBinaryOperation.combineOrIdentity { l, r, _ -> l + r } - fun makeLeft(s: Long, e: Long, si: Interval.Inclusivity = Inclusive, ei: Interval.Inclusivity = Inclusive) = + private fun makeLeft(s: Long, e: Long, si: Interval.Inclusivity = Inclusive, ei: Interval.Inclusivity = Inclusive) = listOf(Segment(between(seconds(s), seconds(e), si, ei), -1)) - fun makeRight(s: Long, e: Long, si: Interval.Inclusivity = Inclusive, ei: Interval.Inclusivity = Inclusive) = + private fun makeRight(s: Long, e: Long, si: Interval.Inclusivity = Inclusive, ei: Interval.Inclusivity = Inclusive) = listOf(Segment(between(seconds(s), seconds(e), si, ei), 1)) + private fun testBothMap2Routines( + left: List>, + right: List>, + resultWithIdentity: List>, + ) { + val resultWithGaps = resultWithIdentity.filter { it.value == 0 } + assertIterableEquals( + resultWithIdentity, + map2SegmentLists(left, right, NullBinaryOperation.combineOrIdentity { l, r, _ -> l + r }) + ) + assertIterableEquals( + resultWithGaps, + map2SegmentLists(left, right, NullBinaryOperation.combineOrNull { l, r, _ -> l + r }) + ) + assertIterableEquals( + resultWithGaps, + map2ParallelLists(left, right, false, false) { l, r, i -> Segment(i, l.value + r.value) } + ) + } + @Test fun identical() { - assertIterableEquals( - listOf(Segment(seconds(1) .. seconds(2), 0)), - map2SegmentLists(makeLeft(1, 2), makeRight(1, 2), op) + testBothMap2Routines( + makeLeft(1, 2), makeRight(1, 2), + listOf(Segment(seconds(1)..seconds(2), 0)) ) } @Test fun identicalExclusive() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(1, 2, Exclusive, Exclusive), makeRight(1, 2, Exclusive, Exclusive), listOf(Segment(between(seconds(1), seconds(2), Exclusive, Exclusive), 0)), - map2SegmentLists(makeLeft(1, 2, Exclusive, Exclusive), makeRight(1, 2, Exclusive, Exclusive), op) ) } @Test fun entireLeftSegmentFirst() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(1, 2), makeRight(3, 4), listOf( - Segment(seconds(1) .. seconds(2), -1), - Segment(seconds(3) .. seconds(4), 1) + Segment(seconds(1)..seconds(2), -1), + Segment(seconds(3)..seconds(4), 1) ), - map2SegmentLists(makeLeft(1, 2), makeRight(3, 4), op) ) } @Test fun entireRightSegmentFirst() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(3, 4), makeRight(1, 2), listOf( - Segment(seconds(1) .. seconds(2), 1), - Segment(seconds(3) .. seconds(4), -1) + Segment(seconds(1)..seconds(2), 1), + Segment(seconds(3)..seconds(4), -1) ), - map2SegmentLists(makeLeft(3, 4), makeRight(1, 2), op) ) } @Test fun leftFirstMomentOfOverlap() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(1, 2), makeRight(2, 3), listOf( Segment(between(seconds(1), seconds(2), endInclusivity = Exclusive), -1), Segment(Interval.at(seconds(2)), 0), Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), 1) ), - map2SegmentLists(makeLeft(1, 2), makeRight(2, 3), op) ) } @Test fun rightFirstMomentOfOverlap() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(2, 3), makeRight(1, 2), listOf( Segment(between(seconds(1), seconds(2), endInclusivity = Exclusive), 1), Segment(Interval.at(seconds(2)), 0), Segment(between(seconds(2), seconds(3), Exclusive, Inclusive), -1) ), - map2SegmentLists(makeLeft(2, 3), makeRight(1, 2), op) ) } @Test fun leftFirstMomentOfNonOverlap() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(1, 2), makeRight(1, 2, Exclusive, Inclusive), listOf( Segment(Interval.at(seconds(1)), -1), Segment(between(seconds(1), seconds(2), Exclusive, Inclusive), 0) ), - map2SegmentLists(makeLeft(1, 2), makeRight(1, 2, Exclusive, Inclusive), op) ) } @Test fun rightFirstMomentOfNonOverlap() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(1, 2, Exclusive, Inclusive), makeRight(1, 2), listOf( Segment(Interval.at(seconds(1)), 1), Segment(between(seconds(1), seconds(2), Exclusive, Inclusive), 0) ), - map2SegmentLists(makeLeft(1, 2, Exclusive, Inclusive), makeRight(1, 2), op) ) } @Test fun leftFirstHalfNonOverlap() { - assertIterableEquals( + testBothMap2Routines( + makeLeft(1, 3), makeRight(2, 4), listOf( - Segment(seconds(1) ..< seconds(2), -1), - Segment(seconds(2) .. seconds(3), 0), + Segment(seconds(1).. Date: Wed, 6 Mar 2024 16:39:16 -0800 Subject: [PATCH 123/159] Apply JvmField annotations --- .../jpl/aerie/timeline/BoundsTransformer.kt | 2 +- .../nasa/jpl/aerie/timeline/CollectOptions.kt | 4 ++-- .../gov/nasa/jpl/aerie/timeline/Duration.kt | 20 +++++++++---------- .../gov/nasa/jpl/aerie/timeline/Interval.kt | 12 +++++------ .../jpl/aerie/timeline/payloads/Connection.kt | 4 ++-- .../aerie/timeline/payloads/LinearEquation.kt | 6 +++--- .../jpl/aerie/timeline/payloads/Segment.kt | 2 +- .../payloads/activities/AnyDirective.kt | 2 +- .../payloads/activities/AnyInstance.kt | 4 ++-- .../timeline/payloads/activities/Directive.kt | 6 +++--- .../timeline/payloads/activities/Instance.kt | 6 +++--- 11 files changed, 34 insertions(+), 34 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt index 5d315b5f07..a859128b59 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/BoundsTransformer.kt @@ -14,7 +14,7 @@ fun interface BoundsTransformer { /** Helper functions for constructing bounds transformers. */ companion object { /** Does nothing. Used for operations that don't need to change the bounds. */ - @JvmStatic + @JvmField val IDENTITY: BoundsTransformer = BoundsTransformer { i -> i } /** diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt index c3aa3af291..4f487e3dc0 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt @@ -5,14 +5,14 @@ package gov.nasa.jpl.aerie.timeline */ data class CollectOptions( /** The bounds on which to evaluate the timeline. */ - val bounds: Interval, + @JvmField val bounds: Interval, /** * Whether to truncate objects that extend outside the bounds. * * Objects with no intersection with the bounds should never be included in the results. */ - val truncateMarginal: Boolean = true + @JvmField val truncateMarginal: Boolean = true ) { /** Creates a new options object with a [BoundsTransformer] applied. */ fun transformBounds(boundsTransformer: BoundsTransformer) = CollectOptions(boundsTransformer(bounds), truncateMarginal) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt index dcfa5e1af4..d66d51f4fb 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt @@ -124,12 +124,12 @@ data class Duration(private val micros: Long) : Comparable { * except that it is no larger than [Duration.MICROSECOND]. * */ - @JvmStatic val EPSILON = Duration(1) + @JvmField val EPSILON = Duration(1) /** * The empty span of time. */ - @JvmStatic val ZERO = Duration(0) + @JvmField val ZERO = Duration(0) /** * The largest observable negative span of time. Attempting to go "more negative" will cause an exception. @@ -138,7 +138,7 @@ data class Duration(private val micros: Long) : Comparable { * Currently, this is precisely -9,223,372,036,854,775,808 microseconds, or approximately -293,274 years. * */ - @JvmStatic val MIN_VALUE = Duration(Long.MIN_VALUE) + @JvmField val MIN_VALUE = Duration(Long.MIN_VALUE) /** * The largest observable positive span of time. Attempting to go "more positive" will cause an exception. @@ -147,25 +147,25 @@ data class Duration(private val micros: Long) : Comparable { * Currently, this is precisely +9,223,372,036,854,775,807 microseconds, or approximately 293,274 years. * */ - @JvmStatic val MAX_VALUE = Duration(Long.MAX_VALUE) + @JvmField val MAX_VALUE = Duration(Long.MAX_VALUE) /** One microsecond (μs). */ - @JvmStatic val MICROSECOND = Duration(1) + @JvmField val MICROSECOND = Duration(1) /** One millisecond (ms), equal to 1000μs. */ - @JvmStatic val MILLISECOND = MICROSECOND * 1000 + @JvmField val MILLISECOND = MICROSECOND * 1000 /** One second (s), equal to 1000ms. */ - @JvmStatic val SECOND = MILLISECOND * 1000 + @JvmField val SECOND = MILLISECOND * 1000 /** One minute (m), equal to 60s. */ - @JvmStatic val MINUTE = SECOND * 60 + @JvmField val MINUTE = SECOND * 60 /** One hour (h), equal to 60m. */ - @JvmStatic val HOUR = MINUTE * 60 + @JvmField val HOUR = MINUTE * 60 /** One hour (d), equal to 24h. */ - @JvmStatic val DAY = HOUR * 24 + @JvmField val DAY = HOUR * 24 /** Constructs a duration with a given number of microseconds. */ @JvmStatic fun microseconds(quantity: Long) = Duration(quantity) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt index 68794eae6a..0f3b52af07 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt @@ -7,12 +7,12 @@ import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike * and start and end inclusivity. */ data class Interval( - /***/ val start: Duration, - /***/ val end: Duration, + /***/ @JvmField val start: Duration, + /***/ @JvmField val end: Duration, /** Whether this interval contains its start time. */ - val startInclusivity: Inclusivity = Inclusivity.Inclusive, + @JvmField val startInclusivity: Inclusivity = Inclusivity.Inclusive, /** Whether this interval contains its end time. */ - val endInclusivity: Inclusivity = startInclusivity + @JvmField val endInclusivity: Inclusivity = startInclusivity ): IntervalLike { /** Constructs an interval that contains both its endpoints. */ @@ -318,9 +318,9 @@ data class Interval( @JvmStatic fun at(point: Duration) = point .. point /** Shorthand for an empty interval. */ - @JvmStatic val EMPTY: Interval = Duration.ZERO .. (Duration.ZERO - Duration.EPSILON) + @JvmField val EMPTY: Interval = Duration.ZERO .. (Duration.ZERO - Duration.EPSILON) /** The widest representable interval (from long min to long max microseconds). */ - @JvmStatic val MIN_MAX = Duration.MIN_VALUE .. Duration.MAX_VALUE + @JvmField val MIN_MAX = Duration.MIN_VALUE .. Duration.MAX_VALUE } } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt index 6af81a6605..0b68a096b5 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Connection.kt @@ -6,9 +6,9 @@ import gov.nasa.jpl.aerie.timeline.Interval data class Connection, TO: IntervalLike>( override val interval: Interval, /** Object at the start of the connection. */ - val from: FROM?, + @JvmField val from: FROM?, /** Object at the end of the connection. */ - val to: TO? + @JvmField val to: TO? ): IntervalLike> { override fun withNewInterval(i: Interval) = Connection(i, from, to) } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt index 384c2a0c9e..d123e398e4 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/LinearEquation.kt @@ -13,11 +13,11 @@ import kotlin.math.absoluteValue /** A linear equation in point-slope form. */ data class LinearEquation( /** The time of the start point. */ - val initialTime: Duration, + @JvmField val initialTime: Duration, /** The value of the start point. */ - val initialValue: Double, + @JvmField val initialValue: Double, /** The rate of change, in units per second. */ - val rate: Double + @JvmField val rate: Double ) { /** Creates a constant linear equation at a given value. */ constructor(constant: Double): this(Duration.ZERO, constant, 0.0) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt index be54c9663c..0d12b1afc1 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/Segment.kt @@ -6,7 +6,7 @@ import gov.nasa.jpl.aerie.timeline.util.coalesceList /** * A generic container that associates a value with an interval on a timeline. */ -data class Segment(/***/ override val interval: Interval, /***/ val value: V): IntervalLike> { +data class Segment(/***/ override val interval: Interval, /***/ @JvmField val value: V): IntervalLike> { /** * Create a new segment on the same interval, with a new value derived from this segment. * diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt index 80eb3f5c52..9ad79df43a 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt @@ -4,5 +4,5 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue /** A general-purpose container for representing the arguments any type of activity directive. */ data class AnyDirective( - /***/ val arguments: Map + /***/ @JvmField val arguments: Map ) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt index 98d8fb9b95..3123568776 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt @@ -4,6 +4,6 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue /** A general-purpose container for representing the arguments and computed attributes of any type of activity instance. */ data class AnyInstance( - /***/ val arguments: Map, - /***/ val computedAttributes: SerializedValue + /***/ @JvmField val arguments: Map, + /***/ @JvmField val computedAttributes: SerializedValue ) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt index 721d9e93d7..971e91a3fb 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Directive.kt @@ -6,13 +6,13 @@ import gov.nasa.jpl.aerie.timeline.Interval /** A wrapper of any type of activity directive containing common data. */ data class Directive( /** The inner payload, typically either [AnyDirective] or a mission model activity type. */ - val inner: A, + @JvmField val inner: A, /** The name of this specific directive. */ - val name: String, + @JvmField val name: String, /** The directive id. */ - val id: Long, + @JvmField val id: Long, override val type: String, override val startTime: Duration diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt index 8b608c17d9..2af9a5e865 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/Instance.kt @@ -6,18 +6,18 @@ import gov.nasa.jpl.aerie.timeline.Interval /** A wrapper of any type of activity instance containing common data. */ data class Instance( /** The inner payload, typically either [AnyInstance] or a mission model activity type. */ - val inner: A, + @JvmField val inner: A, override val type: String, /** The instance id. */ - val id: Long, + @JvmField val id: Long, /** * The maybe-null id of the directive associated with this instance. * * Will be `null` if this is a child activity. */ - val directiveId: Long?, + @JvmField val directiveId: Long?, override val interval: Interval, ): Activity> { override val startTime: Duration From f77c800f2ac3819fb258551d6d743da51afa4f97 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 16:55:12 -0800 Subject: [PATCH 124/159] Fix overloads for Java --- .../kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt | 2 +- .../main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt | 9 +++------ .../main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt | 5 ++++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt index 4f487e3dc0..8fdf667d0b 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/CollectOptions.kt @@ -3,7 +3,7 @@ package gov.nasa.jpl.aerie.timeline /** * Options for collecting a timeline. */ -data class CollectOptions( +data class CollectOptions @JvmOverloads constructor( /** The bounds on which to evaluate the timeline. */ @JvmField val bounds: Interval, diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt index 0f3b52af07..01eb2c412e 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Interval.kt @@ -6,7 +6,7 @@ import gov.nasa.jpl.aerie.timeline.payloads.IntervalLike * An Interval on the timeline, represented by start and end points * and start and end inclusivity. */ -data class Interval( +data class Interval @JvmOverloads constructor( /***/ @JvmField val start: Duration, /***/ @JvmField val end: Duration, /** Whether this interval contains its start time. */ @@ -15,9 +15,6 @@ data class Interval( @JvmField val endInclusivity: Inclusivity = startInclusivity ): IntervalLike { - /** Constructs an interval that contains both its endpoints. */ - constructor(start: Duration, end: Duration) : this(start, end, Inclusivity.Inclusive, Inclusivity.Inclusive) - /** * Labels to indicate whether an interval includes its endpoints. */ @@ -75,7 +72,7 @@ data class Interval( * @param shiftStart Duration to shift the start by. Negative means backward in time. * @param shiftEnd Duration to shift the end by. Defaults to [shiftStart] */ - fun shiftBy(shiftStart: Duration, shiftEnd: Duration = shiftStart) = between( + @JvmOverloads fun shiftBy(shiftStart: Duration, shiftEnd: Duration = shiftStart) = between( start.saturatingPlus(shiftStart), end.saturatingPlus(shiftEnd), startInclusivity, @@ -299,7 +296,7 @@ data class Interval( * @param end The ending time of the interval. * @return A non-empty interval if start < end, or an empty interval otherwise. */ - @JvmStatic fun between( + @JvmOverloads @JvmStatic fun between( start: Duration, end: Duration, startInclusivity: Inclusivity = Inclusivity.Inclusive, diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt index a1445a2983..ca77ab8f24 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Timeline.kt @@ -26,7 +26,10 @@ interface Timeline, THIS: Timeline> { * * @param bounds bounds of evaluation (defaults to [Interval.MIN_MAX] if not provided). */ - fun collect(bounds: Interval = Interval.MIN_MAX) = collect(CollectOptions(bounds)) + fun collect(bounds: Interval) = collect(CollectOptions(bounds)) + + /** [(DOC)][collect] Collects the timeline for all available time. */ + fun collect() = collect(Interval.MIN_MAX) /** * [(DOC)][unsafeCast] **UNSAFE!** Casts this timeline type to another type without changing its contents. From c28bf44827e1fbd4d3836f351031ebacbfc295d0 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 7 Mar 2024 15:41:39 -0800 Subject: [PATCH 125/159] Reduce Duration extremes to MIN/2 and MAX/2 --- .../gov/nasa/jpl/aerie/timeline/Duration.kt | 36 +++++++++++++------ .../aerie/timeline/plan/AeriePostgresPlan.kt | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt index d66d51f4fb..438b1a1463 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/Duration.kt @@ -131,23 +131,31 @@ data class Duration(private val micros: Long) : Comparable { */ @JvmField val ZERO = Duration(0) + private const val MICROSECONDS_MIN = Long.MIN_VALUE / 2 + private const val MICROSECONDS_MAX = Long.MAX_VALUE / 2 + /** - * The largest observable negative span of time. Attempting to go "more negative" will cause an exception. - * - * The value of this quantity should not be assumed. - * Currently, this is precisely -9,223,372,036,854,775,808 microseconds, or approximately -293,274 years. + * The largest valid negative span of time. Attempting to go "more negative" may cause an exception. * + * The value of this quantity should not be assumed. Currently, it equals half of long-min microseconds. + * This is for two reasons: + * 1. It may avoid some overflow errors by giving a 2x margin. + * 2. Java durations are serialized to ISO8601 duration format using only hours, minutes, and fractional seconds. + * This is to avoid timekeeping issues related to leap seconds and leap years. Postgres intervals can represent + * durations up to hundreds of millions of years, but only by using days and months. Thus, attempting to + * interact with Postgres using a duration of long-min or long-max microseconds, represented as hours, causes an overflow. + * However, long-min / 2 and long-max / 2 microseconds are small enough to not cause overflow. */ - @JvmField val MIN_VALUE = Duration(Long.MIN_VALUE) + @JvmField val MIN_VALUE = Duration(MICROSECONDS_MIN) /** - * The largest observable positive span of time. Attempting to go "more positive" will cause an exception. + * The largest valid positive span of time. Attempting to go "more positive" may cause an exception. * - * The value of this quantity should not be assumed. - * Currently, this is precisely +9,223,372,036,854,775,807 microseconds, or approximately 293,274 years. + * The value of this quantity should not be assumed. Currently, it equals half of long-max microseconds. * + * @see MIN_VALUE for an explanation of the choice of extremes. */ - @JvmField val MAX_VALUE = Duration(Long.MAX_VALUE) + @JvmField val MAX_VALUE = Duration(MICROSECONDS_MAX) /** One microsecond (μs). */ @JvmField val MICROSECOND = Duration(1) @@ -219,7 +227,15 @@ data class Duration(private val micros: Long) : Comparable { private fun saturatingAddInternal(left: Long, right: Long): Long { val result = left + right return if (result xor left and (result xor right) < 0) { - Long.MIN_VALUE - (result ushr java.lang.Long.SIZE - 1) + val saturatedToLongBounds = Long.MIN_VALUE - (result ushr java.lang.Long.SIZE - 1) + + // This block is added to clamp the value to less than the long data type bounds. + // IntelliJ says that the second if-branch is always true, but that's some black + // magic wizardry, so I'm not touching it. The compiler can make that optimization + // if its really that smart. + if (saturatedToLongBounds < MICROSECONDS_MIN) Long.MIN_VALUE / 2 + else if (saturatedToLongBounds > MICROSECONDS_MAX) Long.MAX_VALUE / 2 + else saturatedToLongBounds } else result } diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt index 171c30f523..76178967ae 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt @@ -198,7 +198,7 @@ data class AeriePostgresPlan( private val activityDirectivesStatement = c.prepareStatement( "select name, start_offset, type, arguments, id from activity_directive where plan_id = ?" + - " and start_offset > cast(? as interval) and start_offset < cast(? as interval);" + " and start_offset > ?::interval and start_offset < ?::interval;" ) override fun allActivityDirectives() = BaseTimeline(::Directives) { opts -> activityDirectivesStatement.clearParameters() From dac1deb676e42c120feb9f20a8e75580459379a8 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Wed, 6 Mar 2024 16:39:33 -0800 Subject: [PATCH 126/159] e2e tests for remote plan query --- e2e-tests/build.gradle | 5 + .../jpl/aerie/e2e/TimelineRemoteTests.java | 150 ++++++++++++++++++ timeline/build.gradle | 1 - 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle index 9d1eb39b29..33d5e47d01 100644 --- a/e2e-tests/build.gradle +++ b/e2e-tests/build.gradle @@ -46,6 +46,11 @@ task e2eTest(type: Test) { } dependencies { + testImplementation project(":timeline") + testImplementation "com.zaxxer:HikariCP:5.1.0" + testImplementation("org.postgresql:postgresql:42.6.0") + testImplementation project(':merlin-driver') + testImplementation 'com.microsoft.playwright:playwright:1.37.0' testImplementation 'org.glassfish:javax.json:1.1.4' diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java new file mode 100644 index 0000000000..9f1d2099c2 --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java @@ -0,0 +1,150 @@ +package gov.nasa.jpl.aerie.e2e; + +import com.microsoft.playwright.Playwright; +import gov.nasa.jpl.aerie.e2e.utils.BaseURL; +import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; +import gov.nasa.jpl.aerie.e2e.utils.HasuraRequests; +import gov.nasa.jpl.aerie.timeline.Duration; +import gov.nasa.jpl.aerie.timeline.Interval; +import gov.nasa.jpl.aerie.timeline.collections.Instances; +import gov.nasa.jpl.aerie.timeline.collections.profiles.Real; +import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation; +import gov.nasa.jpl.aerie.timeline.payloads.Segment; +import gov.nasa.jpl.aerie.timeline.payloads.activities.AnyInstance; +import gov.nasa.jpl.aerie.timeline.payloads.activities.Instance; +import gov.nasa.jpl.aerie.timeline.plan.AeriePostgresPlan; +import gov.nasa.jpl.aerie.timeline.plan.Plan; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import javax.json.Json; +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TimelineRemoteTests { + // Requests + private Playwright playwright; + private HasuraRequests hasura; + + // Per-Test Data + private int modelId; + private int planId; + private int activityId; + private int simDatasetId; + + private Plan plan; + private Connection connection; + private HikariDataSource dataSource; + @BeforeAll + void beforeAll() { + // Setup Requests + playwright = Playwright.create(); + hasura = new HasuraRequests(playwright); + } + + @AfterAll + void afterAll() { + // Cleanup Requests + hasura.close(); + playwright.close(); + } + + @BeforeEach + void beforeEach() throws IOException, InterruptedException, SQLException { + // Insert the Mission Model + try (final var gateway = new GatewayRequests(playwright)) { + modelId = hasura.createMissionModel( + gateway.uploadJarFile(), + "Banananation (e2e tests)", + "aerie_e2e_tests", + "Timeline Remote Tests"); + } + // Insert the Plan + planId = hasura.createPlan( + modelId, + "Test Plan - Timeline Remote Tests", + "1212h", + "2021-01-01T00:00:00Z"); + //Insert the Activity + activityId = hasura.insertActivity( + planId, + "BiteBanana", + "1h", + Json.createObjectBuilder().add("biteSize", 1).build()); + simDatasetId = hasura.awaitSimulation(planId).simDatasetId(); + + + // Connect to the database + + final var hikariConfig = new HikariConfig(); + + hikariConfig.setDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); + + hikariConfig.addDataSourceProperty("serverName", "localhost"); + hikariConfig.addDataSourceProperty("portNumber", 5432); + hikariConfig.addDataSourceProperty("databaseName", "aerie_merlin"); + hikariConfig.addDataSourceProperty("applicationName", "Merlin Server"); + hikariConfig.setUsername(System.getenv("AERIE_USERNAME")); + hikariConfig.setPassword(System.getenv("AERIE_PASSWORD")); + + hikariConfig.setConnectionInitSql("set time zone 'UTC'"); + dataSource = new HikariDataSource(hikariConfig); + connection = dataSource.getConnection(); + + plan = new AeriePostgresPlan(connection, simDatasetId); + } + + @AfterEach + void afterEach() throws IOException, SQLException { + hasura.deletePlan(planId); + hasura.deleteMissionModel(modelId); + connection.close(); + dataSource.close(); + } + + @Test + void queryActivityInstances() { + final var instances = plan.allActivityInstances().collect(); + assertEquals(1, instances.size()); + final var instance = instances.get(0); + assertEquals("BiteBanana", instance.getType()); + assertEquals(activityId, instance.directiveId); + assertEquals(1, instance.inner.arguments.get("biteSize").asInt().get()); + assertEquals(Duration.ZERO, instance.getInterval().duration()); + } + + @Test + void queryActivityDirectives() { + final var directives = plan.allActivityDirectives().collect(); + assertEquals(1, directives.size()); + final var directive = directives.get(0); + assertEquals("BiteBanana", directive.getType()); + assertEquals(activityId, directive.id); + assertEquals(1, directive.inner.arguments.get("biteSize").asInt().get()); + } + + @Test + void queryResources() { + final var fruit = plan.resource("/fruit", Real::deserialize).collect(); + assertIterableEquals( + List.of( + Segment.of(Interval.betweenClosedOpen(Duration.ZERO, Duration.HOUR), new LinearEquation(4.0)), + Segment.of(Interval.betweenClosedOpen(Duration.HOUR, Duration.HOUR.times(1212)), new LinearEquation(3.0)) + ), + fruit + ); + } +} diff --git a/timeline/build.gradle b/timeline/build.gradle index 478ff92968..13ed1a2a17 100644 --- a/timeline/build.gradle +++ b/timeline/build.gradle @@ -14,7 +14,6 @@ repositories { dependencies { implementation project(':merlin-driver') - implementation project(':merlin-sdk') implementation project(':parsing-utilities') testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' From 9e627be054543390173e4d104f736a1c73f51676 Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 7 Mar 2024 16:36:33 -0800 Subject: [PATCH 127/159] Filter activity queries by type --- .../jpl/aerie/e2e/TimelineRemoteTests.java | 13 ++--- .../payloads/activities/AnyDirective.kt | 8 ++- .../payloads/activities/AnyInstance.kt | 17 +++++- .../aerie/timeline/plan/AeriePostgresPlan.kt | 53 +++++++++++-------- .../gov/nasa/jpl/aerie/timeline/plan/Plan.kt | 27 ++++++++-- 5 files changed, 80 insertions(+), 38 deletions(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java index 9f1d2099c2..dfea0dddc3 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/TimelineRemoteTests.java @@ -1,17 +1,15 @@ package gov.nasa.jpl.aerie.e2e; import com.microsoft.playwright.Playwright; -import gov.nasa.jpl.aerie.e2e.utils.BaseURL; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import gov.nasa.jpl.aerie.e2e.utils.HasuraRequests; import gov.nasa.jpl.aerie.timeline.Duration; import gov.nasa.jpl.aerie.timeline.Interval; -import gov.nasa.jpl.aerie.timeline.collections.Instances; import gov.nasa.jpl.aerie.timeline.collections.profiles.Real; import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation; import gov.nasa.jpl.aerie.timeline.payloads.Segment; -import gov.nasa.jpl.aerie.timeline.payloads.activities.AnyInstance; -import gov.nasa.jpl.aerie.timeline.payloads.activities.Instance; import gov.nasa.jpl.aerie.timeline.plan.AeriePostgresPlan; import gov.nasa.jpl.aerie.timeline.plan.Plan; import org.junit.jupiter.api.AfterAll; @@ -21,9 +19,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; - import javax.json.Json; import java.io.IOException; import java.sql.Connection; @@ -117,7 +112,7 @@ void afterEach() throws IOException, SQLException { @Test void queryActivityInstances() { - final var instances = plan.allActivityInstances().collect(); + final var instances = plan.instances().collect(); assertEquals(1, instances.size()); final var instance = instances.get(0); assertEquals("BiteBanana", instance.getType()); @@ -128,7 +123,7 @@ void queryActivityInstances() { @Test void queryActivityDirectives() { - final var directives = plan.allActivityDirectives().collect(); + final var directives = plan.directives().collect(); assertEquals(1, directives.size()); final var directive = directives.get(0); assertEquals("BiteBanana", directive.getType()); diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt index 9ad79df43a..14f4ee11f7 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyDirective.kt @@ -1,8 +1,14 @@ package gov.nasa.jpl.aerie.timeline.payloads.activities import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import kotlin.jvm.optionals.getOrNull /** A general-purpose container for representing the arguments any type of activity directive. */ data class AnyDirective( /***/ @JvmField val arguments: Map -) +) { + /***/ companion object { + /** Converts a [SerializedValue] object containing activity arguments into an [AnyDirective] object. */ + @JvmStatic fun deserialize(attributes: SerializedValue) = AnyDirective(attributes.asMap().getOrNull()!!) + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt index 3123568776..1ec6569818 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/payloads/activities/AnyInstance.kt @@ -1,9 +1,24 @@ package gov.nasa.jpl.aerie.timeline.payloads.activities import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue +import gov.nasa.jpl.aerie.timeline.plan.AeriePostgresPlan +import kotlin.jvm.optionals.getOrNull /** A general-purpose container for representing the arguments and computed attributes of any type of activity instance. */ data class AnyInstance( /***/ @JvmField val arguments: Map, /***/ @JvmField val computedAttributes: SerializedValue -) +) { + /***/ companion object { + /** + * Converts a [SerializedValue] object containing activity arguments and computed attributes to an [AnyInstance] object. + */ + fun deserialize(attributes: SerializedValue): AnyInstance { + val arguments = attributes.asMap().getOrNull()!!["arguments"]?.asMap()?.getOrNull() + ?: throw AeriePostgresPlan.DatabaseError("Could not get arguments from attributes: $attributes") + val computedAttributes = attributes.asMap().getOrNull()!!["computedAttributes"] + ?: throw AeriePostgresPlan.DatabaseError("Could not get computed attributes from attributes: $attributes") + return AnyInstance(arguments, computedAttributes) + } + } +} diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt index 76178967ae..f6fca17785 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/AeriePostgresPlan.kt @@ -166,27 +166,30 @@ data class AeriePostgresPlan( return result.getSuccessOrThrow { DatabaseError(it.toString()) } } - private val activityInstancesStatement = c.prepareStatement( - "select start_offset, duration, attributes, activity_type_name, id from simulated_activity where simulation_dataset_id = ?;" + private val allInstancesStatement = c.prepareStatement( + "select start_offset, duration, attributes, activity_type_name, id from simulated_activity" + + " where simulation_dataset_id = ?;" ) - override fun allActivityInstances(): Instances { - activityInstancesStatement.clearParameters() - activityInstancesStatement.setInt(1, simDatasetId) + private val filteredInstancesStatement = c.prepareStatement( + "select start_offset, duration, attributes, activity_type_name, id from simulated_activity" + + " where simulation_dataset_id = ? and activity_type_name = ?;" + ) + override fun instances(type: String?, deserializer: (SerializedValue) -> A): Instances { + val statement = if (type == null) allInstancesStatement else filteredInstancesStatement + statement.clearParameters() + statement.setInt(1, simDatasetId) + if (type != null) statement.setString(2, type); intervalStyleStatement.execute() - val response = activityInstancesStatement.executeQuery() - val result = mutableListOf>() + val response = statement.executeQuery() + val result = mutableListOf>() while (response.next()) { val start = Duration.parseISO8601(response.getString(1)) val id = response.getLong(5) val attributesString = response.getString(3) val attributes = parseJson(attributesString) val directiveId = attributes.asMap().getOrNull()?.get("directiveId")?.asInt()?.getOrNull() - val arguments = attributes.asMap().getOrNull()!!["arguments"]?.asMap()?.getOrNull() - ?: throw DatabaseError("Could not get arguments from attributes: $attributesString") - val computedAttributes = attributes.asMap().getOrNull()!!["computedAttributes"] - ?: throw DatabaseError("Could not get computed attributes from attributes: $attributesString") result.add(Instance( - AnyInstance(arguments, computedAttributes), + deserializer(attributes), response.getString(4), id, directiveId, @@ -196,23 +199,27 @@ data class AeriePostgresPlan( return Instances(result) } - private val activityDirectivesStatement = c.prepareStatement( + private val allDirectivesStatement = c.prepareStatement( "select name, start_offset, type, arguments, id from activity_directive where plan_id = ?" + " and start_offset > ?::interval and start_offset < ?::interval;" ) - override fun allActivityDirectives() = BaseTimeline(::Directives) { opts -> - activityDirectivesStatement.clearParameters() - activityDirectivesStatement.setInt(1, planInfo.id) - activityDirectivesStatement.setString(2, opts.bounds.start.toISO8601()) - activityDirectivesStatement.setString(3, opts.bounds.end.toISO8601()) + private val filteredDirectivesStatement = c.prepareStatement( + "select name, start_offset, type, arguments, id from activity_directive where plan_id = ?" + + " and start_offset > ?::interval and start_offset < ?::interval and type = ?;" + ) + override fun directives(type: String?, deserializer: (SerializedValue) -> A) = BaseTimeline(::Directives) { opts -> + val statement = if (type == null) allDirectivesStatement else filteredDirectivesStatement + statement.clearParameters() + statement.setInt(1, planInfo.id) + statement.setString(2, opts.bounds.start.toISO8601()) + statement.setString(3, opts.bounds.end.toISO8601()) + if (type != null) statement.setString(4, type) intervalStyleStatement.execute() - val response = activityDirectivesStatement.executeQuery() - val result = mutableListOf>() + val response = statement.executeQuery() + val result = mutableListOf>() while (response.next()) { result.add(Directive( - AnyDirective( - parseJson(response.getString(4)).asMap().getOrNull()!! - ), + deserializer(parseJson(response.getString(4))), response.getString(1), response.getLong(5), response.getString(3), diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt index 0932050f5f..a0c0452e32 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/plan/Plan.kt @@ -32,8 +32,27 @@ interface Plan { */ fun > resource(name: String, ctor: (List>) -> TL): TL - /** Query all activity instances. */ - fun allActivityInstances(): Instances - /** Query all activity directives. */ - fun allActivityDirectives(): Directives + /** + * Query activity instances. + * + * @param type Activity type name to filter by; queries all activities if null. + * @param deserializer a function from [SerializedValue] to an inner payload type + */ + fun instances(type: String?, deserializer: (SerializedValue) -> A): Instances + /** Queries activity instances, filtered by type, deserializing them as [AnyInstance]. **/ + fun instances(type: String) = instances(type, AnyInstance::deserialize) + /** Queries all activity instances, deserializing them as [AnyInstance]. **/ + fun instances() = instances(null, AnyInstance::deserialize) + + /** + * Query activity directives. + * + * @param type Activity type name to filter by; queries all activities if null. + * @param deserializer a function from [SerializedValue] to an inner payload type + */ + fun directives(type: String?, deserializer: (SerializedValue) -> A): Directives + /** Queries activity directives, filtered by type, deserializing them as [AnyDirective]. **/ + fun directives(type: String) = directives(type, AnyDirective::deserialize) + /** Queries all activity directives, deserializing them as [AnyDirective]. **/ + fun directives() = directives(null, AnyDirective::deserialize) } From e55467379afcbf5e60ff70064f10c1f62eb4d41e Mon Sep 17 00:00:00 2001 From: JoelCourtney Date: Thu, 7 Mar 2024 16:45:03 -0800 Subject: [PATCH 128/159] Rename numeric conversion functions --- .../timeline/collections/profiles/Numbers.kt | 8 ++++---- .../aerie/timeline/collections/profiles/Real.kt | 16 ++++++++-------- .../timeline/ops/numeric/SerialNumericOps.kt | 9 +++++---- .../timeline/ops/numeric/SerialNumericOpsTest.kt | 10 +++++----- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt index 24d3b233de..b0733115d6 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Numbers.kt @@ -38,8 +38,8 @@ data class Numbers(private val timeline: Timeline, Numbers constructor(vararg segments: Segment): this(segments.asList()) constructor(segments: List>): this(BaseTimeline(::Numbers, preprocessList(segments, Segment::valueEquals))) - override fun toSerialLinear() = mapValues(::Real) { LinearEquation(it.value.toDouble()) } - override fun toSerialPrimitiveNumbers(message: String?) = this + override fun toReal() = mapValues(::Real) { LinearEquation(it.value.toDouble()) } + override fun toNumbers(message: String?) = this /* Due to the fact there is no superinterface for numbers that includes any arithmetic @@ -113,7 +113,7 @@ data class Numbers(private val timeline: Timeline, Numbers /** Divides this by a constant number. */ operator fun div(n: Number) = div(Numbers(n)) /** Divides this by a linear profile. */ - operator fun div(other: Real) = this / other.toSerialPrimitiveNumbers("Cannot divide by a non-piecewise-constant divisor.") + operator fun div(other: Real) = this / other.toNumbers("Cannot divide by a non-piecewise-constant divisor.") /** * Calculates this raised to the power of another primitive numeric profile. @@ -128,7 +128,7 @@ data class Numbers(private val timeline: Timeline, Numbers /** Raises this to the power of a constant number. */ infix fun pow(n: Number) = pow(Numbers(n)) /** Raises this to the power of a linear profile. */ - infix fun pow(other: Real) = this pow other.toSerialPrimitiveNumbers("Cannot apply a non-piecewise-constant exponent.") + infix fun pow(other: Real) = this pow other.toNumbers("Cannot apply a non-piecewise-constant exponent.") /** Returns a [Booleans] that is true when this is less than another primitive numeric profile. */ infix fun lessThan(other: Numbers<*>) = diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt index 6e0e164224..3311c7cb8b 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/collections/profiles/Real.kt @@ -25,7 +25,7 @@ data class Real(private val timeline: Timeline, Real>): constructor(vararg segments: Segment): this(segments.asList()) constructor(segments: List>): this(BaseTimeline(::Real, preprocessList(segments, Segment::valueEquals))) - override fun toSerialLinear() = this + override fun toReal() = this override fun LinearEquation.toLinear() = this /** @@ -33,14 +33,14 @@ data class Real(private val timeline: Timeline, Real>): * * @param message error message to throw if this is not piece-wise constant. */ - override fun toSerialPrimitiveNumbers(message: String?) = mapValues(::Numbers) { + override fun toNumbers(message: String?) = mapValues(::Numbers) { if (it.value.isConstant()) it.value.initialValue else if (message == null) throw RealOpException("Cannot convert a non-piecewise-constant linear equation to a constant number. (at time ${it.interval.start})") else throw RealOpException("$message (at time ${it.interval.start})") } /** Adds this and another numeric profile. */ - operator fun plus(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, _ -> + operator fun plus(other: SerialNumericOps<*, *>) = map2Values(other.toReal()) { l, r, _ -> val shiftedRight = r.shiftInitialTime(l.initialTime) LinearEquation(l.initialTime, l.initialValue + shiftedRight.initialValue, l.rate + r.rate) } @@ -49,7 +49,7 @@ data class Real(private val timeline: Timeline, Real>): operator fun plus(n: Number) = plus(Numbers(n)) /** Subtracts another numeric profile from this. */ - operator fun minus(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, _ -> + operator fun minus(other: SerialNumericOps<*, *>) = map2Values(other.toReal()) { l, r, _ -> val shiftedRight = r.shiftInitialTime(l.initialTime) LinearEquation(l.initialTime, l.initialValue - shiftedRight.initialValue, l.rate - r.rate) } @@ -62,7 +62,7 @@ data class Real(private val timeline: Timeline, Real>): * * @throws RealOpException if both profiles have non-zero rate at the same time. */ - operator fun times(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, i -> + operator fun times(other: SerialNumericOps<*, *>) = map2Values(other.toReal()) { l, r, i -> if (!l.isConstant() && !r.isConstant()) throw RealOpException("Cannot multiply two linear equations that are non-constant at the same time (at time ${i.start})") val shiftedRight = r.shiftInitialTime(l.initialTime) val newRate = l.rate * shiftedRight.initialValue + r.rate * l.initialValue @@ -77,7 +77,7 @@ data class Real(private val timeline: Timeline, Real>): * * @throws RealOpException if the divisor has a non-zero rate at any time that the dividend is defined. */ - operator fun div(other: SerialNumericOps<*, *>) = map2Values(other.toSerialLinear()) { l, r, i -> + operator fun div(other: SerialNumericOps<*, *>) = map2Values(other.toReal()) { l, r, i -> if (!r.isConstant()) throw RealOpException("Cannot divide by a non-piecewise-constant linear equation (at time ${i.start})") LinearEquation(l.initialTime, l.initialValue / r.initialValue, l.rate / r.initialValue) } @@ -92,7 +92,7 @@ data class Real(private val timeline: Timeline, Real>): * or if the base has a non-zero rate at any time that the exponent is defined and not * either 0 or 1. */ - infix fun pow(exp: SerialNumericOps<*, *>) = map2Values(exp.toSerialLinear()) { l, r, i -> + infix fun pow(exp: SerialNumericOps<*, *>) = map2Values(exp.toReal()) { l, r, i -> if (!r.isConstant()) throw RealOpException("Cannot apply a non-piecewise-constant exponent (at time ${i.start}") if (r.initialValue == 0.0) LinearEquation(1.0) else if (r.initialValue == 1.0) l @@ -134,7 +134,7 @@ data class Real(private val timeline: Timeline, Real>): infix fun greaterThanOrEqualTo(n: Number) = greaterThanOrEqualTo(Numbers(n)) private fun inequalityHelper(other: SerialNumericOps<*, *>, f: LinearEquation.(LinearEquation) -> Booleans) = - flatMap2Values(::Booleans, other.toSerialLinear()) { l, r, _ -> l.f(r) } + flatMap2Values(::Booleans, other.toReal()) { l, r, _ -> l.f(r) } override fun changes() = diff --git a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt index 0b840f482a..853c350142 100644 --- a/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt +++ b/timeline/src/main/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOps.kt @@ -12,9 +12,10 @@ import gov.nasa.jpl.aerie.timeline.payloads.LinearEquation * Operations for profiles that represent numbers. */ interface SerialNumericOps>: SerialSegmentOps, NumericOps { - /** [(DOC)][toSerialLinear] Converts the profile to a linear profile, a.k.a. [Real] (no-op if it already was linear). */ - fun toSerialLinear(): Real - fun toSerialPrimitiveNumbers(message: String? = null): Numbers<*> + /** [(DOC)][toReal] Converts the profile to a linear profile, a.k.a. [Real] (no-op if it already was linear). */ + fun toReal(): Real + /** [(DOC)][toNumbers] Converts the profile to a constant numbers profile, a.k.a. [Numbers] (no-op if it already was [Numbers]). */ + fun toNumbers(message: String? = null): Numbers<*> /** * [(DOC)][integrate] Calculates the integral of this profile, starting from zero. @@ -30,7 +31,7 @@ interface SerialNumericOps>: SerialSegme * @param unit length of the time basis vector */ fun integrate(unit: Duration = Duration.SECOND) = - toSerialPrimitiveNumbers("Cannot integrate a non-piecewise-constant linear profile.").unsafeOperate(::Real) { opts -> + toNumbers("Cannot integrate a non-piecewise-constant linear profile.").unsafeOperate(::Real) { opts -> val segments = collect(opts) val result = mutableListOf>() val baseRate = Duration.SECOND.ratioOver(unit) diff --git a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt index 3f8e9196f4..fd4629397b 100644 --- a/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt +++ b/timeline/src/test/kotlin/gov/nasa/jpl/aerie/timeline/ops/numeric/SerialNumericOpsTest.kt @@ -25,11 +25,11 @@ class SerialNumericOpsTest { assertIterableEquals( real.collect(), - real.toSerialLinear().collect() + real.toReal().collect() ) assertIterableEquals( real.collect(), - numbers.toSerialLinear().collect() + numbers.toReal().collect() ) } @@ -46,17 +46,17 @@ class SerialNumericOpsTest { assertIterableEquals( numbers.collect(), - numbers.toSerialPrimitiveNumbers().collect() + numbers.toNumbers().collect() ) assertIterableEquals( numbers.toDoubles().collect(), - real.toSerialPrimitiveNumbers().collect() + real.toNumbers().collect() ) assertThrows { Real( Segment(seconds(0)..seconds(1), LinearEquation(seconds(0), 1.0, 1.0)) - ).toSerialPrimitiveNumbers().collect() + ).toNumbers().collect() } } From ad3381005b421766a29cdc791abea0b53ef2121f Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 7 Mar 2024 15:39:13 -0800 Subject: [PATCH 129/159] Add optional `force` parameter to Simulate action - Update parser in MerlinBindings to reflect this change --- deployment/hasura/metadata/actions.graphql | 2 +- .../jpl/aerie/merlin/server/http/HasuraParsers.java | 12 ++++++++++++ .../jpl/aerie/merlin/server/http/MerlinBindings.java | 3 ++- .../jpl/aerie/merlin/server/models/HasuraAction.java | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/deployment/hasura/metadata/actions.graphql b/deployment/hasura/metadata/actions.graphql index 436134dabc..4ea491ca87 100644 --- a/deployment/hasura/metadata/actions.graphql +++ b/deployment/hasura/metadata/actions.graphql @@ -97,7 +97,7 @@ type Query { } type Query { - simulate(planId: Int!): MerlinSimulationResponse + simulate(planId: Int!, force: Boolean): MerlinSimulationResponse } type Query { diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java index f88e507f40..cb3aa1bcea 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/HasuraParsers.java @@ -7,6 +7,7 @@ import java.util.Optional; +import static gov.nasa.jpl.aerie.json.BasicParsers.boolP; import static gov.nasa.jpl.aerie.json.BasicParsers.listP; import static gov.nasa.jpl.aerie.json.BasicParsers.longP; import static gov.nasa.jpl.aerie.json.BasicParsers.mapP; @@ -54,6 +55,17 @@ private static JsonParser> hasura .field("planId", planIdP) .map(HasuraAction.PlanInput::new, HasuraAction.PlanInput::planId)); + public static final JsonParser> hasuraSimulateActionP + = hasuraActionF( + productP + .field("planId", planIdP) + .optionalField("force", nullableP(boolP)) + .map( + untuple((planId, force) -> new HasuraAction.SimulateInput(planId, force.flatMap($ -> $))), + $ -> tuple($.planId(), Optional.of($.force())) + ) + ); + public static final JsonParser> hasuraConstraintsViolationsActionP = hasuraActionF( productP diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java index 9b4575e8ce..4b6bcc9a2f 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java @@ -35,6 +35,7 @@ import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraActivityBulkActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraConstraintsCodeAction; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraConstraintsViolationsActionP; +import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraSimulateActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraUploadExternalDatasetActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraMissionModelActionP; import static gov.nasa.jpl.aerie.merlin.server.http.HasuraParsers.hasuraMissionModelArgumentsActionP; @@ -175,7 +176,7 @@ private void getResourceTypes(final Context ctx) { private void getSimulationResults(final Context ctx) { try { - final var body = parseJson(ctx.body(), hasuraPlanActionP); + final var body = parseJson(ctx.body(), hasuraSimulateActionP); final var planId = body.input().planId(); this.checkPermissions(Action.simulate, body.session(), planId); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java index 860e034be2..a35d29d106 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/models/HasuraAction.java @@ -15,6 +15,7 @@ public sealed interface Input { } public record MissionModelInput(String missionModelId) implements Input { } public record PlanInput(PlanId planId) implements Input { } + public record SimulateInput(PlanId planId, Optional force) implements Input {} public record ConstraintViolationsInput(PlanId planId, Optional simulationDatasetId) implements Input { } public record ActivityInput(String missionModelId, String activityTypeName, From 3da63cb33aebcc429bb6ab6808442c75082e0342 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 7 Mar 2024 16:12:22 -0800 Subject: [PATCH 130/159] Pass `force resim` to SimulationService --- .../nasa/jpl/aerie/merlin/server/http/MerlinBindings.java | 3 ++- .../merlin/server/services/CachedSimulationService.java | 7 ++++++- .../merlin/server/services/GetSimulationResultsAction.java | 4 ++-- .../aerie/merlin/server/services/SimulationService.java | 2 +- .../merlin/server/services/UncachedSimulationService.java | 4 ++-- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java index 4b6bcc9a2f..1d45f90867 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/http/MerlinBindings.java @@ -178,10 +178,11 @@ private void getSimulationResults(final Context ctx) { try { final var body = parseJson(ctx.body(), hasuraSimulateActionP); final var planId = body.input().planId(); + final var force = body.input().force().orElse(false); this.checkPermissions(Action.simulate, body.session(), planId); - final var response = this.simulationAction.run(planId, body.session()); + final var response = this.simulationAction.run(planId, force, body.session()); ctx.result(ResponseSerializers.serializeSimulationResultsResponse(response).toString()); } catch (final InvalidEntityException ex) { ctx.status(400).result(ResponseSerializers.serializeInvalidEntityException(ex).toString()); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java index 98f0849a28..b9ba6a9469 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java @@ -14,7 +14,12 @@ public record CachedSimulationService ( ) implements SimulationService { @Override - public ResultsProtocol.State getSimulationResults(final PlanId planId, final RevisionData revisionData, final String requestedBy) { + public ResultsProtocol.State getSimulationResults( + final PlanId planId, + final boolean forceResim, + final RevisionData revisionData, + final String requestedBy) + { final var cell$ = this.store.lookup(planId); if (cell$.isPresent()) { return cell$.get().get(); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java index bd3852d190..abbb3c6611 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/GetSimulationResultsAction.java @@ -35,11 +35,11 @@ public GetSimulationResultsAction( this.simulationService = Objects.requireNonNull(simulationService); } - public Response run(final PlanId planId, final HasuraAction.Session session) + public Response run(final PlanId planId, final boolean forceResim, final HasuraAction.Session session) throws NoSuchPlanException, MissionModelService.NoSuchMissionModelException { final var revisionData = this.planService.getPlanRevisionData(planId); - final var response = this.simulationService.getSimulationResults(planId, revisionData, session.hasuraUserId()); + final var response = this.simulationService.getSimulationResults(planId, forceResim, revisionData, session.hasuraUserId()); if (response instanceof ResultsProtocol.State.Pending r) { return new Response.Pending(r.simulationDatasetId()); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java index d8b8de2a20..cd733c982a 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/SimulationService.java @@ -9,7 +9,7 @@ import java.util.Optional; public interface SimulationService { - ResultsProtocol.State getSimulationResults(PlanId planId, RevisionData revisionData, final String requestedBy); + ResultsProtocol.State getSimulationResults(PlanId planId, final boolean forceResim, RevisionData revisionData, final String requestedBy); Optional get(PlanId planId, RevisionData revisionData); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java index 7ece2efab2..1350e35aa0 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/UncachedSimulationService.java @@ -14,7 +14,7 @@ public record UncachedSimulationService ( ) implements SimulationService { @Override - public ResultsProtocol.State getSimulationResults(final PlanId planId, final RevisionData revisionData, final String requestedBy) { + public ResultsProtocol.State getSimulationResults(final PlanId planId, final boolean forceResim, final RevisionData revisionData, final String requestedBy) { if (!(revisionData instanceof InMemoryRevisionData inMemoryRevisionData)) { throw new Error("UncachedSimulationService only accepts InMemoryRevisionData"); } @@ -40,7 +40,7 @@ public ResultsProtocol.State getSimulationResults(final PlanId planId, final Rev @Override public Optional get(final PlanId planId, final RevisionData revisionData) { return Optional.ofNullable( - getSimulationResults(planId, revisionData, null) instanceof ResultsProtocol.State.Success s ? + getSimulationResults(planId, false, revisionData, null) instanceof ResultsProtocol.State.Success s ? s.results() : null); } From c6c1516c0b60fe9b720ddd6f96103e578f46e601 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 7 Mar 2024 16:12:49 -0800 Subject: [PATCH 131/159] Add `forceAllocate` to ResultsCellRepository --- .../InMemoryResultsCellRepository.java | 5 +++ .../server/remotes/ResultsCellRepository.java | 1 + .../PostgresResultsCellRepository.java | 14 ++++++++ ...SimulationConfigurationRevisionAction.java | 35 +++++++++++++++++++ .../services/CachedSimulationService.java | 16 +++++---- 5 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java index 93402fa850..dd9e6a2773 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/InMemoryResultsCellRepository.java @@ -46,6 +46,11 @@ public ResultsProtocol.OwnerRole allocate(final PlanId planId, final String requ } } + @Override + public ResultsProtocol.OwnerRole forceAllocate(PlanId planId, String requestedBy) { + return allocate(planId, requestedBy); + } + @Override public Optional claim(final PlanId planId, final Long datasetId) { return Optional.empty(); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java index df80e37b0b..8484efe00b 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/ResultsCellRepository.java @@ -9,6 +9,7 @@ public interface ResultsCellRepository { ResultsProtocol.OwnerRole allocate(PlanId planId, String requestedBy); + ResultsProtocol.OwnerRole forceAllocate(PlanId planId, String requestedBy); Optional claim(PlanId planId, Long datasetId); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java index 7be407e4f6..15437d6526 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/PostgresResultsCellRepository.java @@ -88,6 +88,20 @@ public ResultsProtocol.OwnerRole allocate(final PlanId planId, final String requ } } + /** + * Forcibly allocate a simulation by updating the Simulation Configuration's revision + */ + @Override + public ResultsProtocol.OwnerRole forceAllocate(PlanId planId, String requestedBy) { + try (final var connection = this.dataSource.getConnection(); + final var updateSimConfig = new UpdateSimulationConfigurationRevisionAction(connection)) { + updateSimConfig.apply(planId.id()); + } catch (final SQLException ex) { + throw new DatabaseException("Failed to allocation simulation cell", ex); + } + return allocate(planId, requestedBy); + } + /** * Claim a simulation * diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java new file mode 100644 index 0000000000..7bd18150dc --- /dev/null +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/remotes/postgres/UpdateSimulationConfigurationRevisionAction.java @@ -0,0 +1,35 @@ +package gov.nasa.jpl.aerie.merlin.server.remotes.postgres; + +import org.intellij.lang.annotations.Language; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/*package-local*/ final class UpdateSimulationConfigurationRevisionAction implements AutoCloseable { + private final @Language("SQL") String sql = """ + update simulation + set revision = revision + where plan_id = ?; + """; + + private final PreparedStatement statement; + + public UpdateSimulationConfigurationRevisionAction(final Connection connection) throws SQLException { + this.statement = connection.prepareStatement(sql); + } + + public void apply(final long planId) + throws SQLException + { + this.statement.setLong(1, planId); + final var count = this.statement.executeUpdate(); + if (count > 1) throw new Error("More than one row affected by sim config update by unique key. Is the database corrupted?"); + if (count == 0) throw new SQLException("No simulation configuration exists for plan "+planId); + } + + @Override + public void close() throws SQLException { + this.statement.close(); + } +} diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java index b9ba6a9469..01fd795d22 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/CachedSimulationService.java @@ -20,16 +20,20 @@ public ResultsProtocol.State getSimulationResults( final RevisionData revisionData, final String requestedBy) { + // If force resimulation is enabled, allocate a new cell regardless of whether there was already a valid cell + if (forceResim) { + return this.store.forceAllocate(planId, requestedBy).get(); + } + final var cell$ = this.store.lookup(planId); if (cell$.isPresent()) { return cell$.get().get(); - } else { - // Allocate a fresh cell. - final var cell = this.store.allocate(planId, requestedBy); - - // Return the current value of the reader; if it's incomplete, the caller can check it again later. - return cell.get(); } + + // Allocate a fresh cell. + final var cell = this.store.allocate(planId, requestedBy); + // Return the current value of the reader; if it's incomplete, the caller can check it again later. + return cell.get(); } @Override From b2ee901605a6f5d7dc850e17f407bd08cc36703b Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 11 Mar 2024 09:56:27 -0700 Subject: [PATCH 132/159] Add new e2eTests --- .../gov/nasa/jpl/aerie/e2e/BindingsTests.java | 55 +++++++++++++++--- .../nasa/jpl/aerie/e2e/SimulationTests.java | 46 +++++++++++++++ .../e2e/types/SimulationConfiguration.java | 25 ++++++++ .../gov/nasa/jpl/aerie/e2e/utils/GQL.java | 20 +++++++ .../jpl/aerie/e2e/utils/HasuraRequests.java | 58 +++++++++++++++++++ 5 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java index 571b9a24fc..7d652eedc5 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/BindingsTests.java @@ -21,6 +21,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import javax.json.Json; import javax.json.JsonArray; @@ -30,8 +33,10 @@ import java.io.StringReader; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Named.named; /** * Test the Action Bindings for the Merlin and Scheduler Servers @@ -156,16 +161,17 @@ void invalidPlanId() { // Returns a 404 if the PlanId is invalid // message is "no such plan" final String data = Json.createObjectBuilder() - .add("action", Json.createObjectBuilder().add("name", "simulate")) - .add("input", Json.createObjectBuilder().add("planId", -1)) - .add("request_query", "") - .add("session_variables", admin.getSession()) - .build() - .toString(); + .add("action", Json.createObjectBuilder().add("name", "simulate")) + .add("input", Json.createObjectBuilder().add("planId", -1)) + .add("request_query", "") + .add("session_variables", admin.getSession()) + .build() + .toString(); final var response = request.post("/getSimulationResults", RequestOptions.create().setData(data)); assertEquals(404, response.status()); assertEquals("no such plan", getBody(response).getString("message")); } + @Test void unauthorized() { // Returns a 403 if Unauthorized @@ -178,10 +184,12 @@ void unauthorized() { .toString(); final var response = request.post("/getSimulationResults", RequestOptions.create().setData(data)); assertEquals(403, response.status()); - assertEquals("User '"+nonOwner.name()+"' with role 'user' cannot perform 'simulate' because they are not " - + "a 'PLAN_OWNER_COLLABORATOR' for plan with id '"+planId+"'", - getBody(response).getString("message")); + assertEquals( + "User '" + nonOwner.name() + "' with role 'user' cannot perform 'simulate' because they are not " + + "a 'PLAN_OWNER_COLLABORATOR' for plan with id '" + planId + "'", + getBody(response).getString("message")); } + @Test void valid() throws InterruptedException { // Returns a 200 otherwise @@ -199,6 +207,35 @@ void valid() throws InterruptedException { // Delay 1s to allow any workers to finish with the request Thread.sleep(1000); } + + static Stream forceArgs() { + return Stream.of( + Arguments.arguments(named("valid, force is NULL", JsonValue.NULL)), + Arguments.arguments(named("valid, force is TRUE", JsonValue.TRUE)), + Arguments.arguments(named("valid, force is FALSE", JsonValue.FALSE)) + ); + } + + @ParameterizedTest + @MethodSource("forceArgs") + void validWithForce(JsonValue force) throws InterruptedException { + // Returns a 200 otherwise + // "status" is not "failed" + final String data = Json.createObjectBuilder() + .add("action", Json.createObjectBuilder().add("name", "simulate")) + .add( + "input", + Json.createObjectBuilder().add("planId", planId).add("force", force)) + .add("request_query", "") + .add("session_variables", admin.getSession()) + .build() + .toString(); + final var response = request.post("/getSimulationResults", RequestOptions.create().setData(data)); + assertEquals(200, response.status()); + assertNotEquals("failed", getBody(response).getString("status")); + // Delay 1s to allow any workers to finish with the request + Thread.sleep(1000); + } } @Nested diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java index fb19099fd9..28a482e199 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java @@ -319,4 +319,50 @@ void cancelingSimReturnsPartialResults() throws IOException { assertEquals(startedActivities, results.activities().size()); } } + + @Nested + class ForceResimulation { + private int simulationDatasetId; + @BeforeEach + void beforeEach() throws IOException { + simulationDatasetId = hasura.awaitSimulation(planId).simDatasetId(); + } + @Test + void noResimWhenNull() throws IOException { + assertEquals(simulationDatasetId, hasura.awaitSimulation(planId, null).simDatasetId()); + } + + @Test + void noResimWhenFalse() throws IOException { + assertEquals(simulationDatasetId, hasura.awaitSimulation(planId, false).simDatasetId()); + } + + @Test + void noResimWhenAbsent() throws IOException { + assertEquals(simulationDatasetId, hasura.awaitSimulation(planId).simDatasetId()); + } + + @Test + void resimOnlyUpdatesConfigRevision() throws IOException { + final int planRevision = hasura.getPlanRevision(planId); + final var simConfig = hasura.getSimConfig(planId); + + // Assert forcibly resimming returned a new simulation dataset + final var newSimDatasetId = hasura.awaitSimulation(planId, true).simDatasetId(); + assertNotEquals(simulationDatasetId, newSimDatasetId); + + // Assert that the plan revision is unchanged + assertEquals(planRevision, hasura.getPlanRevision(planId)); + + // Assert that the simulation configuration has only had its revision updated + final var newSimConfig = hasura.getSimConfig(planId); + assertNotEquals(simConfig.revision(), newSimConfig.revision()); + assertEquals(simConfig.id(), newSimConfig.id()); + assertEquals(simConfig.planId(), newSimConfig.planId()); + assertEquals(simConfig.simulationTemplateId(), newSimConfig.simulationTemplateId()); + assertEquals(simConfig.arguments(), newSimConfig.arguments()); + assertEquals(simConfig.simulationStartTime(), newSimConfig.simulationStartTime()); + assertEquals(simConfig.simulationEndTime(), newSimConfig.simulationEndTime()); + } + } } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java new file mode 100644 index 0000000000..3c305faccf --- /dev/null +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/types/SimulationConfiguration.java @@ -0,0 +1,25 @@ +package gov.nasa.jpl.aerie.e2e.types; + +import javax.json.JsonObject; +import java.util.Optional; + +public record SimulationConfiguration( + int id, + int revision, + int planId, + Optional simulationTemplateId, + JsonObject arguments, + String simulationStartTime, + String simulationEndTime +) { + public static SimulationConfiguration fromJSON(JsonObject json) { + return new SimulationConfiguration( + json.getInt("id"), + json.getInt("revision"), + json.getInt("plan_id"), + json.isNull("simulation_template_id") ? Optional.empty() : Optional.of(json.getInt("simulation_template_id")), + json.getJsonObject("arguments"), + json.getString("simulation_start_time"), + json.getString("simulation_end_time")); + } +} diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index 68554dedff..a2dda9d2d1 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -384,6 +384,18 @@ query GetSchedulingRequest($specificationId: Int!, $specificationRev: Int!) { status } }"""), + GET_SIMULATION_CONFIGURATION(""" + query GetSimConfig($planId: Int!) { + sim_config: simulation(where: {plan_id: {_eq:$planId}}) { + id + revision + plan_id + simulation_template_id + arguments + simulation_start_time + simulation_end_time + } + }"""), GET_SIMULATION_DATASET(""" query GetSimulationDataset($id: Int!) { simulationDataset: simulation_dataset_by_pk(id: $id) { @@ -501,6 +513,14 @@ query Simulate($plan_id: Int!) { simulationDatasetId } }"""), + SIMULATE_FORCE(""" + query SimulateForce($plan_id: Int!, $force: Boolean) { + simulate(planId: $plan_id, force: $force){ + status + reason + simulationDatasetId + } + }"""), UPDATE_ACTIVITY_DIRECTIVE_ARGUMENTS(""" mutation updateActivityDirectiveArguments($id: Int!, $plan_id: Int!, $arguments: jsonb!) { updateActivityDirectiveArguments: update_activity_directive_by_pk( diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index d854205198..6d66ea2333 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -260,6 +260,16 @@ private SimulationResponse simulate(int planId) throws IOException { return SimulationResponse.fromJSON(makeRequest(GQL.SIMULATE, variables).getJsonObject("simulate")); } + private SimulationResponse simulateForce(int planId, Boolean force) throws IOException { + final var variables = Json.createObjectBuilder().add("plan_id", planId); + if (force == null) { + variables.add("force", JsonValue.NULL); + } else { + variables.add("force", force); + } + return SimulationResponse.fromJSON(makeRequest(GQL.SIMULATE_FORCE, variables.build()).getJsonObject("simulate")); + } + private SimulationDataset cancelSimulation(int simDatasetId, int timeout) throws IOException { final var variables = Json.createObjectBuilder().add("id", simDatasetId).build(); makeRequest(GQL.CANCEL_SIMULATION, variables); @@ -307,6 +317,47 @@ public SimulationResponse awaitSimulation(int planId, int timeout) throws IOExce throw new TimeoutError("Simulation timed out after " + timeout + " seconds"); } + /** + * Simulate the specified plan, potentially forcibly, with a timeout of 30 seconds + * @param planId the plan to simulate + * @param force whether to forcibly resimulate in the event of an existing dataset. + */ + public SimulationResponse awaitSimulation(int planId, Boolean force) throws IOException { + return awaitSimulation(planId, force, 30); + } + + /** + * Simulate the specified plan, potentially forcibly, with a set timeout + * @param planId the plan to simulate + * @param force whether to forcibly resimulate in the event of an existing dataset. + * @param timeout the length of the timeout, in seconds + */ + public SimulationResponse awaitSimulation(int planId, Boolean force, int timeout) throws IOException { + for (int i = 0; i < timeout; ++i) { + final SimulationResponse response; + // Only use force on the initial request to avoid an infinite loop of making new sim requests + if (i == 0) { + response = simulateForce(planId, force); + } else { + response = simulate(planId); + } + switch (response.status()) { + case "pending", "incomplete" -> { + try { + Thread.sleep(1000); // 1s + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + case "complete" -> { + return response; + } + default -> fail("Simulation returned bad status " + response.status() + " with reason " + response.reason()); + } + } + throw new TimeoutError("Simulation timed out after " + timeout + " seconds"); + } + /** * Start and immediately cancel a simulation with a timeout of 30 seconds * @param planId the plan to simulate @@ -351,6 +402,13 @@ public int getSimulationId(int planId) throws IOException { return makeRequest(GQL.GET_SIMULATION_ID, variables).getJsonArray("simulation").getJsonObject(0).getInt("id"); } + public SimulationConfiguration getSimConfig(int planId) throws IOException { + final var variables = Json.createObjectBuilder().add("planId", planId).build(); + final var simConfig = makeRequest(GQL.GET_SIMULATION_CONFIGURATION, variables).getJsonArray("sim_config"); + assertEquals(1, simConfig.size()); + return SimulationConfiguration.fromJSON(simConfig.getJsonObject(0)); + } + public int insertAndAssociateSimTemplate(int modelId, String description, JsonObject arguments, int simConfigId) throws IOException { From 249266d6838827863dc3b2ce1747251f728ba1e7 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Fri, 15 Mar 2024 17:19:08 -0700 Subject: [PATCH 133/159] Fix background transpiler check for latest command dict/mission model --- sequencing-server/src/backgroundTranspiler.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sequencing-server/src/backgroundTranspiler.ts b/sequencing-server/src/backgroundTranspiler.ts index f78863f882..a049335bac 100644 --- a/sequencing-server/src/backgroundTranspiler.ts +++ b/sequencing-server/src/backgroundTranspiler.ts @@ -15,8 +15,8 @@ export async function backgroundTranspiler(numberOfThreads: number = 2) { } // Fetch latest mission model - const { mission_model_aggregate } = await getLatestMissionModel(graphqlClient); - if (!mission_model_aggregate) { + const { mission_model_aggregate: {aggregate: {max: {id: missionModelId} } } } = await getLatestMissionModel(graphqlClient); + if (!missionModelId) { console.log( '[ Background Transpiler ] Unable to fetch the latest mission model. Aborting background transpiling...', ); @@ -24,16 +24,14 @@ export async function backgroundTranspiler(numberOfThreads: number = 2) { } // Fetch latest command dictionary - const { command_dictionary_aggregate } = await getLatestCommandDictionary(graphqlClient); - if (!command_dictionary_aggregate) { + const { command_dictionary_aggregate: {aggregate: {max: {id: commandDictionaryId} } } } = await getLatestCommandDictionary(graphqlClient); + if (!commandDictionaryId) { console.log( '[ Background Transpiler ] Unable to fetch the latest command dictionary. Aborting background transpiling...', ); return; } - const commandDictionaryId = command_dictionary_aggregate.aggregate.max.id; - const missionModelId = mission_model_aggregate.aggregate.max.id; const { expansion_rule } = await getExpansionRule(graphqlClient, missionModelId, commandDictionaryId); if (expansion_rule === null || expansion_rule.length === 0) { From 98cd5036e0703ba435654fe2467ec9c56628b763 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Mon, 18 Mar 2024 05:46:00 -1000 Subject: [PATCH 134/159] Tine Bug fix that I noticed. I noticed this while I was testing the query refactor branch. I accidentally swapped 'missionID' and 'CommandID' in the Hasura call. --- sequencing-server/src/backgroundTranspiler.ts | 2 +- sequencing-server/src/utils/hasura.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sequencing-server/src/backgroundTranspiler.ts b/sequencing-server/src/backgroundTranspiler.ts index a049335bac..14b6b426f5 100644 --- a/sequencing-server/src/backgroundTranspiler.ts +++ b/sequencing-server/src/backgroundTranspiler.ts @@ -49,7 +49,7 @@ export async function backgroundTranspiler(numberOfThreads: number = 2) { }); const commandTypes = await commandTypescriptDataLoader.load({ - dictionaryId: missionModelId, + dictionaryId: commandDictionaryId, }); if (commandTypes === null) { diff --git a/sequencing-server/src/utils/hasura.ts b/sequencing-server/src/utils/hasura.ts index 29beeb6f58..e6b64c6c01 100644 --- a/sequencing-server/src/utils/hasura.ts +++ b/sequencing-server/src/utils/hasura.ts @@ -453,7 +453,7 @@ export async function getExpansionRule( }>( gql` query GetExpansonLogic { - expansion_rule(where: {authoring_command_dict_id: {_eq: ${missionModelId}}, authoring_mission_model_id: {_eq: ${commandDictionaryId} }}) { + expansion_rule(where: {authoring_command_dict_id: {_eq: ${commandDictionaryId}}, authoring_mission_model_id: {_eq: ${missionModelId} }}) { id activity_type expansion_logic From 1dd4370e1f84994c810ecb3d6d6f02dbf309b6ae Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Fri, 15 Mar 2024 17:14:37 -0700 Subject: [PATCH 135/159] Improve Hasura Queries used in `simulatedActivityBatchLoader.ts` TODO: correctly sum `start_time` and `end_time` --- .../simulatedActivityBatchLoader.ts | 190 ++++++++++++------ 1 file changed, 127 insertions(+), 63 deletions(-) diff --git a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts index 074b7258d5..f3102e3f88 100644 --- a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts +++ b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts @@ -12,34 +12,45 @@ export const simulatedActivitiesBatchLoader: BatchLoader< { graphqlClient: GraphQLClient; activitySchemaDataLoader: InferredDataloader } > = opts => async keys => { const result = await opts.graphqlClient.batchRequests< - { - data: { - simulated_activity: GraphQLSimulatedActivityInstance[]; - }; - }[] + { + data: { + simulation_dataset: { + id: number, + simulation: { + plan: { + id: number, + model_id: number + } + }, + simulation_start_time: string, + dataset: { spans: GQLSpan[]}, + } + }; + }[] >( keys.map(key => ({ document: gql` query ($simulationDatasetId: Int!) { - simulated_activity(where: { simulation_dataset_id: { _eq: $simulationDatasetId } }) { + simulation_dataset: simulation_dataset_by_pk(id: $simulationDatasetId) { id - simulation_dataset { - id - simulation { - plan { - model_id - } + simulation { + plan { + model_id + id + } + } + simulation_start_time + dataset { + spans { + id + attributes + start_offset + duration + activity_type_name: type } } - attributes - start_offset - start_time - end_time - duration - activity_type_name } - } - `, + }`, variables: { simulationDatasetId: key.simulationDatasetId, }, @@ -48,18 +59,37 @@ export const simulatedActivitiesBatchLoader: BatchLoader< return Promise.all( keys.map(async ({ simulationDatasetId }) => { - const simulatedActivities = result.find( - res => res.data.simulated_activity[0]?.simulation_dataset.id === simulationDatasetId, - )?.data.simulated_activity; - if (simulatedActivities === undefined) { + const simulation_dataset = result.find( + res => res.data.simulation_dataset.id === simulationDatasetId, + )?.data.simulation_dataset; + if (simulation_dataset === undefined) { return new ErrorWithStatusCode(`No simulation_dataset with id: ${simulationDatasetId}`, 404); } + + const spans = simulation_dataset.dataset.spans; + + const simulatedActivities: GraphQLSimulatedActivityInstance[] = spans.map(span => { + return { + id: span.id, + simulation_dataset_id: simulation_dataset.id, + plan_id: simulation_dataset.simulation.plan.id, + model_id: simulation_dataset.simulation.plan.model_id, + attributes: span.attributes, + duration: span.duration, + start_offset: span.start_offset, + // TODO: Sum this intervals + durations properly + start_time: simulation_dataset.simulation_start_time + span.start_offset, + end_time: simulation_dataset.simulation_start_time + span.start_offset + span.duration, + activity_type_name: span.activity_type_name + } + } + ) return Promise.all( simulatedActivities.map(async simulatedActivity => mapGraphQLActivityInstance( simulatedActivity, await opts.activitySchemaDataLoader.load({ - missionModelId: simulatedActivity.simulation_dataset.simulation.plan.model_id, + missionModelId: simulation_dataset.simulation.plan.model_id, activityTypeName: simulatedActivity.activity_type_name, }), ), @@ -77,35 +107,43 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa const result = await opts.graphqlClient.batchRequests< { data: { - simulated_activity: GraphQLSimulatedActivityInstance[]; + simulation_dataset: { + id: number, + simulation_start_time: string, + simulation: { + plan: { + id: number, + model_id: number, + } + }, + dataset: {span: GQLSpan} + }; }; }[] >( keys.map(key => ({ document: gql` query ($simulationDatasetId: Int!, $simulatedActivityId: Int!) { - simulated_activity( - where: { simulation_dataset_id: { _eq: $simulationDatasetId }, id: { _eq: $simulatedActivityId } } - ) { + simulation_dataset: simulation_dataset_by_pk(id: $simulationDatasetId) { id - simulation_dataset { - id - simulation { - plan { - model_id - } - plan_id + simulation_start_time + simulation { + plan { + id + model_id + } + } + dataset { + spans: spans(where: {id: {_eq: $simulatedActivityId}}) { + id + attributes + start_offset + duration + activity_type_name: type } } - attributes - start_offset - start_time - end_time - duration - activity_type_name } - } - `, + }`, variables: { simulationDatasetId: key.simulationDatasetId, simulatedActivityId: key.simulatedActivityId, @@ -115,21 +153,42 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa return Promise.all( keys.map(async ({ simulationDatasetId, simulatedActivityId }) => { - const simulatedActivity = result.find( + const simulation_dataset = result.find( res => - res.data.simulated_activity[0]?.simulation_dataset.id === simulationDatasetId && - res.data.simulated_activity[0]?.id === simulatedActivityId, - )?.data.simulated_activity[0]; - if (simulatedActivity === undefined) { + res.data.simulation_dataset.id === simulationDatasetId + )?.data.simulation_dataset; + if (simulation_dataset === undefined) { + return new ErrorWithStatusCode( + `No simulation_dataset with id: ${simulationDatasetId}`, + 404, + ); + } + + const span = simulation_dataset.dataset.span + if(span === undefined) { return new ErrorWithStatusCode( - `No simulation_dataset with id: ${simulationDatasetId} and simulated activity id: ${simulatedActivityId}`, - 404, + `No simulation_dataset with id: ${simulationDatasetId} and simulated activity id: ${simulatedActivityId}`, + 404, ); } + + const simulatedActivity: GraphQLSimulatedActivityInstance = { + id: span.id, + simulation_dataset_id: simulation_dataset.id, + plan_id: simulation_dataset.simulation.plan.id, + model_id: simulation_dataset.simulation.plan.model_id, + attributes: span.attributes, + duration: span.duration, + start_offset: span.start_offset, + // TODO: Sum this intervals + durations properly + start_time: simulation_dataset.simulation_start_time + span.start_offset, + end_time: simulation_dataset.simulation_start_time + span.start_offset + span.duration, + activity_type_name: span.activity_type_name + } return mapGraphQLActivityInstance( simulatedActivity, await opts.activitySchemaDataLoader.load({ - missionModelId: simulatedActivity.simulation_dataset.simulation.plan.model_id, + missionModelId: simulation_dataset.simulation.plan.model_id, activityTypeName: simulatedActivity.activity_type_name, }), ); @@ -173,20 +232,25 @@ export interface SimulatedActivity< activityTypeName: string; } +export interface GQLSpan< + ActivityArguments extends Record = Record, + ActivityComputedAttributes extends Record = Record, +> { + id: number + attributes: GraphQLSimulatedActivityAttributes; + start_offset: string; + duration: string; + activity_type_name: string; +} + export interface GraphQLSimulatedActivityInstance< ActivityArguments extends Record = Record, ActivityComputedAttributes extends Record = Record, > { id: number; - simulation_dataset: { - id: number; - simulation: { - plan: { - model_id: number; - }; - plan_id: number; - }; - }; + simulation_dataset_id: number; + plan_id: number; + model_id: number; attributes: GraphQLSimulatedActivityAttributes; duration: string; start_offset: string; @@ -202,7 +266,7 @@ export function mapGraphQLActivityInstance( return { simulationDataset: { simulation: { - planId: activityInstance.simulation_dataset.simulation.plan_id, + planId: activityInstance.model_id }, }, id: activityInstance.id, @@ -210,7 +274,7 @@ export function mapGraphQLActivityInstance( startOffset: Temporal.Duration.from(parse(activityInstance.start_offset).toISOString()), startTime: Temporal.Instant.from(activityInstance.start_time), endTime: activityInstance.end_time ? Temporal.Instant.from(activityInstance.end_time) : null, - simulationDatasetId: activityInstance.simulation_dataset.id, + simulationDatasetId: activityInstance.simulation_dataset_id, activityTypeName: activityInstance.activity_type_name, attributes: { arguments: Object.entries(activityInstance.attributes.arguments).reduce((acc, [key, value]) => { From eec561b5dbd874179b40ede84fbb7618482ea009 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Fri, 15 Mar 2024 17:16:35 -0700 Subject: [PATCH 136/159] Improve DB Queries in `seqJson.ts` --- sequencing-server/src/routes/seqjson.ts | 66 +++++++++++-------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/sequencing-server/src/routes/seqjson.ts b/sequencing-server/src/routes/seqjson.ts index 14a3fc2b40..3aeedd93f2 100644 --- a/sequencing-server/src/routes/seqjson.ts +++ b/sequencing-server/src/routes/seqjson.ts @@ -137,25 +137,22 @@ seqjsonRouter.post('/get-seqjson-for-seqid-and-simulation-dataset', async (req, errors: ReturnType[] | null; }>( ` - with joined_table as (select activity_instance_commands.commands, - activity_instance_commands.activity_instance_id, - activity_instance_commands.errors, - activity_instance_commands.expansion_run_id - from sequence - join sequence_to_simulated_activity - on sequence.seq_id = sequence_to_simulated_activity.seq_id and - sequence.simulation_dataset_id = - sequence_to_simulated_activity.simulation_dataset_id - join activity_instance_commands - on sequence_to_simulated_activity.simulated_activity_id = - activity_instance_commands.activity_instance_id - join expansion_run - on activity_instance_commands.expansion_run_id = expansion_run.id - where sequence.seq_id = $2 - and sequence.simulation_dataset_id = $1), - max_values as (select activity_instance_id, max(expansion_run_id) as max_expansion_run_id - from joined_table - group by activity_instance_id) + with joined_table as ( + select aic.commands, + aic.activity_instance_id, + aic.errors, + aic.expansion_run_id + from sequence_to_simulated_activity ssa + join activity_instance_commands aic + on ssa.simulated_activity_id = aic.activity_instance_id + where (ssa.simulation_dataset_id, ssa.seq_id) = ($1, $2)), + max_values as ( + select + activity_instance_id, + max(expansion_run_id) as max_expansion_run_id + from joined_table + group by activity_instance_id + ) select joined_table.commands, joined_table.activity_instance_id, joined_table.errors @@ -283,23 +280,16 @@ seqjsonRouter.post('/bulk-get-seqjson-for-seqid-and-simulation-dataset', async ( with joined_table as ( select - activity_instance_commands.commands, - activity_instance_commands.activity_instance_id, - activity_instance_commands.errors, - activity_instance_commands.expansion_run_id, - sequence.seq_id, - sequence.simulation_dataset_id - from sequence - join sequence_to_simulated_activity - on sequence.seq_id = sequence_to_simulated_activity.seq_id - and sequence.simulation_dataset_id = - sequence_to_simulated_activity.simulation_dataset_id - join activity_instance_commands - on sequence_to_simulated_activity.simulated_activity_id = - activity_instance_commands.activity_instance_id - join expansion_run - on activity_instance_commands.expansion_run_id = expansion_run.id - where (sequence.seq_id, sequence.simulation_dataset_id) in (${pgFormat('%L', inputTuples)}) + aic.commands, + aic.activity_instance_id, + aic.errors, + aic.expansion_run_id, + ssa.seq_id, + ssa.simulation_dataset_id + from sequence_to_simulated_activity ssa + join activity_instance_commands aic + on ssa.simulated_activity_id = aic.activity_instance_id + where (ssa.seq_id, ssa.simulation_dataset_id) in (${pgFormat('%L', inputTuples)}) ), max_values as ( select @@ -328,8 +318,8 @@ seqjsonRouter.post('/bulk-get-seqjson-for-seqid-and-simulation-dataset', async ( }>( ` select metadata, seq_id, simulation_dataset_id - from sequence - where (sequence.seq_id, sequence.simulation_dataset_id) in (${pgFormat('%L', inputTuples)}); + from sequence s + where (s.seq_id, s.simulation_dataset_id) in (${pgFormat('%L', inputTuples)}); `, ), ]); From 0895a6a31515a558d577328c9b59645edb4a216d Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Mon, 18 Mar 2024 05:07:59 -1000 Subject: [PATCH 137/159] Adding time calculation and minor tweaks. * Using the span to calculate an activityInstance's 'startTime' and 'endTime' --- .../simulatedActivityBatchLoader.ts | 136 +++++++++--------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts index f3102e3f88..2c741f0fc0 100644 --- a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts +++ b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts @@ -12,21 +12,21 @@ export const simulatedActivitiesBatchLoader: BatchLoader< { graphqlClient: GraphQLClient; activitySchemaDataLoader: InferredDataloader } > = opts => async keys => { const result = await opts.graphqlClient.batchRequests< - { - data: { - simulation_dataset: { - id: number, - simulation: { - plan: { - id: number, - model_id: number - } - }, - simulation_start_time: string, - dataset: { spans: GQLSpan[]}, - } + { + data: { + simulation_dataset: { + id: number; + simulation: { + plan: { + id: number; + model_id: number; + }; + }; + simulation_start_time: string; + dataset: { spans: GQLSpan[] }; }; - }[] + }; + }[] >( keys.map(key => ({ document: gql` @@ -50,7 +50,8 @@ export const simulatedActivitiesBatchLoader: BatchLoader< } } } - }`, + } + `, variables: { simulationDatasetId: key.simulationDatasetId, }, @@ -59,9 +60,8 @@ export const simulatedActivitiesBatchLoader: BatchLoader< return Promise.all( keys.map(async ({ simulationDatasetId }) => { - const simulation_dataset = result.find( - res => res.data.simulation_dataset.id === simulationDatasetId, - )?.data.simulation_dataset; + const simulation_dataset = result.find(res => res.data.simulation_dataset.id === simulationDatasetId)?.data + .simulation_dataset; if (simulation_dataset === undefined) { return new ErrorWithStatusCode(`No simulation_dataset with id: ${simulationDatasetId}`, 404); } @@ -69,21 +69,18 @@ export const simulatedActivitiesBatchLoader: BatchLoader< const spans = simulation_dataset.dataset.spans; const simulatedActivities: GraphQLSimulatedActivityInstance[] = spans.map(span => { - return { - id: span.id, - simulation_dataset_id: simulation_dataset.id, - plan_id: simulation_dataset.simulation.plan.id, - model_id: simulation_dataset.simulation.plan.model_id, - attributes: span.attributes, - duration: span.duration, - start_offset: span.start_offset, - // TODO: Sum this intervals + durations properly - start_time: simulation_dataset.simulation_start_time + span.start_offset, - end_time: simulation_dataset.simulation_start_time + span.start_offset + span.duration, - activity_type_name: span.activity_type_name - } - } - ) + return { + id: span.id, + simulation_dataset_id: simulation_dataset.id, + plan_id: simulation_dataset.simulation.plan.id, + model_id: simulation_dataset.simulation.plan.model_id, + attributes: span.attributes, + duration: span.duration, + start_offset: span.start_offset, + simulation_start_time: simulation_dataset.simulation_start_time, + activity_type_name: span.activity_type_name, + }; + }); return Promise.all( simulatedActivities.map(async simulatedActivity => mapGraphQLActivityInstance( @@ -108,15 +105,15 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa { data: { simulation_dataset: { - id: number, - simulation_start_time: string, + id: number; + simulation_start_time: string; simulation: { plan: { - id: number, - model_id: number, - } - }, - dataset: {span: GQLSpan} + id: number; + model_id: number; + }; + }; + dataset: { spans: GQLSpan[] }; }; }; }[] @@ -134,7 +131,7 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa } } dataset { - spans: spans(where: {id: {_eq: $simulatedActivityId}}) { + spans: spans(where: { id: { _eq: $simulatedActivityId } }) { id attributes start_offset @@ -143,7 +140,8 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa } } } - }`, + } + `, variables: { simulationDatasetId: key.simulationDatasetId, simulatedActivityId: key.simulatedActivityId, @@ -153,25 +151,22 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa return Promise.all( keys.map(async ({ simulationDatasetId, simulatedActivityId }) => { - const simulation_dataset = result.find( - res => - res.data.simulation_dataset.id === simulationDatasetId + const simulation_dataset = result.find(res => + res.data?.simulation_dataset?.dataset?.spans?.some(span => span.id === simulatedActivityId), )?.data.simulation_dataset; if (simulation_dataset === undefined) { - return new ErrorWithStatusCode( - `No simulation_dataset with id: ${simulationDatasetId}`, - 404, - ); + return new ErrorWithStatusCode(`No simulation_dataset with id: ${simulationDatasetId}`, 404); } - const span = simulation_dataset.dataset.span - if(span === undefined) { + const spans = simulation_dataset?.dataset.spans; + if (spans === undefined || spans.length === 0 || spans[0] === undefined) { return new ErrorWithStatusCode( - `No simulation_dataset with id: ${simulationDatasetId} and simulated activity id: ${simulatedActivityId}`, - 404, + `No simulation_dataset with id: ${simulationDatasetId} and simulated activity id: ${simulatedActivityId}`, + 404, ); } + const span = spans[0]; const simulatedActivity: GraphQLSimulatedActivityInstance = { id: span.id, simulation_dataset_id: simulation_dataset.id, @@ -180,11 +175,9 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa attributes: span.attributes, duration: span.duration, start_offset: span.start_offset, - // TODO: Sum this intervals + durations properly - start_time: simulation_dataset.simulation_start_time + span.start_offset, - end_time: simulation_dataset.simulation_start_time + span.start_offset + span.duration, - activity_type_name: span.activity_type_name - } + simulation_start_time: simulation_dataset.simulation_start_time, + activity_type_name: span.activity_type_name, + }; return mapGraphQLActivityInstance( simulatedActivity, await opts.activitySchemaDataLoader.load({ @@ -236,7 +229,7 @@ export interface GQLSpan< ActivityArguments extends Record = Record, ActivityComputedAttributes extends Record = Record, > { - id: number + id: number; attributes: GraphQLSimulatedActivityAttributes; start_offset: string; duration: string; @@ -254,8 +247,7 @@ export interface GraphQLSimulatedActivityInstance< attributes: GraphQLSimulatedActivityAttributes; duration: string; start_offset: string; - start_time: string; - end_time: string; + simulation_start_time: string; activity_type_name: string; } @@ -263,17 +255,27 @@ export function mapGraphQLActivityInstance( activityInstance: GraphQLSimulatedActivityInstance, activitySchema: GraphQLActivitySchema, ): SimulatedActivity { + const duration = activityInstance.duration + ? Temporal.Duration.from(parse(activityInstance.duration).toISOString()) + : null; + const startOffset: Temporal.Duration = Temporal.Duration.from(parse(activityInstance.start_offset).toISOString()); + const startTime: Temporal.Instant = Temporal.Instant.from(activityInstance.simulation_start_time) + .toZonedDateTimeISO('UTC') + .add(startOffset) + .toInstant(); + const endTime = duration ? startTime.toZonedDateTimeISO('UTC').add(duration).toInstant() : null; + return { simulationDataset: { simulation: { - planId: activityInstance.model_id + planId: activityInstance.plan_id, }, }, id: activityInstance.id, - duration: activityInstance.duration ? Temporal.Duration.from(parse(activityInstance.duration).toISOString()) : null, - startOffset: Temporal.Duration.from(parse(activityInstance.start_offset).toISOString()), - startTime: Temporal.Instant.from(activityInstance.start_time), - endTime: activityInstance.end_time ? Temporal.Instant.from(activityInstance.end_time) : null, + duration, + startOffset, + startTime, + endTime, simulationDatasetId: activityInstance.simulation_dataset_id, activityTypeName: activityInstance.activity_type_name, attributes: { @@ -283,7 +285,7 @@ export function mapGraphQLActivityInstance( acc[key] = convertType(value, param.schema); } return acc; - }, {} as { [attributeName: string]: any }), + }, {} as Record), directiveId: activityInstance.attributes.directiveId, computed: activityInstance.attributes.computedAttributes ? convertType(activityInstance.attributes.computedAttributes, activitySchema.computed_attributes_value_schema) From 423034fe2bef0ed7f2da6f3068b56b1b2c6f06cf Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 18 Mar 2024 12:24:59 -0700 Subject: [PATCH 138/159] Add error if too many spans found --- .../src/lib/batchLoaders/simulatedActivityBatchLoader.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts index 2c741f0fc0..0e0462ba3f 100644 --- a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts +++ b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts @@ -166,6 +166,13 @@ export const simulatedActivityInstanceBySimulatedActivityIdBatchLoader: BatchLoa ); } + if(spans.length > 1) { + return new ErrorWithStatusCode( + `Too many spans with simulated activity id ${simulatedActivityId} found for simulation_dataset with id ${simulationDatasetId}`, + 404, + ); + } + const span = spans[0]; const simulatedActivity: GraphQLSimulatedActivityInstance = { id: span.id, From 4b165f801844ce7aebe3f3fa00d70118e9b852cc Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 29 Jan 2024 12:54:07 -0800 Subject: [PATCH 139/159] Split Scheduling Conditions into Metadata and Definition - Update Specification to include revision - Add a Model Specification for Scheduling Conditions - Add tags tables --- .../down.sql | 145 ++++++++++ .../up.sql | 262 ++++++++++++++++++ .../scheduling_condition_definition_tags.sql | 12 + .../metadata/scheduling_condition_tags.sql | 9 + .../scheduler/tables/scheduling_condition.sql | 54 ---- .../scheduling_condition_definition.sql | 51 ++++ .../tables/scheduling_condition_metadata.sql | 51 ++++ ...eduling_model_specification_conditions.sql | 24 ++ .../scheduling_specification_conditions.sql | 58 +++- 9 files changed, 605 insertions(+), 61 deletions(-) create mode 100644 deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql create mode 100644 deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql create mode 100644 scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_definition_tags.sql create mode 100644 scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_tags.sql delete mode 100644 scheduler-server/sql/scheduler/tables/scheduling_condition.sql create mode 100644 scheduler-server/sql/scheduler/tables/scheduling_condition_definition.sql create mode 100644 scheduler-server/sql/scheduler/tables/scheduling_condition_metadata.sql create mode 100644 scheduler-server/sql/scheduler/tables/scheduling_model_specification_conditions.sql diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql new file mode 100644 index 0000000000..868df91a8a --- /dev/null +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql @@ -0,0 +1,145 @@ +/********** +SCHEDULING CONDITION +***********/ +/* +RESTORE ORIGINAL +*/ +create table scheduling_condition ( + id integer generated by default as identity, + revision integer not null default 0, + name text not null, + definition text not null, + + model_id integer not null, + description text not null default '', + author text null, + last_modified_by text null, + created_date timestamptz not null default now(), + modified_date timestamptz not null default now(), + + constraint scheduling_condition_synthetic_key + primary key (id) +); + +comment on table scheduling_condition is e'' + 'A condition restricting scheduling of a plan.'; +comment on column scheduling_condition.id is e'' + 'The synthetic identifier for this scheduling condition.'; +comment on column scheduling_condition.revision is e'' + 'A monotonic clock that ticks for every change to this scheduling condition.'; +comment on column scheduling_condition.definition is e'' + 'The source code for a Typescript module defining this scheduling condition'; +comment on column scheduling_condition.model_id is e'' + 'The mission model used to which this scheduling condition is associated.'; +comment on column scheduling_condition.name is e'' + 'A short human readable name for this condition'; +comment on column scheduling_condition.description is e'' + 'A longer text description of this scheduling condition.'; +comment on column scheduling_condition.author is e'' + 'The original user who authored this scheduling condition.'; +comment on column scheduling_condition.last_modified_by is e'' + 'The last user who modified this scheduling condition.'; +comment on column scheduling_condition.created_date is e'' + 'The date this scheduling condition was created.'; +comment on column scheduling_condition.modified_date is e'' + 'The date this scheduling condition was last modified.'; + +create function update_logging_on_update_scheduling_condition() + returns trigger + security definer +language plpgsql as $$begin + new.revision = old.revision + 1; + new.modified_date = now(); +return new; +end$$; + +create trigger update_logging_on_update_scheduling_condition_trigger + before update on scheduling_condition + for each row + when (pg_trigger_depth() < 1) + execute function update_logging_on_update_scheduling_condition(); + +/* +DATA MIGRATION +*/ +-- Conditions not on a model spec will not be kept, as the scheduler DB can't get the model id from the plan id +-- Because there is no uniqueness constraint on Scheduling Conditions when it comes to specifications, the ids can be preserved +with specified_definition(condition_id, condition_revision, model_id, definition, definition_creation) as ( + select cd.condition_id, cd.revision, s.model_id, cd.definition, cd.created_at + from scheduling_model_specification_conditions s + left join scheduling_condition_definition cd using (condition_id) + where ((s.condition_revision is not null and s.condition_revision = cd.revision) + or (s.condition_revision is null and cd.revision = (select def.revision + from scheduling_condition_definition def + where def.condition_id = s.condition_id + order by def.revision desc limit 1))) +) +insert into scheduling_condition(id, revision, name, definition, model_id, description, + author, last_modified_by, created_date, modified_date) +select m.id, sd.condition_revision, m.name, sd.definition, sd.model_id, m.description, + m.owner, m.updated_by, m.updated_at, greatest(m.updated_at::timestamptz, sd.definition_creation::timestamptz) + from scheduling_condition_metadata m + inner join specified_definition sd on m.id = sd.condition_id; + +/* +POST DATA MIGRATION TABLE CHANGES +*/ +drop trigger set_timestamp on scheduling_condition_metadata; +drop function scheduling_condition_metadata_set_updated_at(); + +alter table scheduling_condition + alter column id set generated always; +/* +SPECIFICATIONS +*/ +drop trigger increment_revision_on_condition_delete on scheduling_specification_conditions; +drop function increment_spec_revision_on_conditions_spec_delete(); +drop trigger increment_revision_on_condition_update on scheduling_specification_conditions; +drop function increment_spec_revision_on_conditions_spec_update(); + +alter table scheduling_specification_conditions + drop constraint scheduling_specification_condition_definition_exists, + drop constraint scheduling_specification_condition_exists, + add constraint scheduling_specification_conditions_references_scheduling_conditions + foreign key (condition_id) + references scheduling_condition + on update cascade + on delete cascade, + drop constraint scheduling_specification_conditions_specification_exists, + add constraint scheduling_specification_conditions_references_scheduling_specification + foreign key (specification_id) + references scheduling_specification + on update cascade + on delete cascade, + drop column condition_revision; + +comment on table scheduling_specification_conditions is e'' + 'A join table associating scheduling specifications with scheduling conditions.'; +comment on column scheduling_specification_conditions.specification_id is e'' + 'The ID of the scheduling specification a scheduling goal is associated with.'; +comment on column scheduling_specification_conditions.condition_id is e'' + 'The ID of the condition a scheduling specification is associated with.'; +comment on column scheduling_specification_conditions.enabled is null; + +drop table scheduling_model_specification_conditions; + +/* +TAGS +*/ +drop table metadata.scheduling_condition_definition_tags; +drop table metadata.scheduling_condition_tags; + +/* +DEFINITION +*/ +drop trigger scheduling_goal_definition_set_revision on scheduling_condition_definition; +drop function scheduling_condition_definition_set_revision(); +drop table scheduling_condition_definition; + +/* +METADATA +*/ +drop index condition_name_unique_if_published; +drop table scheduling_condition_metadata; + +call migrations.mark_migration_rolled_back('13'); diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql new file mode 100644 index 0000000000..c2ce2059d4 --- /dev/null +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql @@ -0,0 +1,262 @@ +/********** +SCHEDULING CONDITION +***********/ +/* +METADATA +*/ +create table scheduling_condition_metadata ( + id integer generated by default as identity, + + name text not null, + description text not null default '', + public boolean not null default false, + + owner text, + updated_by text, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + constraint scheduling_condition_metadata_pkey + primary key (id) +); + +-- A partial index is used to enforce name uniqueness only on conditions visible to other users +create unique index condition_name_unique_if_published on scheduling_condition_metadata (name) where public; + +comment on table scheduling_condition_metadata is e'' + 'A condition restricting scheduling of a plan.'; +comment on column scheduling_condition_metadata.id is e'' + 'The unique identifier for this scheduling condition.'; +comment on column scheduling_condition_metadata.name is e'' + 'A short human readable name for this condition'; +comment on column scheduling_condition_metadata.description is e'' + 'A longer text description of this scheduling condition.'; +comment on column scheduling_condition_metadata.public is e'' + 'Whether this goal is visible to all users.'; +comment on column scheduling_condition_metadata.owner is e'' + 'The user responsible for this condition.'; +comment on column scheduling_condition_metadata.updated_by is e'' + 'The user who last modified this condition''s metadata.'; +comment on column scheduling_condition_metadata.created_at is e'' + 'The time at which this condition was created.'; +comment on column scheduling_condition_metadata.updated_at is e'' + 'The time at which this condition''s metadata was last modified.'; + +/* +DEFINITION +*/ +create table scheduling_condition_definition( + condition_id integer not null, + revision integer not null default 0, + definition text not null, + author text, + created_at timestamptz not null default now(), + + constraint scheduling_condition_definition_pkey + primary key (condition_id, revision), + constraint scheduling_condition_definition_condition_exists + foreign key (condition_id) + references scheduling_condition_metadata + on update cascade + on delete cascade +); + +comment on table scheduling_condition_definition is e'' + 'The specific revisions of a scheduling condition''s definition'; +comment on column scheduling_condition_definition.revision is e'' + 'An identifier of this definition.'; +comment on column scheduling_condition_definition.definition is e'' + 'An executable expression in the Merlin scheduling language.'; +comment on column scheduling_condition_definition.author is e'' + 'The user who authored this revision.'; +comment on column scheduling_condition_definition.created_at is e'' + 'When this revision was created.'; + +create function scheduling_condition_definition_set_revision() +returns trigger +volatile +language plpgsql as $$ +declare + max_revision integer; +begin + -- Grab the current max value of revision, or -1, if this is the first revision + select coalesce((select revision + from scheduling_condition_definition + where condition_id = new.condition_id + order by revision desc + limit 1), -1) + into max_revision; + + new.revision = max_revision + 1; + return new; +end +$$; + +create trigger scheduling_goal_definition_set_revision + before insert on scheduling_condition_definition + for each row + execute function scheduling_condition_definition_set_revision(); + +/* +TAGS +*/ +create table metadata.scheduling_condition_tags ( + condition_id integer references public.scheduling_condition_metadata + on update cascade + on delete cascade, + tag_id integer not null, + primary key (condition_id, tag_id) +); +comment on table metadata.scheduling_condition_tags is e'' + 'The tags associated with a scheduling condition.'; + +create table metadata.scheduling_condition_definition_tags ( + condition_id integer not null, + condition_revision integer not null, + tag_id integer not null, + primary key (condition_id, condition_revision, tag_id), + foreign key (condition_id, condition_revision) references scheduling_condition_definition + on update cascade + on delete cascade +); + +comment on table metadata.scheduling_condition_definition_tags is e'' + 'The tags associated with a specific scheduling condition definition.'; + +/* +SPECIFICATIONS +*/ +create table scheduling_model_specification_conditions( + model_id integer not null, + condition_id integer not null, + condition_revision integer, -- latest is NULL + + primary key (model_id, condition_id), + foreign key (condition_id) + references scheduling_condition_metadata + on update cascade + on delete restrict, + foreign key (condition_id, condition_revision) + references scheduling_condition_definition + on update cascade + on delete restrict +); + +comment on table scheduling_model_specification_conditions is e'' +'The set of scheduling conditions that all plans using the model should include in their scheduling specification.'; +comment on column scheduling_model_specification_conditions.model_id is e'' +'The model which this specification is for. Half of the primary key.'; +comment on column scheduling_model_specification_conditions.condition_id is e'' +'The id of a specific scheduling condition in the specification. Half of the primary key.'; +comment on column scheduling_model_specification_conditions.condition_revision is e'' +'The version of the scheduling condition definition to use. Leave NULL to use the latest version.'; + +alter table scheduling_specification_conditions + add column condition_revision integer, + -- This constraint's name is too long + drop constraint scheduling_specification_conditions_references_scheduling_specification, + add constraint scheduling_specification_conditions_specification_exists + foreign key (specification_id) + references scheduling_specification + on update cascade + on delete cascade, + drop constraint scheduling_specification_conditions_references_scheduling_conditions, + add constraint scheduling_specification_condition_exists + foreign key (condition_id) + references scheduling_condition_metadata + on update cascade + on delete restrict, + add constraint scheduling_specification_condition_definition_exists + foreign key (condition_id, condition_revision) + references scheduling_condition_definition + on update cascade + on delete restrict; + +comment on table scheduling_specification_conditions is e'' + 'The set of scheduling conditions to be used on a given plan.'; +comment on column scheduling_specification_conditions.specification_id is e'' + 'The plan scheduling specification which this condition is on. Half of the primary key.'; +comment on column scheduling_specification_conditions.condition_id is e'' + 'The ID of a specific condition in the specification. Half of the primary key.'; +comment on column scheduling_specification_conditions.condition_revision is e'' + 'The version of the condition definition to use. Leave NULL to use the latest version.'; +comment on column scheduling_specification_conditions.enabled is e'' + 'Whether to use a given condition. Defaults to TRUE.'; + +create function increment_spec_revision_on_conditions_spec_update() + returns trigger + security definer +language plpgsql as $$ +begin + update scheduling_specification + set revision = revision + 1 + where id = new.specification_id; + return new; +end; +$$; + +create trigger increment_revision_on_condition_update + before insert or update on scheduling_specification_conditions + for each row + execute function increment_spec_revision_on_conditions_spec_update(); + +create function increment_spec_revision_on_conditions_spec_delete() + returns trigger + security definer +language plpgsql as $$ +begin + update scheduling_specification + set revision = revision + 1 + where id = new.specification_id; + return new; +end; +$$; + +create trigger increment_revision_on_condition_delete + before delete on scheduling_specification_conditions + for each row + execute function increment_spec_revision_on_conditions_spec_delete(); + +/* +DATA MIGRATION +*/ +insert into scheduling_condition_metadata(id, name, description, public, owner, updated_by, created_at, updated_at) +select id, name, description, false, author, last_modified_by, created_date, modified_date +from scheduling_condition; + +insert into scheduling_condition_definition(condition_id, definition, author, created_at) +select id, definition, author, modified_date +from scheduling_condition; + +insert into scheduling_model_specification_conditions(model_id, condition_id) +select model_id, id +from scheduling_condition; + +/* +POST DATA MIGRATION TABLE CHANGES +*/ +alter table scheduling_condition_metadata + alter column id set generated always; + +create function scheduling_condition_metadata_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp +before update on scheduling_condition_metadata +for each row +execute function scheduling_condition_metadata_set_updated_at(); + +/* +DROP ORIGINAL +*/ +drop trigger update_logging_on_update_scheduling_condition_trigger on scheduling_condition; +drop function update_logging_on_update_scheduling_condition(); +drop table scheduling_condition; + +call migrations.mark_migration_applied('13'); diff --git a/scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_definition_tags.sql b/scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_definition_tags.sql new file mode 100644 index 0000000000..f29d45981f --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_definition_tags.sql @@ -0,0 +1,12 @@ +create table metadata.scheduling_condition_definition_tags ( + condition_id integer not null, + condition_revision integer not null, + tag_id integer not null, + primary key (condition_id, condition_revision, tag_id), + foreign key (condition_id, condition_revision) references scheduling_condition_definition + on update cascade + on delete cascade +); + +comment on table metadata.scheduling_condition_definition_tags is e'' + 'The tags associated with a specific scheduling condition definition.'; diff --git a/scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_tags.sql b/scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_tags.sql new file mode 100644 index 0000000000..f209b9854f --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/metadata/scheduling_condition_tags.sql @@ -0,0 +1,9 @@ +create table metadata.scheduling_condition_tags ( + condition_id integer references public.scheduling_condition_metadata + on update cascade + on delete cascade, + tag_id integer not null, + primary key (condition_id, tag_id) +); +comment on table metadata.scheduling_condition_tags is e'' + 'The tags associated with a scheduling condition.'; diff --git a/scheduler-server/sql/scheduler/tables/scheduling_condition.sql b/scheduler-server/sql/scheduler/tables/scheduling_condition.sql deleted file mode 100644 index d4aabbb776..0000000000 --- a/scheduler-server/sql/scheduler/tables/scheduling_condition.sql +++ /dev/null @@ -1,54 +0,0 @@ -create table scheduling_condition ( - id integer generated always as identity, - revision integer not null default 0, - name text not null, - definition text not null, - - model_id integer not null, - description text not null default '', - author text null, - last_modified_by text null, - created_date timestamptz not null default now(), - modified_date timestamptz not null default now(), - - constraint scheduling_condition_synthetic_key - primary key (id) -); - -comment on table scheduling_condition is e'' - 'A condition restricting scheduling of a plan.'; -comment on column scheduling_condition.id is e'' - 'The synthetic identifier for this scheduling condition.'; -comment on column scheduling_condition.revision is e'' - 'A monotonic clock that ticks for every change to this scheduling condition.'; -comment on column scheduling_condition.definition is e'' - 'The source code for a Typescript module defining this scheduling condition'; -comment on column scheduling_condition.model_id is e'' - 'The mission model used to which this scheduling condition is associated.'; -comment on column scheduling_condition.name is e'' - 'A short human readable name for this condition'; -comment on column scheduling_condition.description is e'' - 'A longer text description of this scheduling condition.'; -comment on column scheduling_condition.author is e'' - 'The original user who authored this scheduling condition.'; -comment on column scheduling_condition.last_modified_by is e'' - 'The last user who modified this scheduling condition.'; -comment on column scheduling_condition.created_date is e'' - 'The date this scheduling condition was created.'; -comment on column scheduling_condition.modified_date is e'' - 'The date this scheduling condition was last modified.'; - -create function update_logging_on_update_scheduling_condition() - returns trigger - security definer -language plpgsql as $$begin - new.revision = old.revision + 1; - new.modified_date = now(); -return new; -end$$; - -create trigger update_logging_on_update_scheduling_condition_trigger - before update on scheduling_condition - for each row - when (pg_trigger_depth() < 1) - execute function update_logging_on_update_scheduling_condition(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_condition_definition.sql b/scheduler-server/sql/scheduler/tables/scheduling_condition_definition.sql new file mode 100644 index 0000000000..2501ac11a4 --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/scheduling_condition_definition.sql @@ -0,0 +1,51 @@ +create table scheduling_condition_definition( + condition_id integer not null, + revision integer not null default 0, + definition text not null, + author text, + created_at timestamptz not null default now(), + + constraint scheduling_condition_definition_pkey + primary key (condition_id, revision), + constraint scheduling_condition_definition_condition_exists + foreign key (condition_id) + references scheduling_condition_metadata + on update cascade + on delete cascade +); + +comment on table scheduling_condition_definition is e'' + 'The specific revisions of a scheduling condition''s definition'; +comment on column scheduling_condition_definition.revision is e'' + 'An identifier of this definition.'; +comment on column scheduling_condition_definition.definition is e'' + 'An executable expression in the Merlin scheduling language.'; +comment on column scheduling_condition_definition.author is e'' + 'The user who authored this revision.'; +comment on column scheduling_condition_definition.created_at is e'' + 'When this revision was created.'; + +create function scheduling_condition_definition_set_revision() +returns trigger +volatile +language plpgsql as $$ +declare + max_revision integer; +begin + -- Grab the current max value of revision, or -1, if this is the first revision + select coalesce((select revision + from scheduling_condition_definition + where condition_id = new.condition_id + order by revision desc + limit 1), -1) + into max_revision; + + new.revision = max_revision + 1; + return new; +end +$$; + +create trigger scheduling_goal_definition_set_revision + before insert on scheduling_condition_definition + for each row + execute function scheduling_condition_definition_set_revision(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_condition_metadata.sql b/scheduler-server/sql/scheduler/tables/scheduling_condition_metadata.sql new file mode 100644 index 0000000000..60a23d880b --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/scheduling_condition_metadata.sql @@ -0,0 +1,51 @@ +create table scheduling_condition_metadata ( + id integer generated always as identity, + + name text not null, + description text not null default '', + public boolean not null default false, + + owner text, + updated_by text, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + constraint scheduling_condition_metadata_pkey + primary key (id) +); + +-- A partial index is used to enforce name uniqueness only on conditions visible to other users +create unique index condition_name_unique_if_published on scheduling_condition_metadata (name) where public; + +comment on table scheduling_condition_metadata is e'' + 'A condition restricting scheduling of a plan.'; +comment on column scheduling_condition_metadata.id is e'' + 'The unique identifier for this scheduling condition.'; +comment on column scheduling_condition_metadata.name is e'' + 'A short human readable name for this condition'; +comment on column scheduling_condition_metadata.description is e'' + 'A longer text description of this scheduling condition.'; +comment on column scheduling_condition_metadata.public is e'' + 'Whether this goal is visible to all users.'; +comment on column scheduling_condition_metadata.owner is e'' + 'The user responsible for this condition.'; +comment on column scheduling_condition_metadata.updated_by is e'' + 'The user who last modified this condition''s metadata.'; +comment on column scheduling_condition_metadata.created_at is e'' + 'The time at which this condition was created.'; +comment on column scheduling_condition_metadata.updated_at is e'' + 'The time at which this condition''s metadata was last modified.'; + +create function scheduling_condition_metadata_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp +before update on scheduling_condition_metadata +for each row +execute function scheduling_condition_metadata_set_updated_at(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_model_specification_conditions.sql b/scheduler-server/sql/scheduler/tables/scheduling_model_specification_conditions.sql new file mode 100644 index 0000000000..1230483f20 --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/scheduling_model_specification_conditions.sql @@ -0,0 +1,24 @@ +create table scheduling_model_specification_conditions( + model_id integer not null, + condition_id integer not null, + condition_revision integer, -- latest is NULL + + primary key (model_id, condition_id), + foreign key (condition_id) + references scheduling_condition_metadata + on update cascade + on delete restrict, + foreign key (condition_id, condition_revision) + references scheduling_condition_definition + on update cascade + on delete restrict +); + +comment on table scheduling_model_specification_conditions is e'' +'The set of scheduling conditions that all plans using the model should include in their scheduling specification.'; +comment on column scheduling_model_specification_conditions.model_id is e'' +'The model which this specification is for. Half of the primary key.'; +comment on column scheduling_model_specification_conditions.condition_id is e'' +'The id of a specific scheduling condition in the specification. Half of the primary key.'; +comment on column scheduling_model_specification_conditions.condition_revision is e'' +'The version of the scheduling condition definition to use. Leave NULL to use the latest version.'; diff --git a/scheduler-server/sql/scheduler/tables/scheduling_specification_conditions.sql b/scheduler-server/sql/scheduler/tables/scheduling_specification_conditions.sql index badd50bd29..4a9657ef08 100644 --- a/scheduler-server/sql/scheduler/tables/scheduling_specification_conditions.sql +++ b/scheduler-server/sql/scheduler/tables/scheduling_specification_conditions.sql @@ -1,25 +1,69 @@ create table scheduling_specification_conditions ( specification_id integer not null, condition_id integer not null, + condition_revision integer, -- latest is NULL enabled boolean default true, constraint scheduling_specification_conditions_primary_key primary key (specification_id, condition_id), - constraint scheduling_specification_conditions_references_scheduling_specification + constraint scheduling_specification_conditions_specification_exists foreign key (specification_id) references scheduling_specification on update cascade on delete cascade, - constraint scheduling_specification_conditions_references_scheduling_conditions + constraint scheduling_specification_condition_exists foreign key (condition_id) - references scheduling_condition + references scheduling_condition_metadata on update cascade - on delete cascade + on delete restrict, + constraint scheduling_specification_condition_definition_exists + foreign key (condition_id, condition_revision) + references scheduling_condition_definition + on update cascade + on delete restrict ); comment on table scheduling_specification_conditions is e'' - 'A join table associating scheduling specifications with scheduling conditions.'; + 'The set of scheduling conditions to be used on a given plan.'; comment on column scheduling_specification_conditions.specification_id is e'' - 'The ID of the scheduling specification a scheduling goal is associated with.'; + 'The plan scheduling specification which this condition is on. Half of the primary key.'; comment on column scheduling_specification_conditions.condition_id is e'' - 'The ID of the condition a scheduling specification is associated with.'; + 'The ID of a specific condition in the specification. Half of the primary key.'; +comment on column scheduling_specification_conditions.condition_revision is e'' + 'The version of the condition definition to use. Leave NULL to use the latest version.'; +comment on column scheduling_specification_conditions.enabled is e'' + 'Whether to use a given condition. Defaults to TRUE.'; + +create function increment_spec_revision_on_conditions_spec_update() + returns trigger + security definer +language plpgsql as $$ +begin + update scheduling_specification + set revision = revision + 1 + where id = new.specification_id; + return new; +end; +$$; + +create trigger increment_revision_on_condition_update + before insert or update on scheduling_specification_conditions + for each row + execute function increment_spec_revision_on_conditions_spec_update(); + +create function increment_spec_revision_on_conditions_spec_delete() + returns trigger + security definer +language plpgsql as $$ +begin + update scheduling_specification + set revision = revision + 1 + where id = new.specification_id; + return new; +end; +$$; + +create trigger increment_revision_on_condition_delete + before delete on scheduling_specification_conditions + for each row + execute function increment_spec_revision_on_conditions_spec_delete(); From 5e495b71035f6c151371680065a0c565afe5453d Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 29 Jan 2024 16:44:18 -0800 Subject: [PATCH 140/159] Split Scheduling Goals into Metadata and Definition - Update Specification to include revision - Add a Model Specification for Scheduling Goals - Add and update tags tables --- .../down.sql | 237 +++++++++ .../up.sql | 466 ++++++++++++++++++ .../scheduling_goal_definition_tags.sql | 12 + .../tables/metadata/scheduling_goal_tags.sql | 2 +- .../sql/scheduler/tables/scheduling_goal.sql | 54 -- .../tables/scheduling_goal_definition.sql | 52 ++ .../tables/scheduling_goal_metadata.sql | 51 ++ .../scheduling_model_specification_goals.sql | 140 ++++++ .../tables/scheduling_specification.sql | 18 - .../tables/scheduling_specification_goals.sql | 214 +++++--- 10 files changed, 1095 insertions(+), 151 deletions(-) create mode 100644 scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_definition_tags.sql delete mode 100644 scheduler-server/sql/scheduler/tables/scheduling_goal.sql create mode 100644 scheduler-server/sql/scheduler/tables/scheduling_goal_definition.sql create mode 100644 scheduler-server/sql/scheduler/tables/scheduling_goal_metadata.sql create mode 100644 scheduler-server/sql/scheduler/tables/scheduling_model_specification_goals.sql diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql index 868df91a8a..a6b39092fa 100644 --- a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql @@ -1,3 +1,240 @@ +/********** +SCHEDULING GOALS +***********/ +/* +RESTORE ORIGINAL +*/ +create table scheduling_goal ( + id integer generated always as identity, + revision integer not null default 0, + name text not null, + definition text not null, + + model_id integer not null, + description text not null default '', + author text null, + last_modified_by text null, + created_date timestamptz not null default now(), + modified_date timestamptz not null default now(), + + constraint scheduling_goal_synthetic_key + primary key (id) +); + +comment on table scheduling_goal is e'' + 'A goal for scheduling of a plan.'; +comment on column scheduling_goal.id is e'' + 'The synthetic identifier for this scheduling goal.'; +comment on column scheduling_goal.revision is e'' + 'A monotonic clock that ticks for every change to this scheduling goal.'; +comment on column scheduling_goal.definition is e'' + 'The source code for a Typescript module defining this scheduling goal'; +comment on column scheduling_goal.model_id is e'' + 'The mission model used to which this scheduling goal is associated.'; +comment on column scheduling_goal.name is e'' + 'A short human readable name for this goal'; +comment on column scheduling_goal.description is e'' + 'A longer text description of this scheduling goal.'; +comment on column scheduling_goal.author is e'' + 'The original user who authored this scheduling goal.'; +comment on column scheduling_goal.last_modified_by is e'' + 'The last user who modified this scheduling goal.'; +comment on column scheduling_goal.created_date is e'' + 'The date this scheduling goal was created.'; +comment on column scheduling_goal.modified_date is e'' + 'The date this scheduling goal was last modified.'; + +create function update_logging_on_update_scheduling_goal() + returns trigger + security definer +language plpgsql as $$begin + new.revision = old.revision + 1; + new.modified_date = now(); +return new; +end$$; + +create trigger update_logging_on_update_scheduling_goal_trigger + before update on scheduling_goal + for each row + when (pg_trigger_depth() < 1) + execute function update_logging_on_update_scheduling_goal(); + +/* +DATA MIGRATION +*/ +-- Goals not on a model spec will not be kept, as the scheduler DB can't get the model id from the plan id +-- Because multiple spec may be using the same goal/goal definition, we have to regenerate the id +with specified_definition(goal_id, goal_revision, model_id, definition, definition_creation) as ( + select gd.goal_id, gd.revision, s.model_id, gd.definition, gd.created_at + from scheduling_model_specification_goals s + left join scheduling_goal_definition gd using (goal_id) + where ((s.goal_revision is not null and s.goal_revision = gd.revision) + or (s.goal_revision is null and gd.revision = (select def.revision + from scheduling_goal_definition def + where def.goal_id = s.goal_id + order by def.revision desc limit 1))) +) +insert into scheduling_goal(revision, name, definition, model_id, description, + author, last_modified_by, created_date, modified_date) +select sd.goal_revision, m.name, sd.definition, sd.model_id, m.description, + m.owner, m.updated_by, m.created_at, greatest(m.updated_at::timestamptz, sd.definition_creation::timestamptz) + from scheduling_goal_metadata m + inner join specified_definition sd on m.id = sd.goal_id; +/* +POST DATA MIGRATION TABLE CHANGES +*/ +drop trigger set_timestamp on scheduling_goal_metadata; +drop function scheduling_goal_metadata_set_updated_at(); + +/* +SCHEDULING SPECIFICATION +*/ +create function increment_revision_on_goal_update() + returns trigger + security definer +language plpgsql as $$begin + with goals as ( + select g.specification_id from scheduling_specification_goals as g + where g.goal_id = new.id + ) + update scheduling_specification set revision = revision + 1 + where exists(select 1 from goals where specification_id = id); + return new; +end$$; +create trigger increment_revision_on_goal_update + before update on scheduling_goal + for each row + execute function increment_revision_on_goal_update(); + + +/* +SPECIFICATIONS +*/ +drop trigger increment_revision_on_goal_delete on scheduling_specification_goals; +drop function increment_spec_revision_on_goal_spec_delete(); +drop trigger increment_revision_on_goal_update on scheduling_specification_goals; +drop function increment_spec_revision_on_goal_spec_update(); + +create or replace function delete_scheduling_specification_goal_func() + returns trigger as $$begin + update scheduling_specification_goals + set priority = priority - 1 + where specification_id = OLD.specification_id + and priority > OLD.priority; + return null; + end;$$ +language plpgsql; + +create or replace function update_scheduling_specification_goal_func() + returns trigger as $$begin + if (pg_trigger_depth() = 1) then + if NEW.priority > OLD.priority then + if NEW.priority > ( + select coalesce(max(priority), -1) from scheduling_specification_goals + where specification_id = new.specification_id + ) + 1 then + raise exception 'Updated priority % for specification_id % is not consecutive', NEW.priority, new.specification_id; + end if; + update scheduling_specification_goals + set priority = priority - 1 + where specification_id = NEW.specification_id + and priority between OLD.priority + 1 and NEW.priority + and goal_id <> NEW.goal_id; + else + update scheduling_specification_goals + set priority = priority + 1 + where specification_id = NEW.specification_id + and priority between NEW.priority and OLD.priority - 1 + and goal_id <> NEW.goal_id; + end if; + end if; + return NEW; + end;$$ +language plpgsql; + +create or replace function insert_scheduling_specification_goal_func() + returns trigger as $$begin + if NEW.priority IS NULL then + NEW.priority = ( + select coalesce(max(priority), -1) from scheduling_specification_goals + where specification_id = NEW.specification_id + ) + 1; + elseif NEW.priority > ( + select coalesce(max(priority), -1) from scheduling_specification_goals + where specification_id = new.specification_id + ) + 1 then + raise exception 'Inserted priority % for specification_id % is not consecutive', NEW.priority, NEW.specification_id; + end if; + update scheduling_specification_goals + set priority = priority + 1 + where specification_id = NEW.specification_id + and priority >= NEW.priority; + return NEW; + end;$$ +language plpgsql; + +alter table scheduling_specification_goals + add constraint scheduling_specification_unique_goal_id + unique (goal_id), + drop constraint scheduling_spec_goal_definition_exists, + drop constraint scheduling_spec_goal_exists, + add constraint scheduling_specification_goals_references_scheduling_goals + foreign key (goal_id) + references scheduling_goal + on update cascade + on delete cascade, + drop constraint scheduling_specification_goals_specification_exists, + add constraint scheduling_specification_goals_references_scheduling_specification + foreign key (specification_id) + references scheduling_specification + on update cascade + on delete cascade, + alter column enabled drop not null, + alter column priority set default null, + drop column goal_revision; + +comment on table scheduling_specification_goals is e'' + 'A join table associating scheduling specifications with scheduling goals.'; +comment on column scheduling_specification_goals.specification_id is e'' + 'The ID of the scheduling specification a scheduling goal is associated with.'; +comment on column scheduling_specification_goals.goal_id is e'' + 'The ID of the scheduling goal a scheduling specification is associated with.'; +comment on column scheduling_specification_goals.priority is e'' + 'The relative priority of a scheduling goal in relation to other ' + 'scheduling goals within the same specification.'; +comment on column scheduling_specification_goals.enabled is null; +comment on column scheduling_specification_goals.simulate_after is e'' + 'Whether to re-simulate after evaluating this goal and before the next goal.'; + +drop trigger delete_scheduling_model_specification_goal on scheduling_model_specification_goals; +drop function delete_scheduling_model_specification_goal_func(); +drop trigger update_scheduling_model_specification_goal on scheduling_model_specification_goals; +drop function update_scheduling_model_specification_goal_func(); +drop trigger insert_scheduling_model_specification_goal on scheduling_model_specification_goals; +drop function insert_scheduling_model_specification_goal_func(); + +drop table scheduling_model_specification_goals; +/* +TAGS +*/ +drop table metadata.scheduling_goal_definition_tags; +alter table metadata.scheduling_goal_tags +drop constraint scheduling_goal_tags_goal_id_fkey, +add foreign key (goal_id) references public.scheduling_goal + on update cascade + on delete cascade; +/* +DEFINITION +*/ +drop trigger scheduling_goal_definition_set_revision on scheduling_goal_definition; +drop function scheduling_goal_definition_set_revision(); +drop table scheduling_goal_definition; +/* +METADATA +*/ +drop index goal_name_unique_if_published; +drop table scheduling_goal_metadata; + /********** SCHEDULING CONDITION ***********/ diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql index c2ce2059d4..f9ff0a991f 100644 --- a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql @@ -259,4 +259,470 @@ drop trigger update_logging_on_update_scheduling_condition_trigger on scheduling drop function update_logging_on_update_scheduling_condition(); drop table scheduling_condition; +/********** +SCHEDULING GOALS +***********/ +/* +METADATA +*/ +create table scheduling_goal_metadata ( + id integer generated by default as identity, + + name text not null, + description text not null default '', + public boolean not null default false, + + owner text, + updated_by text, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + constraint scheduling_goal_metadata_pkey + primary key (id) +); + +-- A partial index is used to enforce name uniqueness only on goals visible to other users +create unique index goal_name_unique_if_published on scheduling_goal_metadata (name) where public; + +comment on table scheduling_goal_metadata is e'' + 'A goal for scheduling a plan.'; +comment on column scheduling_goal_metadata.id is e'' + 'The unique identifier of the goal'; +comment on column scheduling_goal_metadata.name is e'' + 'A human-meaningful name.'; +comment on column scheduling_goal_metadata.description is e'' + 'A detailed description suitable for long-form documentation.'; +comment on column scheduling_goal_metadata.public is e'' + 'Whether this goal is visible to all users.'; +comment on column scheduling_goal_metadata.owner is e'' + 'The user responsible for this goal.'; +comment on column scheduling_goal_metadata.updated_by is e'' + 'The user who last modified this goal''s metadata.'; +comment on column scheduling_goal_metadata.created_at is e'' + 'The time at which this goal was created.'; +comment on column scheduling_goal_metadata.updated_at is e'' + 'The time at which this goal''s metadata was last modified.'; + +/* +DEFINITION +*/ +create table scheduling_goal_definition( + goal_id integer not null, + revision integer not null default 0, + + definition text not null, + author text, + created_at timestamptz not null default now(), + + constraint scheduling_goal_definition_pkey + primary key (goal_id, revision), + constraint scheduling_goal_definition_goal_exists + foreign key (goal_id) + references scheduling_goal_metadata + on update cascade + on delete cascade +); + +comment on table scheduling_goal_definition is e'' + 'The specific revisions of a scheduling goal''s definition'; +comment on column scheduling_goal_definition.revision is e'' + 'An identifier of this definition.'; +comment on column scheduling_goal_definition.definition is e'' + 'An executable expression in the Merlin scheduling language.'; +comment on column scheduling_goal_definition.author is e'' + 'The user who authored this revision.'; +comment on column scheduling_goal_definition.created_at is e'' + 'When this revision was created.'; + +create function scheduling_goal_definition_set_revision() +returns trigger +volatile +language plpgsql as $$ +declare + max_revision integer; +begin + -- Grab the current max value of revision, or -1, if this is the first revision + select coalesce((select revision + from scheduling_goal_definition + where goal_id = new.goal_id + order by revision desc + limit 1), -1) + into max_revision; + + new.revision = max_revision + 1; + return new; +end +$$; + +create trigger scheduling_goal_definition_set_revision + before insert on scheduling_goal_definition + for each row + execute function scheduling_goal_definition_set_revision(); + + +/* +TAGS +*/ +alter table metadata.scheduling_goal_tags +drop constraint scheduling_goal_tags_goal_id_fkey, +add foreign key (goal_id) references public.scheduling_goal_metadata + on update cascade + on delete cascade; + +create table metadata.scheduling_goal_definition_tags ( + goal_id integer not null, + goal_revision integer not null, + tag_id integer not null, + primary key (goal_id, goal_revision, tag_id), + foreign key (goal_id, goal_revision) references scheduling_goal_definition + on update cascade + on delete cascade +); + +comment on table metadata.scheduling_goal_definition_tags is e'' + 'The tags associated with a specific scheduling condition definition.'; + +/* +SPECIFICATIONS +*/ +create table scheduling_model_specification_goals( + model_id integer not null, + goal_id integer not null, + goal_revision integer, -- latest is NULL + priority integer not null, + + primary key (model_id, goal_id), + foreign key (goal_id) + references scheduling_goal_metadata + on update cascade + on delete restrict, + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete restrict, + constraint model_spec_unique_goal_priorities + unique (model_id, priority) deferrable initially deferred, + constraint model_spec_nonnegative_priority + check (priority >= 0) +); + +comment on table scheduling_model_specification_goals is e'' +'The set of scheduling goals that all plans using the model should include in their scheduling specification.'; +comment on column scheduling_model_specification_goals.model_id is e'' +'The model which this specification is for. Half of the primary key.'; +comment on column scheduling_model_specification_goals.goal_id is e'' +'The id of a specific scheduling goal in the specification. Half of the primary key.'; +comment on column scheduling_model_specification_goals.goal_revision is e'' +'The version of the scheduling goal definition to use. Leave NULL to use the latest version.'; +comment on column scheduling_model_specification_goals.priority is e'' + 'The relative priority of the scheduling goal in relation to other goals on the same specification.'; + +create function insert_scheduling_model_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_model_specification_goals smg + where smg.model_id = new.model_id + order by priority desc + limit 1), -1) + 1 + into next_priority; + + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for model_id % is not consecutive', new.priority, new.model_id), + hint = ('The next available priority is %.', next_priority); + end if; + + if new.priority is null then + new.priority = next_priority; + end if; + + update scheduling_model_specification_goals + set priority = priority + 1 + where model_id = new.model_id + and priority >= new.priority; + return new; +end; +$$; + +comment on function insert_scheduling_model_specification_goal_func() is e'' + 'Checks that the inserted priority is consecutive, and reorders (increments) higher or equal priorities to make room.'; + +create trigger insert_scheduling_model_specification_goal + before insert + on scheduling_model_specification_goals + for each row +execute function insert_scheduling_model_specification_goal_func(); + +create function update_scheduling_model_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_model_specification_goals smg + where smg.model_id = new.model_id + order by priority desc + limit 1), -1) + 1 + into next_priority; + + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for model_id % is not consecutive', new.priority, new.model_id), + hint = ('The next available priority is %.', next_priority); + end if; + + if new.priority > old.priority then + update scheduling_model_specification_goals + set priority = priority - 1 + where model_id = new.model_id + and priority between old.priority + 1 and new.priority + and goal_id != new.goal_id; + else + update scheduling_model_specification_goals + set priority = priority + 1 + where model_id = new.model_id + and priority between new.priority and old.priority - 1 + and goal_id != new.goal_id; + end if; + return new; +end; +$$; + +comment on function update_scheduling_model_specification_goal_func() is e'' + 'Checks that the updated priority is consecutive, and reorders priorities to make room.'; + +create trigger update_scheduling_model_specification_goal + before update + on scheduling_model_specification_goals + for each row + when (OLD.priority is distinct from NEW.priority and pg_trigger_depth() < 1) +execute function update_scheduling_model_specification_goal_func(); + +create function delete_scheduling_model_specification_goal_func() + returns trigger + language plpgsql as $$ +begin + update scheduling_model_specification_goals + set priority = priority - 1 + where model_id = old.model_id + and priority > old.priority; + return null; +end; +$$; + +comment on function delete_scheduling_model_specification_goal_func() is e'' + 'Reorders (decrements) priorities to fill the gap from deleted priority.'; + +create trigger delete_scheduling_model_specification_goal + after delete + on scheduling_model_specification_goals + for each row +execute function delete_scheduling_model_specification_goal_func(); + +alter table scheduling_specification_goals + add column goal_revision integer, + alter column priority drop default, + alter column enabled set not null, + -- This constraint's name is too long + drop constraint scheduling_specification_goals_references_scheduling_specification, + add constraint scheduling_specification_goals_specification_exists + foreign key (specification_id) + references scheduling_specification + on update cascade + on delete cascade, + drop constraint scheduling_specification_goals_references_scheduling_goals, + add constraint scheduling_spec_goal_exists + foreign key (goal_id) + references scheduling_goal_metadata + on update cascade + on delete restrict, + add constraint scheduling_spec_goal_definition_exists + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete restrict, + drop constraint scheduling_specification_unique_goal_id; + +comment on table scheduling_specification_goals is e'' + 'The scheduling goals to be executed against a given plan.'; +comment on column scheduling_specification_goals.specification_id is e'' + 'The plan scheduling specification this goal is on. Half of the primary key.'; +comment on column scheduling_specification_goals.goal_id is e'' + 'The id of a specific goal in the specification. Half of the primary key.'; +comment on column scheduling_specification_goals.goal_revision is e'' + 'The version of the goal definition to use. Leave NULL to use the latest version.'; +comment on column scheduling_specification_goals.priority is e'' + 'The relative priority of a scheduling goal in relation to other ' + 'scheduling goals within the same specification.'; +comment on column scheduling_specification_goals.enabled is e'' + 'Whether to run a given goal. Defaults to TRUE.'; +comment on column scheduling_specification_goals.simulate_after is e'' + 'Whether to re-simulate after evaluating this goal and before the next goal.'; + +create or replace function insert_scheduling_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_specification_goals ssg + where ssg.specification_id = new.specification_id + order by priority desc + limit 1), -1) + 1 + into next_priority; + + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for specification_id % is not consecutive', new.priority, new.specification_id), + hint = ('The next available priority is %.', next_priority); + end if; + + if new.priority is null then + new.priority = next_priority; + end if; + + update scheduling_specification_goals + set priority = priority + 1 + where specification_id = new.specification_id + and priority >= new.priority; + return new; +end; +$$; + +create or replace function update_scheduling_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_specification_goals ssg + where ssg.specification_id = new.specification_id + order by priority desc + limit 1), -1) + 1 + into next_priority; + + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for specification_id % is not consecutive', new.priority, new.specification_id), + hint = ('The next available priority is %.', next_priority); + end if; + + if new.priority > old.priority then + update scheduling_specification_goals + set priority = priority - 1 + where specification_id = new.specification_id + and priority between old.priority + 1 and new.priority + and goal_id != new.goal_id; + else + update scheduling_specification_goals + set priority = priority + 1 + where specification_id = new.specification_id + and priority between new.priority and old.priority - 1 + and goal_id != new.goal_id; + end if; + return new; +end; +$$; + +create or replace function delete_scheduling_specification_goal_func() + returns trigger + language plpgsql as $$ +begin + update scheduling_specification_goals + set priority = priority - 1 + where specification_id = old.specification_id + and priority > old.priority; + return null; +end; +$$; + +create function increment_spec_revision_on_goal_spec_update() + returns trigger + security definer +language plpgsql as $$begin + update scheduling_specification + set revision = revision + 1 + where id = new.specification_id; + return new; +end$$; + +create trigger increment_revision_on_goal_update + before insert or update on scheduling_specification_goals + for each row + execute function increment_spec_revision_on_goal_spec_update(); + +create function increment_spec_revision_on_goal_spec_delete() + returns trigger + security definer +language plpgsql as $$begin + update scheduling_specification + set revision = revision + 1 + where id = old.specification_id; + return old; +end$$; + +create trigger increment_revision_on_goal_delete + before delete on scheduling_specification_goals + for each row + execute function increment_spec_revision_on_goal_spec_delete(); + +/* +SCHEDULING SPECIFICATION +*/ +drop trigger increment_revision_on_goal_update on scheduling_goal; +drop function increment_revision_on_goal_update(); + +/* +DATA MIGRATION +*/ +insert into scheduling_goal_metadata(id, name, description, public, owner, updated_by, created_at, updated_at) +select id, name, description, false, author, last_modified_by, created_date, modified_date +from scheduling_goal; + +insert into scheduling_goal_definition(goal_id, definition, author, created_at) +select id, definition, author, modified_date +from scheduling_goal; + +insert into scheduling_model_specification_goals(model_id, goal_id) +select model_id, id +from scheduling_goal; + +/* +POST DATA MIGRATION TABLE CHANGES +*/ +alter table scheduling_goal_metadata + alter column id set generated always; + +create function scheduling_goal_metadata_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp +before update on scheduling_goal_metadata +for each row +execute function scheduling_goal_metadata_set_updated_at(); + +/* +DROP ORIGINAL +*/ +drop trigger update_logging_on_update_scheduling_goal_trigger on scheduling_goal; +drop function update_logging_on_update_scheduling_goal(); +drop table scheduling_goal; + call migrations.mark_migration_applied('13'); diff --git a/scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_definition_tags.sql b/scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_definition_tags.sql new file mode 100644 index 0000000000..6c43be051e --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_definition_tags.sql @@ -0,0 +1,12 @@ +create table metadata.scheduling_goal_definition_tags ( + goal_id integer not null, + goal_revision integer not null, + tag_id integer not null, + primary key (goal_id, goal_revision, tag_id), + foreign key (goal_id, goal_revision) references scheduling_goal_definition + on update cascade + on delete cascade +); + +comment on table metadata.scheduling_goal_definition_tags is e'' + 'The tags associated with a specific scheduling condition definition.'; diff --git a/scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_tags.sql b/scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_tags.sql index fb2d243e29..1effedabf2 100644 --- a/scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_tags.sql +++ b/scheduler-server/sql/scheduler/tables/metadata/scheduling_goal_tags.sql @@ -1,5 +1,5 @@ create table metadata.scheduling_goal_tags ( - goal_id integer references public.scheduling_goal + goal_id integer references public.scheduling_goal_metadata on update cascade on delete cascade, tag_id integer not null, diff --git a/scheduler-server/sql/scheduler/tables/scheduling_goal.sql b/scheduler-server/sql/scheduler/tables/scheduling_goal.sql deleted file mode 100644 index 5a5ddfdc06..0000000000 --- a/scheduler-server/sql/scheduler/tables/scheduling_goal.sql +++ /dev/null @@ -1,54 +0,0 @@ -create table scheduling_goal ( - id integer generated always as identity, - revision integer not null default 0, - name text not null, - definition text not null, - - model_id integer not null, - description text not null default '', - author text null, - last_modified_by text null, - created_date timestamptz not null default now(), - modified_date timestamptz not null default now(), - - constraint scheduling_goal_synthetic_key - primary key (id) -); - -comment on table scheduling_goal is e'' - 'A goal for scheduling of a plan.'; -comment on column scheduling_goal.id is e'' - 'The synthetic identifier for this scheduling goal.'; -comment on column scheduling_goal.revision is e'' - 'A monotonic clock that ticks for every change to this scheduling goal.'; -comment on column scheduling_goal.definition is e'' - 'The source code for a Typescript module defining this scheduling goal'; -comment on column scheduling_goal.model_id is e'' - 'The mission model used to which this scheduling goal is associated.'; -comment on column scheduling_goal.name is e'' - 'A short human readable name for this goal'; -comment on column scheduling_goal.description is e'' - 'A longer text description of this scheduling goal.'; -comment on column scheduling_goal.author is e'' - 'The original user who authored this scheduling goal.'; -comment on column scheduling_goal.last_modified_by is e'' - 'The last user who modified this scheduling goal.'; -comment on column scheduling_goal.created_date is e'' - 'The date this scheduling goal was created.'; -comment on column scheduling_goal.modified_date is e'' - 'The date this scheduling goal was last modified.'; - -create function update_logging_on_update_scheduling_goal() - returns trigger - security definer -language plpgsql as $$begin - new.revision = old.revision + 1; - new.modified_date = now(); -return new; -end$$; - -create trigger update_logging_on_update_scheduling_goal_trigger - before update on scheduling_goal - for each row - when (pg_trigger_depth() < 1) - execute function update_logging_on_update_scheduling_goal(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_goal_definition.sql b/scheduler-server/sql/scheduler/tables/scheduling_goal_definition.sql new file mode 100644 index 0000000000..62588d6c29 --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/scheduling_goal_definition.sql @@ -0,0 +1,52 @@ +create table scheduling_goal_definition( + goal_id integer not null, + revision integer not null default 0, + + definition text not null, + author text, + created_at timestamptz not null default now(), + + constraint scheduling_goal_definition_pkey + primary key (goal_id, revision), + constraint scheduling_goal_definition_goal_exists + foreign key (goal_id) + references scheduling_goal_metadata + on update cascade + on delete cascade +); + +comment on table scheduling_goal_definition is e'' + 'The specific revisions of a scheduling goal''s definition'; +comment on column scheduling_goal_definition.revision is e'' + 'An identifier of this definition.'; +comment on column scheduling_goal_definition.definition is e'' + 'An executable expression in the Merlin scheduling language.'; +comment on column scheduling_goal_definition.author is e'' + 'The user who authored this revision.'; +comment on column scheduling_goal_definition.created_at is e'' + 'When this revision was created.'; + +create function scheduling_goal_definition_set_revision() +returns trigger +volatile +language plpgsql as $$ +declare + max_revision integer; +begin + -- Grab the current max value of revision, or -1, if this is the first revision + select coalesce((select revision + from scheduling_goal_definition + where goal_id = new.goal_id + order by revision desc + limit 1), -1) + into max_revision; + + new.revision = max_revision + 1; + return new; +end +$$; + +create trigger scheduling_goal_definition_set_revision + before insert on scheduling_goal_definition + for each row + execute function scheduling_goal_definition_set_revision(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_goal_metadata.sql b/scheduler-server/sql/scheduler/tables/scheduling_goal_metadata.sql new file mode 100644 index 0000000000..e968d901c3 --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/scheduling_goal_metadata.sql @@ -0,0 +1,51 @@ +create table scheduling_goal_metadata ( + id integer generated always as identity, + + name text not null, + description text not null default '', + public boolean not null default false, + + owner text, + updated_by text, + + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + + constraint scheduling_goal_metadata_pkey + primary key (id) +); + +-- A partial index is used to enforce name uniqueness only on goals visible to other users +create unique index goal_name_unique_if_published on scheduling_goal_metadata (name) where public; + +comment on table scheduling_goal_metadata is e'' + 'A goal for scheduling a plan.'; +comment on column scheduling_goal_metadata.id is e'' + 'The unique identifier of the goal'; +comment on column scheduling_goal_metadata.name is e'' + 'A human-meaningful name.'; +comment on column scheduling_goal_metadata.description is e'' + 'A detailed description suitable for long-form documentation.'; +comment on column scheduling_goal_metadata.public is e'' + 'Whether this goal is visible to all users.'; +comment on column scheduling_goal_metadata.owner is e'' + 'The user responsible for this goal.'; +comment on column scheduling_goal_metadata.updated_by is e'' + 'The user who last modified this goal''s metadata.'; +comment on column scheduling_goal_metadata.created_at is e'' + 'The time at which this goal was created.'; +comment on column scheduling_goal_metadata.updated_at is e'' + 'The time at which this goal''s metadata was last modified.'; + +create function scheduling_goal_metadata_set_updated_at() +returns trigger +security definer +language plpgsql as $$begin + new.updated_at = now(); + return new; +end$$; + +create trigger set_timestamp +before update on scheduling_goal_metadata +for each row +execute function scheduling_goal_metadata_set_updated_at(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_model_specification_goals.sql b/scheduler-server/sql/scheduler/tables/scheduling_model_specification_goals.sql new file mode 100644 index 0000000000..65738a481d --- /dev/null +++ b/scheduler-server/sql/scheduler/tables/scheduling_model_specification_goals.sql @@ -0,0 +1,140 @@ +create table scheduling_model_specification_goals( + model_id integer not null, + goal_id integer not null, + goal_revision integer, -- latest is NULL + priority integer not null, + + primary key (model_id, goal_id), + foreign key (goal_id) + references scheduling_goal_metadata + on update cascade + on delete restrict, + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete restrict, + constraint model_spec_unique_goal_priorities + unique (model_id, priority) deferrable initially deferred, + constraint model_spec_nonnegative_priority + check (priority >= 0) +); + +comment on table scheduling_model_specification_goals is e'' +'The set of scheduling goals that all plans using the model should include in their scheduling specification.'; +comment on column scheduling_model_specification_goals.model_id is e'' +'The model which this specification is for. Half of the primary key.'; +comment on column scheduling_model_specification_goals.goal_id is e'' +'The id of a specific scheduling goal in the specification. Half of the primary key.'; +comment on column scheduling_model_specification_goals.goal_revision is e'' +'The version of the scheduling goal definition to use. Leave NULL to use the latest version.'; +comment on column scheduling_model_specification_goals.priority is e'' + 'The relative priority of the scheduling goal in relation to other goals on the same specification.'; + +create function insert_scheduling_model_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_model_specification_goals smg + where smg.model_id = new.model_id + order by priority desc + limit 1), -1) + 1 + into next_priority; + + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for model_id % is not consecutive', new.priority, new.model_id), + hint = ('The next available priority is %.', next_priority); + end if; + + if new.priority is null then + new.priority = next_priority; + end if; + + update scheduling_model_specification_goals + set priority = priority + 1 + where model_id = new.model_id + and priority >= new.priority; + return new; +end; +$$; + +comment on function insert_scheduling_model_specification_goal_func() is e'' + 'Checks that the inserted priority is consecutive, and reorders (increments) higher or equal priorities to make room.'; + +create trigger insert_scheduling_model_specification_goal + before insert + on scheduling_model_specification_goals + for each row +execute function insert_scheduling_model_specification_goal_func(); + +create function update_scheduling_model_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_model_specification_goals smg + where smg.model_id = new.model_id + order by priority desc + limit 1), -1) + 1 + into next_priority; + + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for model_id % is not consecutive', new.priority, new.model_id), + hint = ('The next available priority is %.', next_priority); + end if; + + if new.priority > old.priority then + update scheduling_model_specification_goals + set priority = priority - 1 + where model_id = new.model_id + and priority between old.priority + 1 and new.priority + and goal_id != new.goal_id; + else + update scheduling_model_specification_goals + set priority = priority + 1 + where model_id = new.model_id + and priority between new.priority and old.priority - 1 + and goal_id != new.goal_id; + end if; + return new; +end; +$$; + +comment on function update_scheduling_model_specification_goal_func() is e'' + 'Checks that the updated priority is consecutive, and reorders priorities to make room.'; + +create trigger update_scheduling_model_specification_goal + before update + on scheduling_model_specification_goals + for each row + when (OLD.priority is distinct from NEW.priority and pg_trigger_depth() < 1) +execute function update_scheduling_model_specification_goal_func(); + +create function delete_scheduling_model_specification_goal_func() + returns trigger + language plpgsql as $$ +begin + update scheduling_model_specification_goals + set priority = priority - 1 + where model_id = old.model_id + and priority > old.priority; + return null; +end; +$$; + +comment on function delete_scheduling_model_specification_goal_func() is e'' + 'Reorders (decrements) priorities to fill the gap from deleted priority.'; + +create trigger delete_scheduling_model_specification_goal + after delete + on scheduling_model_specification_goals + for each row +execute function delete_scheduling_model_specification_goal_func(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_specification.sql b/scheduler-server/sql/scheduler/tables/scheduling_specification.sql index e26c133da9..7e82f023c7 100644 --- a/scheduler-server/sql/scheduler/tables/scheduling_specification.sql +++ b/scheduler-server/sql/scheduler/tables/scheduling_specification.sql @@ -39,26 +39,8 @@ language plpgsql as $$begin return new; end$$; -create function increment_revision_on_goal_update() - returns trigger - security definer -language plpgsql as $$begin - with goals as ( - select g.specification_id from scheduling_specification_goals as g - where g.goal_id = new.id - ) - update scheduling_specification set revision = revision + 1 - where exists(select 1 from goals where specification_id = id); - return new; -end$$; - create trigger increment_revision_on_update_trigger before update on scheduling_specification for each row when (pg_trigger_depth() < 1) execute function increment_revision_on_update(); - -create trigger increment_revision_on_goal_update - before update on scheduling_goal - for each row - execute function increment_revision_on_goal_update(); diff --git a/scheduler-server/sql/scheduler/tables/scheduling_specification_goals.sql b/scheduler-server/sql/scheduler/tables/scheduling_specification_goals.sql index b2f7d21b2d..c39013dbbb 100644 --- a/scheduler-server/sql/scheduler/tables/scheduling_specification_goals.sql +++ b/scheduler-server/sql/scheduler/tables/scheduling_specification_goals.sql @@ -1,12 +1,9 @@ create table scheduling_specification_goals ( specification_id integer not null, goal_id integer not null, - priority integer - not null - default null -- Nulls are detected and replaced with the next - -- available priority by the insert trigger - constraint non_negative_specification_goal_priority check (priority >= 0), - enabled boolean default true, + goal_revision integer, -- latest is null + priority integer not null, + enabled boolean not null default true, simulate_after boolean not null default true, @@ -14,98 +11,74 @@ create table scheduling_specification_goals ( primary key (specification_id, goal_id), constraint scheduling_specification_goals_unique_priorities unique (specification_id, priority) deferrable initially deferred, - constraint scheduling_specification_goals_references_scheduling_specification + constraint scheduling_specification_goals_specification_exists foreign key (specification_id) references scheduling_specification on update cascade on delete cascade, - constraint scheduling_specification_goals_references_scheduling_goals + constraint non_negative_specification_goal_priority check (priority >= 0), + constraint scheduling_spec_goal_exists foreign key (goal_id) - references scheduling_goal + references scheduling_goal_metadata on update cascade - on delete cascade, - constraint scheduling_specification_unique_goal_id - unique (goal_id) + on delete restrict, + constraint scheduling_spec_goal_definition_exists + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete restrict ); comment on table scheduling_specification_goals is e'' - 'A join table associating scheduling specifications with scheduling goals.'; + 'The scheduling goals to be executed against a given plan.'; comment on column scheduling_specification_goals.specification_id is e'' - 'The ID of the scheduling specification a scheduling goal is associated with.'; + 'The plan scheduling specification this goal is on. Half of the primary key.'; comment on column scheduling_specification_goals.goal_id is e'' - 'The ID of the scheduling goal a scheduling specification is associated with.'; + 'The id of a specific goal in the specification. Half of the primary key.'; +comment on column scheduling_specification_goals.goal_revision is e'' + 'The version of the goal definition to use. Leave NULL to use the latest version.'; comment on column scheduling_specification_goals.priority is e'' 'The relative priority of a scheduling goal in relation to other ' 'scheduling goals within the same specification.'; +comment on column scheduling_specification_goals.enabled is e'' + 'Whether to run a given goal. Defaults to TRUE.'; comment on column scheduling_specification_goals.simulate_after is e'' 'Whether to re-simulate after evaluating this goal and before the next goal.'; -create or replace function insert_scheduling_specification_goal_func() - returns trigger as $$begin - if NEW.priority IS NULL then - NEW.priority = ( - select coalesce(max(priority), -1) from scheduling_specification_goals - where specification_id = NEW.specification_id - ) + 1; - elseif NEW.priority > ( - select coalesce(max(priority), -1) from scheduling_specification_goals - where specification_id = new.specification_id - ) + 1 then - raise exception 'Inserted priority % for specification_id % is not consecutive', NEW.priority, NEW.specification_id; - end if; - update scheduling_specification_goals - set priority = priority + 1 - where specification_id = NEW.specification_id - and priority >= NEW.priority; - return NEW; - end;$$ -language plpgsql; +create function insert_scheduling_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_specification_goals ssg + where ssg.specification_id = new.specification_id + order by priority desc + limit 1), -1) + 1 + into next_priority; -comment on function insert_scheduling_specification_goal_func is e'' - 'Checks that the inserted priority is consecutive, and reorders (increments) higher or equal priorities to make room.'; + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for specification_id % is not consecutive', new.priority, new.specification_id), + hint = ('The next available priority is %.', next_priority); + end if; -create or replace function update_scheduling_specification_goal_func() - returns trigger as $$begin - if (pg_trigger_depth() = 1) then - if NEW.priority > OLD.priority then - if NEW.priority > ( - select coalesce(max(priority), -1) from scheduling_specification_goals - where specification_id = new.specification_id - ) + 1 then - raise exception 'Updated priority % for specification_id % is not consecutive', NEW.priority, new.specification_id; - end if; - update scheduling_specification_goals - set priority = priority - 1 - where specification_id = NEW.specification_id - and priority between OLD.priority + 1 and NEW.priority - and goal_id <> NEW.goal_id; - else - update scheduling_specification_goals - set priority = priority + 1 - where specification_id = NEW.specification_id - and priority between NEW.priority and OLD.priority - 1 - and goal_id <> NEW.goal_id; - end if; - end if; - return NEW; - end;$$ -language plpgsql; + if new.priority is null then + new.priority = next_priority; + end if; -comment on function update_scheduling_specification_goal_func is e'' - 'Checks that the updated priority is consecutive, and reorders priorities to make room.'; + update scheduling_specification_goals + set priority = priority + 1 + where specification_id = new.specification_id + and priority >= new.priority; + return new; +end; +$$; -create function delete_scheduling_specification_goal_func() - returns trigger as $$begin - update scheduling_specification_goals - set priority = priority - 1 - where specification_id = OLD.specification_id - and priority > OLD.priority; - return null; - end;$$ -language plpgsql; - -comment on function delete_scheduling_specification_goal_func() is e'' - 'Reorders (decrements) priorities to fill the gap from deleted priority.'; +comment on function insert_scheduling_specification_goal_func is e'' + 'Checks that the inserted priority is consecutive, and reorders (increments) higher or equal priorities to make room.'; create trigger insert_scheduling_specification_goal before insert @@ -113,15 +86,100 @@ create trigger insert_scheduling_specification_goal for each row execute function insert_scheduling_specification_goal_func(); +create function update_scheduling_specification_goal_func() + returns trigger + language plpgsql as $$ + declare + next_priority integer; +begin + select coalesce( + (select priority + from scheduling_specification_goals ssg + where ssg.specification_id = new.specification_id + order by priority desc + limit 1), -1) + 1 + into next_priority; + + if new.priority > next_priority then + raise numeric_value_out_of_range using + message = ('Updated priority % for specification_id % is not consecutive', new.priority, new.specification_id), + hint = ('The next available priority is %.', next_priority); + end if; + + if new.priority > old.priority then + update scheduling_specification_goals + set priority = priority - 1 + where specification_id = new.specification_id + and priority between old.priority + 1 and new.priority + and goal_id != new.goal_id; + else + update scheduling_specification_goals + set priority = priority + 1 + where specification_id = new.specification_id + and priority between new.priority and old.priority - 1 + and goal_id != new.goal_id; + end if; + return new; +end; +$$; + +comment on function update_scheduling_specification_goal_func is e'' + 'Checks that the updated priority is consecutive, and reorders priorities to make room.'; + create trigger update_scheduling_specification_goal before update on scheduling_specification_goals for each row - when (OLD.priority is distinct from NEW.priority) + when (OLD.priority is distinct from NEW.priority and pg_trigger_depth() < 1) execute function update_scheduling_specification_goal_func(); +create function delete_scheduling_specification_goal_func() + returns trigger + language plpgsql as $$ +begin + update scheduling_specification_goals + set priority = priority - 1 + where specification_id = old.specification_id + and priority > old.priority; + return null; +end; +$$; + +comment on function delete_scheduling_specification_goal_func() is e'' + 'Reorders (decrements) priorities to fill the gap from deleted priority.'; + create trigger delete_scheduling_specification_goal after delete on scheduling_specification_goals for each row execute function delete_scheduling_specification_goal_func(); + +create function increment_spec_revision_on_goal_spec_update() + returns trigger + security definer +language plpgsql as $$begin + update scheduling_specification + set revision = revision + 1 + where id = new.specification_id; + return new; +end$$; + +create trigger increment_revision_on_goal_update + before insert or update on scheduling_specification_goals + for each row + execute function increment_spec_revision_on_goal_spec_update(); + +create function increment_spec_revision_on_goal_spec_delete() + returns trigger + security definer +language plpgsql as $$begin + update scheduling_specification + set revision = revision + 1 + where id = old.specification_id; + return old; +end$$; + +create trigger increment_revision_on_goal_delete + before delete on scheduling_specification_goals + for each row + execute function increment_spec_revision_on_goal_spec_delete(); From 663cbc5d277600c9b3509f29924ea8da70139179 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 1 Feb 2024 16:19:24 -0800 Subject: [PATCH 141/159] Update Scheduling Request Tables - Record additional data from the specification in the request record - Update analysis tables to include the definition of the goal used --- .../down.sql | 111 +++++++++++++ .../up.sql | 154 ++++++++++++++++++ .../tables/scheduling_goal_analysis.sql | 10 +- ...uling_goal_analysis_created_activities.sql | 9 +- ...ng_goal_analysis_satisfying_activities.sql | 9 +- .../scheduler/tables/scheduling_request.sql | 55 +++++-- 6 files changed, 323 insertions(+), 25 deletions(-) diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql index a6b39092fa..86b0a91888 100644 --- a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/down.sql @@ -59,6 +59,117 @@ create trigger update_logging_on_update_scheduling_goal_trigger when (pg_trigger_depth() < 1) execute function update_logging_on_update_scheduling_goal(); +/* +ANALYSIS TABLES +*/ +/* Dropped FKs are restored first */ +alter table scheduling_goal_analysis_satisfying_activities + drop constraint satisfying_activities_references_scheduling_goal, + add constraint satisfying_activities_references_scheduling_goal + foreign key (goal_id) + references scheduling_goal + on update cascade + on delete cascade, + drop constraint satisfying_activities_primary_key, + add constraint satisfying_activities_primary_key + primary key (analysis_id, goal_id, activity_id), + drop column goal_revision; + +alter table scheduling_goal_analysis_created_activities + drop constraint created_activities_references_scheduling_goal, + add constraint created_activities_references_scheduling_goal + foreign key (goal_id) + references scheduling_goal + on update cascade + on delete cascade, + drop constraint created_activities_primary_key, + add constraint created_activities_primary_key + primary key (analysis_id, goal_id, activity_id), + drop column goal_revision; + +alter table scheduling_goal_analysis + drop constraint scheduling_goal_analysis_references_scheduling_goal, + add constraint scheduling_goal_analysis_references_scheduling_goal + foreign key (goal_id) + references scheduling_goal + on update cascade + on delete cascade, + drop constraint scheduling_goal_analysis_primary_key, + add constraint scheduling_goal_analysis_primary_key + primary key (analysis_id, goal_id), + drop column goal_revision; + +/* +SCHEDULING REQUEST +*/ +create or replace function notify_scheduler_workers () +returns trigger +security definer +language plpgsql as $$ +begin + perform ( + with payload(specification_revision, + specification_id, + analysis_id) as + ( + select NEW.specification_revision, + NEW.specification_id, + NEW.analysis_id + ) + select pg_notify('scheduling_request_notification', json_strip_nulls(row_to_json(payload))::text) + from payload + ); + return null; +end$$; + +/* These FKs are dropped ahead of the pkey swap to remove the dependency on the PK's index */ +alter table scheduling_goal_analysis_satisfying_activities + drop constraint satisfying_activities_references_scheduling_request; +alter table scheduling_goal_analysis_created_activities + drop constraint created_activities_references_scheduling_request; +alter table scheduling_goal_analysis + drop constraint scheduling_goal_analysis_references_scheduling_request; + +alter table scheduling_request + drop constraint start_before_end, + drop constraint scheduling_request_unique, + add constraint scheduling_request_analysis_unique + unique (analysis_id), + drop constraint scheduling_request_pkey, + add constraint scheduling_request_primary_key + primary key(specification_id, specification_revision), + drop column simulation_arguments, + drop column horizon_end, + drop column horizon_start, + drop column plan_revision; + +comment on column scheduling_request.canceled is null; +comment on column scheduling_request.reason is e'' + 'The reason for failure when a scheduling request fails.'; +comment on column scheduling_request.dataset_id is null; + +/* Restore dropped FKs */ +alter table scheduling_goal_analysis_satisfying_activities + add constraint satisfying_activities_references_scheduling_request + foreign key (analysis_id) + references scheduling_request (analysis_id) + on update cascade + on delete cascade; + +alter table scheduling_goal_analysis_created_activities + add constraint created_activities_references_scheduling_request + foreign key (analysis_id) + references scheduling_request (analysis_id) + on update cascade + on delete cascade; + +alter table scheduling_goal_analysis + add constraint scheduling_goal_analysis_references_scheduling_request + foreign key (analysis_id) + references scheduling_request (analysis_id) + on update cascade + on delete cascade; + /* DATA MIGRATION */ diff --git a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql index f9ff0a991f..e2e8af38f9 100644 --- a/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql +++ b/deployment/hasura/migrations/AerieScheduler/13_versioning_scheduling_goals_conditions/up.sql @@ -718,6 +718,160 @@ before update on scheduling_goal_metadata for each row execute function scheduling_goal_metadata_set_updated_at(); +/* +SCHEDULING REQUEST +*/ +/* These FKs are dropped ahead of the pkey swap to remove the dependency on analysisId's index */ +alter table scheduling_goal_analysis_satisfying_activities + drop constraint satisfying_activities_references_scheduling_request; +alter table scheduling_goal_analysis_created_activities + drop constraint created_activities_references_scheduling_request; +alter table scheduling_goal_analysis + drop constraint scheduling_goal_analysis_references_scheduling_request; + +alter table scheduling_request + add column plan_revision integer not null default -1, + add column horizon_start timestamptz, + add column horizon_end timestamptz, + add column simulation_arguments jsonb not null default '{}', + drop constraint scheduling_request_primary_key, + add constraint scheduling_request_pkey primary key(analysis_id), + drop constraint scheduling_request_analysis_unique, + add constraint scheduling_request_unique + unique (specification_id, specification_revision, plan_revision), + add constraint start_before_end + check (horizon_start <= horizon_end); + +-- Insert values from the current config for the horizon +-- This is fine as temporal subset scheduling isn't really a feature +update scheduling_request + set horizon_start = s.horizon_start, + horizon_end = s.horizon_end +from scheduling_specification s +where s.id = scheduling_request.specification_id; + +-- Drop defaults +alter table scheduling_request + alter column plan_revision drop default, + alter column horizon_start set not null, + alter column horizon_end set not null, + alter column simulation_arguments drop default ; + +comment on column scheduling_request.dataset_id is e'' + 'The dataset containing the final simulation results for the simulation. NULL if no simulations were run during scheduling.'; +comment on column scheduling_request.plan_revision is e'' + 'The revision of the plan corresponding to the given revision of the dataset.'; +comment on column scheduling_request.reason is e'' + 'The reason for failure in the event a scheduling request fails.'; +comment on column scheduling_request.canceled is e'' + 'Whether the scheduling run has been marked as canceled.'; +comment on column scheduling_request.horizon_start is e'' + 'The start of the scheduling and simulation horizon for this scheduling run.'; +comment on column scheduling_request.horizon_end is e'' + 'The end of the scheduling and simulation horizon for this scheduling run.'; +comment on column scheduling_request.simulation_arguments is e'' + 'The arguments simulations run during the scheduling run will use.'; + +/* Restore dropped FKs */ +alter table scheduling_goal_analysis_satisfying_activities + add constraint satisfying_activities_references_scheduling_request + foreign key (analysis_id) + references scheduling_request (analysis_id) + on update cascade + on delete cascade; + +alter table scheduling_goal_analysis_created_activities + add constraint created_activities_references_scheduling_request + foreign key (analysis_id) + references scheduling_request (analysis_id) + on update cascade + on delete cascade; + +alter table scheduling_goal_analysis + add constraint scheduling_goal_analysis_references_scheduling_request + foreign key (analysis_id) + references scheduling_request (analysis_id) + on update cascade + on delete cascade; + +create or replace function notify_scheduler_workers () +returns trigger +security definer +language plpgsql as $$ +begin + perform ( + with payload(specification_revision, + plan_revision, + specification_id, + analysis_id) as + ( + select NEW.specification_revision, + NEW.plan_revision, + NEW.specification_id, + NEW.analysis_id + ) + select pg_notify('scheduling_request_notification', json_strip_nulls(row_to_json(payload))::text) + from payload + ); + return null; +end$$; + +/* +ANALYSIS TABLES +*/ +/* Dropped FKs are restored first */ +/* 0 is the initial default as all revisions will be at 0 by this point in the migration */ +alter table scheduling_goal_analysis + add column goal_revision integer not null default 0, + drop constraint scheduling_goal_analysis_primary_key, + add constraint scheduling_goal_analysis_primary_key + primary key (analysis_id, goal_id, goal_revision), + drop constraint scheduling_goal_analysis_references_scheduling_goal, + add constraint scheduling_goal_analysis_references_scheduling_goal + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete cascade; +alter table scheduling_goal_analysis + alter column goal_revision drop default; + +comment on column scheduling_goal_analysis.goal_revision is e'' + 'The associated version of the goal definition used.'; + +alter table scheduling_goal_analysis_created_activities + add column goal_revision integer not null default 0, + drop constraint created_activities_primary_key, + add constraint created_activities_primary_key + primary key (analysis_id, goal_id, goal_revision, activity_id), + drop constraint created_activities_references_scheduling_goal, + add constraint created_activities_references_scheduling_goal + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete cascade; +alter table scheduling_goal_analysis_created_activities + alter column goal_revision drop default; + +comment on column scheduling_goal_analysis_created_activities.goal_revision is e'' + 'The associated version of the goal definition used.'; + +alter table scheduling_goal_analysis_satisfying_activities + add column goal_revision integer not null default 0, + drop constraint satisfying_activities_primary_key, + add constraint satisfying_activities_primary_key + primary key (analysis_id, goal_id, goal_revision, activity_id), + drop constraint satisfying_activities_references_scheduling_goal, + add constraint satisfying_activities_references_scheduling_goal + foreign key (goal_id, goal_revision) + references scheduling_goal_definition + on update cascade + on delete cascade; +alter table scheduling_goal_analysis_satisfying_activities + alter column goal_revision drop default; + +comment on column scheduling_goal_analysis_satisfying_activities.goal_revision is e'' + 'The associated version of the goal definition used.'; + /* DROP ORIGINAL */ diff --git a/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis.sql b/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis.sql index f5cf8a9215..fd4039e465 100644 --- a/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis.sql +++ b/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis.sql @@ -1,19 +1,19 @@ create table scheduling_goal_analysis ( analysis_id integer not null, goal_id integer not null, - + goal_revision integer not null, satisfied boolean not null, constraint scheduling_goal_analysis_primary_key - primary key (analysis_id, goal_id), + primary key (analysis_id, goal_id, goal_revision), constraint scheduling_goal_analysis_references_scheduling_request foreign key (analysis_id) references scheduling_request (analysis_id) on update cascade on delete cascade, constraint scheduling_goal_analysis_references_scheduling_goal - foreign key (goal_id) - references scheduling_goal + foreign key (goal_id, goal_revision) + references scheduling_goal_definition on update cascade on delete cascade ); @@ -24,5 +24,7 @@ comment on column scheduling_goal_analysis.analysis_id is e'' 'The associated analysis ID.'; comment on column scheduling_goal_analysis.goal_id is e'' 'The associated goal ID.'; +comment on column scheduling_goal_analysis.goal_revision is e'' + 'The associated version of the goal definition used.'; comment on column scheduling_goal_analysis.satisfied is e'' 'Whether the associated goal was satisfied by the scheduling run.'; diff --git a/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_created_activities.sql b/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_created_activities.sql index ceb09eb6cc..4d231c9cc2 100644 --- a/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_created_activities.sql +++ b/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_created_activities.sql @@ -1,18 +1,19 @@ create table scheduling_goal_analysis_created_activities ( analysis_id integer not null, goal_id integer not null, + goal_revision integer not null, activity_id integer not null, constraint created_activities_primary_key - primary key (analysis_id, goal_id, activity_id), + primary key (analysis_id, goal_id, goal_revision, activity_id), constraint created_activities_references_scheduling_request foreign key (analysis_id) references scheduling_request (analysis_id) on update cascade on delete cascade, constraint created_activities_references_scheduling_goal - foreign key (goal_id) - references scheduling_goal + foreign key (goal_id, goal_revision) + references scheduling_goal_definition on update cascade on delete cascade ); @@ -23,5 +24,7 @@ comment on column scheduling_goal_analysis_created_activities.analysis_id is e'' 'The associated analysis ID.'; comment on column scheduling_goal_analysis_created_activities.goal_id is e'' 'The associated goal ID.'; +comment on column scheduling_goal_analysis_created_activities.goal_revision is e'' + 'The associated version of the goal definition used.'; comment on column scheduling_goal_analysis_created_activities.activity_id is e'' 'The ID of an activity instance created to satisfy the associated goal.'; diff --git a/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_satisfying_activities.sql b/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_satisfying_activities.sql index a9401aa110..2b88ae2069 100644 --- a/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_satisfying_activities.sql +++ b/scheduler-server/sql/scheduler/tables/scheduling_goal_analysis_satisfying_activities.sql @@ -1,18 +1,19 @@ create table scheduling_goal_analysis_satisfying_activities ( analysis_id integer not null, goal_id integer not null, + goal_revision integer not null, activity_id integer not null, constraint satisfying_activities_primary_key - primary key (analysis_id, goal_id, activity_id), + primary key (analysis_id, goal_id, goal_revision, activity_id), constraint satisfying_activities_references_scheduling_request foreign key (analysis_id) references scheduling_request (analysis_id) on update cascade on delete cascade, constraint satisfying_activities_references_scheduling_goal - foreign key (goal_id) - references scheduling_goal + foreign key (goal_id, goal_revision) + references scheduling_goal_definition on update cascade on delete cascade ); @@ -23,5 +24,7 @@ comment on column scheduling_goal_analysis_satisfying_activities.analysis_id is 'The associated analysis ID.'; comment on column scheduling_goal_analysis_satisfying_activities.goal_id is e'' 'The associated goal ID.'; +comment on column scheduling_goal_analysis_satisfying_activities.goal_revision is e'' + 'The associated version of the goal definition used.'; comment on column scheduling_goal_analysis_satisfying_activities.activity_id is e'' 'The ID of an activity instance satisfying the associated goal.'; diff --git a/scheduler-server/sql/scheduler/tables/scheduling_request.sql b/scheduler-server/sql/scheduler/tables/scheduling_request.sql index 0586877ec3..431b1bb5d8 100644 --- a/scheduler-server/sql/scheduler/tables/scheduling_request.sql +++ b/scheduler-server/sql/scheduler/tables/scheduling_request.sql @@ -1,41 +1,64 @@ create type status_t as enum('pending', 'incomplete', 'failed', 'success'); create table scheduling_request ( - specification_id integer not null, analysis_id integer generated always as identity, - requested_by text, - requested_at timestamptz not null default now(), + specification_id integer not null, + dataset_id integer default null, + specification_revision integer not null, + plan_revision integer not null, + + -- Scheduling State status status_t not null default 'pending', reason jsonb null, canceled boolean not null default false, - dataset_id integer default null, - specification_revision integer not null, + -- Simulation Arguments Used in Scheduling + horizon_start timestamptz not null, + horizon_end timestamptz not null, + simulation_arguments jsonb not null, + + -- Additional Metadata + requested_by text, + requested_at timestamptz not null default now(), - constraint scheduling_request_primary_key - primary key(specification_id, specification_revision), - constraint scheduling_request_analysis_unique - unique (analysis_id), + constraint scheduling_request_pkey + primary key(analysis_id), + constraint scheduling_request_unique + unique (specification_id, specification_revision, plan_revision), constraint scheduling_request_references_scheduling_specification foreign key(specification_id) references scheduling_specification on update cascade - on delete cascade + on delete cascade, + constraint start_before_end + check (horizon_start <= horizon_end) ); comment on table scheduling_request is e'' 'The status of a scheduling run that is to be performed (or has been performed).'; -comment on column scheduling_request.specification_id is e'' - 'The ID of scheduling specification for this scheduling run.'; comment on column scheduling_request.analysis_id is e'' 'The ID associated with the analysis of this scheduling run.'; +comment on column scheduling_request.specification_id is e'' + 'The ID of scheduling specification for this scheduling run.'; +comment on column scheduling_request.dataset_id is e'' + 'The dataset containing the final simulation results for the simulation. NULL if no simulations were run during scheduling.'; +comment on column scheduling_request.specification_revision is e'' + 'The revision of the scheduling_specification associated with this request.'; +comment on column scheduling_request.plan_revision is e'' + 'The revision of the plan corresponding to the given revision of the dataset.'; comment on column scheduling_request.status is e'' 'The state of the the scheduling request.'; comment on column scheduling_request.reason is e'' - 'The reason for failure when a scheduling request fails.'; -comment on column scheduling_request.specification_revision is e'' - 'The revision of the scheduling_specification associated with this request.'; + 'The reason for failure in the event a scheduling request fails.'; +comment on column scheduling_request.canceled is e'' + 'Whether the scheduling run has been marked as canceled.'; +comment on column scheduling_request.horizon_start is e'' + 'The start of the scheduling and simulation horizon for this scheduling run.'; +comment on column scheduling_request.horizon_end is e'' + 'The end of the scheduling and simulation horizon for this scheduling run.'; +comment on column scheduling_request.simulation_arguments is e'' + 'The arguments simulations run during the scheduling run will use.'; comment on column scheduling_request.requested_by is e'' 'The user who made the scheduling request.'; comment on column scheduling_request.requested_at is e'' @@ -51,10 +74,12 @@ language plpgsql as $$ begin perform ( with payload(specification_revision, + plan_revision, specification_id, analysis_id) as ( select NEW.specification_revision, + NEW.plan_revision, NEW.specification_id, NEW.analysis_id ) From f41e20c69bcb89e6e4af166577197e65550f1b7f Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Mon, 29 Jan 2024 12:54:47 -0800 Subject: [PATCH 142/159] Update Scheduler DB init code --- .../sql/scheduler/applied_migrations.sql | 1 + scheduler-server/sql/scheduler/init.sql | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/scheduler-server/sql/scheduler/applied_migrations.sql b/scheduler-server/sql/scheduler/applied_migrations.sql index 347ff9e5ab..e09c03fe65 100644 --- a/scheduler-server/sql/scheduler/applied_migrations.sql +++ b/scheduler-server/sql/scheduler/applied_migrations.sql @@ -15,3 +15,4 @@ call migrations.mark_migration_applied('9'); call migrations.mark_migration_applied('10'); call migrations.mark_migration_applied('11'); call migrations.mark_migration_applied('12'); +call migrations.mark_migration_applied('13'); diff --git a/scheduler-server/sql/scheduler/init.sql b/scheduler-server/sql/scheduler/init.sql index 0bc728cb22..4eb1ac9463 100644 --- a/scheduler-server/sql/scheduler/init.sql +++ b/scheduler-server/sql/scheduler/init.sql @@ -7,17 +7,30 @@ begin; \ir tables/schema_migrations.sql \ir applied_migrations.sql - -- Scheduling intents. - \ir tables/scheduling_goal.sql + -- Scheduling Goals + \ir tables/scheduling_goal_metadata.sql + \ir tables/scheduling_goal_definition.sql + + -- Scheduling Conditions + \ir tables/scheduling_condition_metadata.sql + \ir tables/scheduling_condition_definition.sql + + -- Scheduling Specification \ir tables/scheduling_specification.sql \ir tables/scheduling_specification_goals.sql + \ir tables/scheduling_specification_conditions.sql + \ir tables/scheduling_model_specification_conditions.sql + \ir tables/scheduling_model_specification_goals.sql + + -- Scheduling Output \ir tables/scheduling_request.sql \ir tables/scheduling_goal_analysis.sql \ir tables/scheduling_goal_analysis_created_activities.sql \ir tables/scheduling_goal_analysis_satisfying_activities.sql - \ir tables/scheduling_condition.sql - \ir tables/scheduling_specification_conditions.sql -- Table-specific Metadata \ir tables/metadata/scheduling_goal_tags.sql + \ir tables/metadata/scheduling_goal_definition_tags.sql + \ir tables/metadata/scheduling_condition_tags.sql + \ir tables/metadata/scheduling_condition_definition_tags.sql end; From fa64348e99f67c4cf1cee429f361fe35b8eac93c Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 30 Jan 2024 13:56:45 -0800 Subject: [PATCH 143/159] Hasura Metadata --- .../tables/public_activity_directive.yaml | 2 +- .../scheduling_condition_definition_tags.yaml | 58 ++++++++++++ .../metadata/scheduling_condition_tags.yaml | 52 +++++++++++ .../scheduling_goal_definition_tags.yaml | 58 ++++++++++++ .../tables/metadata/scheduling_goal_tags.yaml | 6 +- ...ublic_scheduling_condition_definition.yaml | 80 ++++++++++++++++ .../public_scheduling_condition_metadata.yaml | 84 +++++++++++++++++ .../tables/public_scheduling_goal.yaml | 91 ------------------- .../public_scheduling_goal_analysis.yaml | 14 ++- .../public_scheduling_goal_definition.yaml | 80 ++++++++++++++++ .../public_scheduling_goal_metadata.yaml | 84 +++++++++++++++++ ...uling_model_specification_conditions.yaml} | 35 +++---- ..._scheduling_model_specification_goals.yaml | 65 +++++++++++++ ...c_scheduling_specification_conditions.yaml | 25 +++-- ...public_scheduling_specification_goals.yaml | 25 +++-- .../AerieScheduler/tables/tables.yaml | 11 ++- 16 files changed, 629 insertions(+), 141 deletions(-) create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_definition_tags.yaml create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_tags.yaml create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_definition_tags.yaml create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_definition.yaml create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_metadata.yaml delete mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml rename deployment/hasura/metadata/databases/AerieScheduler/tables/{public_scheduling_condition.yaml => public_scheduling_model_specification_conditions.yaml} (56%) create mode 100644 deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_model_specification_goals.yaml diff --git a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_activity_directive.yaml b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_activity_directive.yaml index 288a6cd887..c4e2bf5cf1 100644 --- a/deployment/hasura/metadata/databases/AerieMerlin/tables/public_activity_directive.yaml +++ b/deployment/hasura/metadata/databases/AerieMerlin/tables/public_activity_directive.yaml @@ -70,7 +70,7 @@ remote_relationships: source: AerieScheduler table: schema: public - name: scheduling_goal + name: scheduling_goal_metadata field_mapping: source_scheduling_goal_id: id select_permissions: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_definition_tags.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_definition_tags.yaml new file mode 100644 index 0000000000..142301412f --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_definition_tags.yaml @@ -0,0 +1,58 @@ +table: + name: scheduling_condition_definition_tags + schema: metadata +configuration: + custom_name: "scheduling_condition_definition_tags" +object_relationships: + - name: condition_definition + using: + foreign_key_constraint_on: + - condition_id + - condition_revision +remote_relationships: + - name: tag + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: metadata + name: tags + field_mapping: + tag_id: id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [condition_id, condition_revision, tag_id] + check: {} + - role: user + permission: + columns: [condition_id, condition_revision, tag_id] + check: {"condition_definition":{"_or":[ + {"author":{"_eq":"X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]}} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: {"condition_definition":{"_or":[ + {"author":{"_eq":"X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]}} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_tags.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_tags.yaml new file mode 100644 index 0000000000..eab069b8eb --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_condition_tags.yaml @@ -0,0 +1,52 @@ +table: + name: scheduling_condition_tags + schema: metadata +configuration: + custom_name: "scheduling_condition_tags" +object_relationships: + - name: condition_metadata + using: + foreign_key_constraint_on: condition_id +remote_relationships: + - name: tag + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: metadata + name: tags + field_mapping: + tag_id: id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [condition_id, tag_id] + check: {} + - role: user + permission: + columns: [condition_id, tag_id] + check: {"condition_metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: {"condition_metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_definition_tags.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_definition_tags.yaml new file mode 100644 index 0000000000..a75ff73f3e --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_definition_tags.yaml @@ -0,0 +1,58 @@ +table: + name: scheduling_goal_definition_tags + schema: metadata +configuration: + custom_name: "scheduling_goal_definition_tags" +object_relationships: + - name: goal_definition + using: + foreign_key_constraint_on: + - goal_id + - goal_revision +remote_relationships: + - name: tag + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: metadata + name: tags + field_mapping: + tag_id: id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [goal_id, goal_revision, tag_id] + check: {} + - role: user + permission: + columns: [goal_id, goal_revision, tag_id] + check: {"goal_definition":{"_or":[ + {"author":{"_eq":"X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]}} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: {"goal_definition":{"_or":[ + {"author":{"_eq":"X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]}} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_tags.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_tags.yaml index 98e03b3855..1a1c49c8c6 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_tags.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/metadata/scheduling_goal_tags.yaml @@ -4,7 +4,7 @@ table: configuration: custom_name: "scheduling_goal_tags" object_relationships: - - name: scheduling_goal + - name: goal_metadata using: foreign_key_constraint_on: goal_id remote_relationships: @@ -42,11 +42,11 @@ insert_permissions: - role: user permission: columns: [goal_id, tag_id] - check: {} + check: {"goal_metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}} delete_permissions: - role: aerie_admin permission: filter: {} - role: user permission: - filter: {} + filter: {"goal_metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_definition.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_definition.yaml new file mode 100644 index 0000000000..47fc8119c6 --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_definition.yaml @@ -0,0 +1,80 @@ +table: + name: scheduling_condition_definition + schema: public +object_relationships: + - name: metadata + using: + foreign_key_constraint_on: condition_id +array_relationships: + - name: tags + using: + foreign_key_constraint_on: + columns: + - condition_id + - condition_revision + table: + name: scheduling_condition_definition_tags + schema: metadata + - name: models_using + using: + foreign_key_constraint_on: + columns: + - condition_id + - condition_revision + table: + name: scheduling_model_specification_conditions + schema: public + - name: plans_using + using: + foreign_key_constraint_on: + columns: + - condition_id + - condition_revision + table: + name: scheduling_specification_conditions + schema: public +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' +# This should have filtering based on privacy, but cross-database permissions restrictions prevent that + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [condition_id, definition] + check: {} + set: + author: "x-hasura-user-id" + - role: user + permission: + columns: [condition_id, definition] + check: {"_or":[{"metadata":{"public":{"_eq":true}}},{"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]} + set: + author: "x-hasura-user-id" +update_permissions: + - role: aerie_admin + permission: + columns: [definition, author] + filter: {} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: + {"_or":[ + {"author": {"_eq": "X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_metadata.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_metadata.yaml new file mode 100644 index 0000000000..163317aa07 --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition_metadata.yaml @@ -0,0 +1,84 @@ +table: + name: scheduling_condition_metadata + schema: public +array_relationships: + - name: tags + using: + foreign_key_constraint_on: + column: condition_id + table: + name: scheduling_condition_tags + schema: metadata + - name: versions + using: + foreign_key_constraint_on: + column: condition_id + table: + name: scheduling_condition_definition + schema: public + - name: models_using + using: + foreign_key_constraint_on: + column: condition_id + table: + name: scheduling_model_specification_conditions + schema: public + - name: plans_using + using: + foreign_key_constraint_on: + column: condition_id + table: + name: scheduling_specification_conditions + schema: public +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' +# This should have filtering based on privacy, but cross-database permissions restrictions prevent that + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [name, description, public] + check: {} + set: + owner: "x-hasura-user-id" + updated_by: "x-hasura-user-id" + - role: user + permission: + columns: [name, description, public] + check: {} + set: + owner: "x-hasura-user-id" + updated_by: "x-hasura-user-id" +update_permissions: + - role: aerie_admin + permission: + columns: [name, description, public, owner] + filter: {} + set: + updated_by: "x-hasura-user-id" + - role: user + permission: + columns: [name, description, public, owner] + filter: { "owner": { "_eq": "X-Hasura-User-Id" } } + set: + updated_by: "x-hasura-user-id" +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: { "owner": { "_eq": "X-Hasura-User-Id" } } diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml deleted file mode 100644 index cf7720e5a0..0000000000 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal.yaml +++ /dev/null @@ -1,91 +0,0 @@ -table: - name: scheduling_goal - schema: public -object_relationships: - - name: scheduling_specification_goal - using: - foreign_key_constraint_on: - column: goal_id - table: - name: scheduling_specification_goals - schema: public -array_relationships: -- name: analyses - using: - foreign_key_constraint_on: - column: goal_id - table: - name: scheduling_goal_analysis - schema: public -- name: tags - using: - manual_configuration: - remote_table: - name: scheduling_goal_tags - schema: metadata - insertion_order: null - column_mapping: - id: goal_id -remote_relationships: -- name: model - definition: - to_source: - relationship_type: object - source: AerieMerlin - table: - schema: public - name: mission_model - field_mapping: - model_id: id -select_permissions: - - role: aerie_admin - permission: - columns: '*' - filter: {} - allow_aggregations: true - - role: user - permission: - columns: '*' - filter: {} - allow_aggregations: true - - role: viewer - permission: - columns: '*' - filter: {} - allow_aggregations: true -# TODO: Modify these once we have a solution for cross-db auth (These permissions should be based on plan ownership/collaboratorship) -insert_permissions: - - role: aerie_admin - permission: - columns: [name, definition, model_id, description] - check: {} - set: - author: "x-hasura-user-id" - last_modified_by: "x-hasura-user-id" - - role: user - permission: - columns: [name, definition, model_id, description] - check: {} - set: - author: "x-hasura-user-id" - last_modified_by: "x-hasura-user-id" -update_permissions: - - role: aerie_admin - permission: - columns: [name, definition, model_id, description] - filter: {} - set: - last_modified_by: "x-hasura-user-id" - - role: user - permission: - columns: [name, definition, description] - filter: {} - set: - last_modified_by: "x-hasura-user-id" -delete_permissions: - - role: aerie_admin - permission: - filter: {} - - role: user - permission: - filter: {} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml index 590ff89be1..c22c697185 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_analysis.yaml @@ -11,9 +11,19 @@ object_relationships: insertion_order: null column_mapping: analysis_id: analysis_id -- name: goal +- name: goal_metadata using: - foreign_key_constraint_on: goal_id + manual_configuration: + column_mapping: + goal_id: id + remote_table: + name: scheduling_goal_metadata + schema: public +- name: goal_definition + using: + foreign_key_constraint_on: + - goal_id + - goal_revision array_relationships: - name: satisfying_activities using: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml new file mode 100644 index 0000000000..9f57b83529 --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml @@ -0,0 +1,80 @@ +table: + name: scheduling_goal_definition + schema: public +object_relationships: + - name: metadata + using: + foreign_key_constraint_on: goal_id +array_relationships: + - name: tags + using: + foreign_key_constraint_on: + columns: + - goal_id + - goal_revision + table: + name: scheduling_goal_definition_tags + schema: metadata + - name: models_using + using: + foreign_key_constraint_on: + columns: + - goal_id + - goal_revision + table: + name: scheduling_model_specification_goals + schema: public + - name: plans_using + using: + foreign_key_constraint_on: + columns: + - goal_id + - goal_revision + table: + name: scheduling_specification_goals + schema: public +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' +# This should have filtering based on privacy, but cross-database permissions restrictions prevent that + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [goal_id, definition] + check: {} + set: + author: "x-hasura-user-id" + - role: user + permission: + columns: [goal_id, definition] + check: {"_or":[{"metadata":{"public":{"_eq":true}}},{"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]} + set: + author: "x-hasura-user-id" +update_permissions: + - role: aerie_admin + permission: + columns: [definition, author] + filter: {} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: + {"_or":[ + {"author": {"_eq": "X-Hasura-User-Id"}}, + {"metadata":{"owner":{"_eq":"X-Hasura-User-Id"}}}]} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml new file mode 100644 index 0000000000..ddfe1b6a0b --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml @@ -0,0 +1,84 @@ +table: + name: scheduling_goal_metadata + schema: public +array_relationships: + - name: tags + using: + foreign_key_constraint_on: + column: goal_id + table: + name: scheduling_goal_tags + schema: metadata + - name: versions + using: + foreign_key_constraint_on: + column: goal_id + table: + name: scheduling_goal_definition + schema: public + - name: models_using + using: + foreign_key_constraint_on: + column: goal_id + table: + name: scheduling_model_specification_goals + schema: public + - name: plans_using + using: + foreign_key_constraint_on: + column: goal_id + table: + name: scheduling_specification_goals + schema: public +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' +# This should have filtering based on privacy, but cross-database permissions restrictions prevent that + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +insert_permissions: + - role: aerie_admin + permission: + columns: [name, description, public] + check: {} + set: + owner: "x-hasura-user-id" + updated_by: "x-hasura-user-id" + - role: user + permission: + columns: [name, description, public] + check: {} + set: + owner: "x-hasura-user-id" + updated_by: "x-hasura-user-id" +update_permissions: + - role: aerie_admin + permission: + columns: [name, description, public, owner] + filter: {} + set: + updated_by: "x-hasura-user-id" + - role: user + permission: + columns: [name, description, public, owner] + filter: { "owner": { "_eq": "X-Hasura-User-Id" } } + set: + updated_by: "x-hasura-user-id" +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: { "owner": { "_eq": "X-Hasura-User-Id" } } diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_model_specification_conditions.yaml similarity index 56% rename from deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition.yaml rename to deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_model_specification_conditions.yaml index fdb37c7dce..034c84ef91 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_condition.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_model_specification_conditions.yaml @@ -1,14 +1,15 @@ table: - name: scheduling_condition + name: scheduling_model_specification_conditions schema: public -array_relationships: - - name: scheduling_specification_conditions +object_relationships: + - name: condition_metadata + using: + foreign_key_constraint_on: condition_id + - name: condition_definition using: foreign_key_constraint_on: - column: condition_id - table: - name: scheduling_specification_conditions - schema: public + - condition_id + - condition_revision remote_relationships: - name: model definition: @@ -36,35 +37,25 @@ select_permissions: columns: '*' filter: {} allow_aggregations: true -# TODO: Modify these once we have a solution for cross-db auth (These permissions should be based on plan ownership/collaboratorship) +# TODO: Modify these once we have a solution for cross-db auth (These permissions should be based on model ownership) insert_permissions: - role: aerie_admin permission: - columns: [name, definition, model_id, description] + columns: [model_id, condition_id, condition_revision] check: {} - set: - author: "x-hasura-user-id" - last_modified_by: "x-hasura-user-id" - role: user permission: - columns: [name, definition, model_id, description] + columns: [model_id, condition_id, condition_revision] check: {} - set: - author: "x-hasura-user-id" - last_modified_by: "x-hasura-user-id" update_permissions: - role: aerie_admin permission: - columns: [name, definition, description, model_id] + columns: [condition_revision] filter: {} - set: - last_modified_by: "x-hasura-user-id" - role: user permission: - columns: [name, definition, description] + columns: [condition_revision] filter: {} - set: - last_modified_by: "x-hasura-user-id" delete_permissions: - role: aerie_admin permission: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_model_specification_goals.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_model_specification_goals.yaml new file mode 100644 index 0000000000..3e80cf581e --- /dev/null +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_model_specification_goals.yaml @@ -0,0 +1,65 @@ +table: + name: scheduling_model_specification_goals + schema: public +object_relationships: + - name: goal_metadata + using: + foreign_key_constraint_on: goal_id + - name: goal_definition + using: + foreign_key_constraint_on: + - goal_id + - goal_revision +remote_relationships: +- name: model + definition: + to_source: + relationship_type: object + source: AerieMerlin + table: + schema: public + name: mission_model + field_mapping: + model_id: id +select_permissions: + - role: aerie_admin + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: user + permission: + columns: '*' + filter: {} + allow_aggregations: true + - role: viewer + permission: + columns: '*' + filter: {} + allow_aggregations: true +# TODO: Modify these once we have a solution for cross-db auth (These permissions should be based on model ownership) +insert_permissions: + - role: aerie_admin + permission: + columns: [model_id, goal_id, goal_revision] + check: {} + - role: user + permission: + columns: [model_id, goal_id, goal_revision] + check: {} +update_permissions: + - role: aerie_admin + permission: + columns: [goal_revision] + filter: {} + - role: user + permission: + columns: [goal_revision] + filter: {} +delete_permissions: + - role: aerie_admin + permission: + filter: {} + - role: user + permission: + filter: {} diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_conditions.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_conditions.yaml index 8f4a672ae2..2f60212f94 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_conditions.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_conditions.yaml @@ -2,12 +2,17 @@ table: name: scheduling_specification_conditions schema: public object_relationships: -- name: condition - using: - foreign_key_constraint_on: condition_id -- name: specification - using: - foreign_key_constraint_on: specification_id + - name: condition_metadata + using: + foreign_key_constraint_on: condition_id + - name: condition_definition + using: + foreign_key_constraint_on: + - condition_id + - condition_revision + - name: specification + using: + foreign_key_constraint_on: specification_id select_permissions: - role: aerie_admin permission: @@ -28,20 +33,20 @@ select_permissions: insert_permissions: - role: aerie_admin permission: - columns: [specification_id, condition_id, enabled] + columns: [specification_id, condition_id, condition_revision, enabled] check: {} - role: user permission: - columns: [specification_id, condition_id, enabled] + columns: [specification_id, condition_id, condition_revision, enabled] check: {} update_permissions: - role: aerie_admin permission: - columns: [specification_id, condition_id, enabled] + columns: [condition_revision, enabled] filter: {} - role: user permission: - columns: [enabled] + columns: [condition_revision, enabled] filter: {} delete_permissions: - role: aerie_admin diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml index d82486e5c6..c71679a788 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml @@ -2,12 +2,17 @@ table: name: scheduling_specification_goals schema: public object_relationships: -- name: goal - using: - foreign_key_constraint_on: goal_id -- name: specification - using: - foreign_key_constraint_on: specification_id + - name: goal_metadata + using: + foreign_key_constraint_on: goal_id + - name: goal_definition + using: + foreign_key_constraint_on: + - goal_id + - goal_revision + - name: specification + using: + foreign_key_constraint_on: specification_id select_permissions: - role: aerie_admin permission: @@ -28,20 +33,20 @@ select_permissions: insert_permissions: - role: aerie_admin permission: - columns: [specification_id, goal_id, priority, enabled, simulate_after] + columns: [specification_id, goal_id, goal_revision, priority, enabled] check: {} - role: user permission: - columns: [specification_id, goal_id, priority, enabled, simulate_after] + columns: [specification_id, goal_id, goal_revision, priority, enabled] check: {} update_permissions: - role: aerie_admin permission: - columns: [specification_id, goal_id, priority, enabled, simulate_after] + columns: [goal_revision, priority, enabled] filter: {} - role: user permission: - columns: [priority, enabled, simulate_after] + columns: [goal_revision, priority, enabled] filter: {} delete_permissions: - role: aerie_admin diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/tables.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/tables.yaml index b6303b348d..ec6669c746 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/tables.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/tables.yaml @@ -1,11 +1,18 @@ -- "!include public_scheduling_condition.yaml" -- "!include public_scheduling_goal.yaml" +- "!include public_scheduling_condition_definition.yaml" +- "!include public_scheduling_condition_metadata.yaml" +- "!include public_scheduling_goal_definition.yaml" +- "!include public_scheduling_goal_metadata.yaml" - "!include public_scheduling_goal_analysis.yaml" - "!include public_scheduling_goal_analysis_created_activities.yaml" - "!include public_scheduling_goal_analysis_satisfying_activities.yaml" +- "!include public_scheduling_model_specification_goals.yaml" +- "!include public_scheduling_model_specification_conditions.yaml" - "!include public_scheduling_request.yaml" - "!include public_scheduling_specification.yaml" - "!include public_scheduling_specification_goals.yaml" - "!include public_scheduling_specification_conditions.yaml" # Metadata +- "!include metadata/scheduling_condition_tags.yaml" +- "!include metadata/scheduling_condition_definition_tags.yaml" - "!include metadata/scheduling_goal_tags.yaml" +- "!include metadata/scheduling_goal_definition_tags.yaml" From 65bb1eece6084e68adf7048a80594a32611fd86e Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 12:32:30 -0800 Subject: [PATCH 144/159] Update DBTests --- .../database/SchedulerDatabaseTests.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/SchedulerDatabaseTests.java b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/SchedulerDatabaseTests.java index b2a3a04256..0368bcbe6e 100644 --- a/db-tests/src/test/java/gov/nasa/jpl/aerie/database/SchedulerDatabaseTests.java +++ b/db-tests/src/test/java/gov/nasa/jpl/aerie/database/SchedulerDatabaseTests.java @@ -49,16 +49,21 @@ insert into scheduling_specification( int insertGoal() throws SQLException { try (final var statement = connection.createStatement()) { final var res = statement.executeQuery(""" - insert into scheduling_goal( - revision, name, definition, model_id, description, author, last_modified_by, created_date, modified_date - ) values (0, 'goal', 'does thing', 0, 'hey there', 'its me', 'also me', now(), now()) returning id; + with metadata(id, owner) as ( + insert into scheduling_goal_metadata(name, description, owner, updated_by) + values ('test goal', 'no-op', 'scheduler db tests', 'scheduler db tests') + returning id, owner + ) + insert into scheduling_goal_definition(goal_id, definition, author) + select m.id, 'nothing', m.owner + from metadata m + returning goal_id as id; """); res.next(); return res.getInt("id"); } } - @Nested class TestSpecificationAndTemplateGoalTriggers { int[] specificationIds; @@ -73,7 +78,8 @@ void beforeEach() throws SQLException { @AfterEach void afterEach() throws SQLException { helper.clearTable("scheduling_specification"); - helper.clearTable("scheduling_goal"); + helper.clearTable("scheduling_goal_metadata"); + helper.clearTable("scheduling_goal_definition"); helper.clearTable("scheduling_specification_goals"); } @@ -143,12 +149,13 @@ private int getSpecificationRevision(int specificationId) throws SQLException { } @Test - void shouldIncrementSpecRevisionAfterModifyingGoal() throws SQLException { + void shouldIncrementSpecRevisionAfterModifyingGoalSpec() throws SQLException { insertGoalPriorities(0, new int[] {0, 1, 2, 3, 4}, new int[]{0, 1, 2, 3, 4}); final var revisionBefore = getSpecificationRevision(specificationIds[0]); connection.createStatement().executeUpdate(""" - update scheduling_goal - set name = 'other name' where id = %d; + update scheduling_specification_goals + set goal_revision = 0 + where goal_id = %d; """.formatted(goalIds[3])); final var revisionAfter = getSpecificationRevision(specificationIds[0]); assertEquals(revisionBefore + 1, revisionAfter); From 2f136be8d03e9758a4b1a1b43047bb683c00c79f Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 12:41:02 -0800 Subject: [PATCH 145/159] Remove `InMemoryStore` option - The Scheduler uses a `Mock` setup for tests. Additionally, this code is completely unsupported --- .../scheduler/server/SchedulerAppDriver.java | 9 ----- .../server/config/InMemoryStore.java | 3 -- .../aerie/scheduler/server/config/Store.java | 2 +- .../mocks/InMemoryResultsCellRepository.java | 33 ------------------- .../InMemorySpecificationRepository.java | 22 ------------- 5 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/InMemoryStore.java delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemoryResultsCellRepository.java delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemorySpecificationRepository.java diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java index 3f87b46791..3ebba98005 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java @@ -6,12 +6,9 @@ import gov.nasa.jpl.aerie.permissions.PermissionsService; import gov.nasa.jpl.aerie.permissions.gql.GraphQLPermissionsService; import gov.nasa.jpl.aerie.scheduler.server.config.AppConfiguration; -import gov.nasa.jpl.aerie.scheduler.server.config.InMemoryStore; import gov.nasa.jpl.aerie.scheduler.server.config.PostgresStore; import gov.nasa.jpl.aerie.scheduler.server.config.Store; import gov.nasa.jpl.aerie.scheduler.server.http.SchedulerBindings; -import gov.nasa.jpl.aerie.scheduler.server.mocks.InMemoryResultsCellRepository; -import gov.nasa.jpl.aerie.scheduler.server.mocks.InMemorySpecificationRepository; import gov.nasa.jpl.aerie.scheduler.server.remotes.ResultsCellRepository; import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PostgresResultsCellRepository; @@ -115,12 +112,6 @@ private static Stores loadStores( return new Stores( new PostgresSpecificationRepository(hikariDataSource), new PostgresResultsCellRepository(hikariDataSource)); - } else if (store instanceof InMemoryStore) { - final var inMemorySchedulerRepository = new InMemorySpecificationRepository(); - return new Stores( - inMemorySchedulerRepository, - new InMemoryResultsCellRepository(inMemorySchedulerRepository)); - } else { throw new UnexpectedSubtypeError(Store.class, store); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/InMemoryStore.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/InMemoryStore.java deleted file mode 100644 index f7d5f5fb01..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/InMemoryStore.java +++ /dev/null @@ -1,3 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.config; - -public record InMemoryStore() implements Store { } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/Store.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/Store.java index 5ecd9d09df..eac491b853 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/Store.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/config/Store.java @@ -1,3 +1,3 @@ package gov.nasa.jpl.aerie.scheduler.server.config; -public sealed interface Store permits PostgresStore, InMemoryStore { } +public sealed interface Store permits PostgresStore { } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemoryResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemoryResultsCellRepository.java deleted file mode 100644 index a096305e84..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemoryResultsCellRepository.java +++ /dev/null @@ -1,33 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.mocks; - -import java.util.Optional; -import gov.nasa.jpl.aerie.scheduler.server.ResultsProtocol; -import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; -import gov.nasa.jpl.aerie.scheduler.server.remotes.ResultsCellRepository; -import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; - -public record InMemoryResultsCellRepository(SpecificationRepository specificationRepository) implements ResultsCellRepository { - @Override - public ResultsProtocol.OwnerRole allocate(final SpecificationId specificationId, final String requestedBy) - { - throw new UnsupportedOperationException(); // TODO stubbed method must be implemented - } - - @Override - public Optional claim(SpecificationId specificationId) - { - throw new UnsupportedOperationException(); // TODO stubbed method must be implemented - } - - @Override - public Optional lookup(final SpecificationId specificationId) - { - throw new UnsupportedOperationException(); // TODO stubbed method must be implemented - } - - @Override - public void deallocate(final ResultsProtocol.OwnerRole resultsCell) - { - throw new UnsupportedOperationException(); // TODO stubbed method must be implemented - } -} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemorySpecificationRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemorySpecificationRepository.java deleted file mode 100644 index 87fd35a580..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/mocks/InMemorySpecificationRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.mocks; - -import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; -import gov.nasa.jpl.aerie.scheduler.server.models.Specification; -import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; -import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; -import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; - -public final class InMemorySpecificationRepository implements SpecificationRepository { - @Override - public Specification getSpecification(final SpecificationId specificationId) throws NoSuchSpecificationException - { - throw new UnsupportedOperationException(); // TODO stubbed method must be implemented - } - - @Override - public RevisionData getSpecificationRevisionData(final SpecificationId specificationId) - throws NoSuchSpecificationException - { - throw new UnsupportedOperationException(); // TODO stubbed method must be implemented - } -} From f7cac71a0f2699addb00af5b803e3ca409db5aa3 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 12:46:46 -0800 Subject: [PATCH 146/159] Simplify Scheduler Services - `MockSpecificationService` was really a `MockSpecificationRepository`, as indicated by its private field that was a repository of Specifications - The one remaining implementation of `SpecificationService` deferred exclusively to the implementation of its repository, so `SpecificationService` was flattened - The above applies for `SchedulerService` as well --- .../scheduler/server/SchedulerAppDriver.java | 8 +++---- .../services/CachedSchedulerService.java | 24 ------------------- .../services/LocalSpecificationService.java | 23 ------------------ .../server/services/SchedulerService.java | 17 ++++++++++--- .../server/services/SpecificationService.java | 17 ++++++++++--- .../worker/SchedulerWorkerAppDriver.java | 4 ++-- ....java => MockSpecificationRepository.java} | 17 ++++++------- .../services/SchedulingIntegrationTests.java | 3 ++- 8 files changed, 45 insertions(+), 68 deletions(-) delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/CachedSchedulerService.java delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/LocalSpecificationService.java rename scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/{MockSpecificationService.java => MockSpecificationRepository.java} (50%) diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java index 3ebba98005..6fa88c4f91 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/SchedulerAppDriver.java @@ -13,11 +13,11 @@ import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PostgresResultsCellRepository; import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PostgresSpecificationRepository; -import gov.nasa.jpl.aerie.scheduler.server.services.CachedSchedulerService; import gov.nasa.jpl.aerie.scheduler.server.services.GenerateSchedulingLibAction; import gov.nasa.jpl.aerie.scheduler.server.services.GraphQLMerlinService; -import gov.nasa.jpl.aerie.scheduler.server.services.LocalSpecificationService; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleAction; +import gov.nasa.jpl.aerie.scheduler.server.services.SchedulerService; +import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; import gov.nasa.jpl.aerie.scheduler.server.services.UnexpectedSubtypeError; import io.javalin.Javalin; import org.eclipse.jetty.server.Connector; @@ -54,8 +54,8 @@ public static void main(final String[] args) { final var stores = loadStores(config); //create objects in each service abstraction layer (mirroring MerlinApp) - final var specificationService = new LocalSpecificationService(stores.specifications()); - final var schedulerService = new CachedSchedulerService(stores.results()); + final var specificationService = new SpecificationService(stores.specifications()); + final var schedulerService = new SchedulerService(stores.results()); final var scheduleAction = new ScheduleAction(specificationService, schedulerService); final var generateSchedulingLibAction = new GenerateSchedulingLibAction(merlinService); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/CachedSchedulerService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/CachedSchedulerService.java deleted file mode 100644 index 3910ca94af..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/CachedSchedulerService.java +++ /dev/null @@ -1,24 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.services; - -import gov.nasa.jpl.aerie.scheduler.server.ResultsProtocol; -import gov.nasa.jpl.aerie.scheduler.server.remotes.ResultsCellRepository; - -public record CachedSchedulerService( - ResultsCellRepository store -) implements SchedulerService { - - @Override - public ResultsProtocol.State getScheduleResults(final ScheduleRequest request, final String requestedBy) { - final var specificationId = request.specificationId(); - final var cell$ = this.store.lookup(specificationId); - if (cell$.isPresent()) { - return cell$.get().get(); - } else { - // Allocate a fresh cell. - final var cell = this.store.allocate(specificationId, requestedBy); - - // Return the current value of the reader; if it's incomplete, the caller can check it again later. - return cell.get(); - } - } -} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/LocalSpecificationService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/LocalSpecificationService.java deleted file mode 100644 index 26bd5c1cc8..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/LocalSpecificationService.java +++ /dev/null @@ -1,23 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.services; - -import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; -import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; -import gov.nasa.jpl.aerie.scheduler.server.models.Specification; -import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; -import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; - -public record LocalSpecificationService(SpecificationRepository specificationRepository) implements SpecificationService { - @Override - public Specification getSpecification(final SpecificationId specificationId) - throws NoSuchSpecificationException, SpecificationLoadException - { - return specificationRepository.getSpecification(specificationId); - } - - @Override - public RevisionData getSpecificationRevisionData(final SpecificationId specificationId) - throws NoSuchSpecificationException - { - return specificationRepository.getSpecificationRevisionData(specificationId); - } -} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerService.java index 6a40223998..7fb813f769 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerService.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.services; import gov.nasa.jpl.aerie.scheduler.server.ResultsProtocol; +import gov.nasa.jpl.aerie.scheduler.server.remotes.ResultsCellRepository; /** * services operations at the intersection of plans and scheduling goals; eg scheduling instances to satisfy goals @@ -8,13 +9,23 @@ * provides both mutation operations to actively improve a plan's goal satisfaction score (eg by inserting activity * instances into the plan) and passive queries to ascertain the current satisfaction level of a plan */ -//TODO: add separate scheduling goal and prioritization management service -public interface SchedulerService { +public record SchedulerService(ResultsCellRepository store) { /** * schedules activity instances into the target plan in order to further satisfy the associated scheduling goals * * @param request details of the scheduling request, including the target plan and goals to operate on * @return summary of the scheduling run, including goal satisfaction metrics and changes made */ - ResultsProtocol.State getScheduleResults(final ScheduleRequest request, final String requestedBy); + public ResultsProtocol.State getScheduleResults(final ScheduleRequest request, final String requestedBy) { + final var cell$ = this.store.lookup(request); + if (cell$.isPresent()) { + return cell$.get().get(); + } else { + // Allocate a fresh cell. + final var cell = this.store.allocate(request.specificationId(), requestedBy); + + // Return the current value of the reader; if it's incomplete, the caller can check it again later. + return cell.get(); + } + } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java index e1c629d700..a4716ca025 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SpecificationService.java @@ -4,9 +4,20 @@ import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; +import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; +import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.SpecificationRevisionData; -public interface SpecificationService { +public record SpecificationService(SpecificationRepository specificationRepository) { // Queries - Specification getSpecification(SpecificationId specificationId) throws NoSuchSpecificationException, SpecificationLoadException; - RevisionData getSpecificationRevisionData(SpecificationId specificationId) throws NoSuchSpecificationException; + public Specification getSpecification(final SpecificationId specificationId) + throws NoSuchSpecificationException, SpecificationLoadException + { + return specificationRepository.getSpecification(specificationId); + } + + public SpecificationRevisionData getSpecificationRevisionData(final SpecificationId specificationId) + throws NoSuchSpecificationException + { + return specificationRepository.getSpecificationRevisionData(specificationId); + } } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index 968be25e29..79361bd1ca 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -18,8 +18,8 @@ import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PostgresSpecificationRepository; import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.SpecificationRevisionData; import gov.nasa.jpl.aerie.scheduler.server.services.GraphQLMerlinService; -import gov.nasa.jpl.aerie.scheduler.server.services.LocalSpecificationService; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; +import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; import gov.nasa.jpl.aerie.scheduler.server.services.UnexpectedSubtypeError; import gov.nasa.jpl.aerie.scheduler.worker.postgres.PostgresSchedulingRequestNotificationPayload; import gov.nasa.jpl.aerie.scheduler.worker.services.SchedulingDSLCompilationService; @@ -63,7 +63,7 @@ public static void main(String[] args) throws Exception { new PostgresSpecificationRepository(hikariDataSource), new PostgresResultsCellRepository(hikariDataSource)); - final var specificationService = new LocalSpecificationService(stores.specifications()); + final var specificationService = new SpecificationService(stores.specifications()); final var scheduleAgent = new SynchronousSchedulerAgent(specificationService, merlinService, config.merlinFileStore(), diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationService.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationRepository.java similarity index 50% rename from scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationService.java rename to scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationRepository.java index b1a31369bc..63ca06938c 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationService.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockSpecificationRepository.java @@ -3,32 +3,33 @@ import java.util.Map; import java.util.Optional; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; -import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; -import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; -import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; +import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; +import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.SpecificationRevisionData; -class MockSpecificationService implements SpecificationService +class MockSpecificationRepository implements SpecificationRepository { Map specifications; - MockSpecificationService(final Map specifications) { + MockSpecificationRepository(final Map specifications) { this.specifications = specifications; } @Override public Specification getSpecification(final SpecificationId specificationId) - throws NoSuchSpecificationException, SpecificationLoadException + throws NoSuchSpecificationException { return Optional.ofNullable(specifications.get(specificationId)) .orElseThrow(() -> new NoSuchSpecificationException(specificationId)); } @Override - public RevisionData getSpecificationRevisionData(final SpecificationId specificationId) + public SpecificationRevisionData getSpecificationRevisionData(final SpecificationId specificationId) throws NoSuchSpecificationException { - return $ -> RevisionData.MatchResult.success(); + if(!specifications.containsKey(specificationId)) throw new NoSuchSpecificationException(specificationId); + final var spec = specifications.get(specificationId); + return new SpecificationRevisionData(spec.specificationRevision(), spec.planRevision()); } } diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index fd5731ff8f..9ff6bb6ed1 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -55,6 +55,7 @@ import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; import gov.nasa.jpl.aerie.scheduler.model.Plan; +import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -2010,7 +2011,7 @@ private SchedulingRunResults runScheduler( for (final var goal : goals) { goalsByPriority.add(new GoalRecord(goal.goalId(), new GoalSource(goal.definition()), goal.enabled(), goal.simulateAfter())); } - final var specificationService = new MockSpecificationService(Map.of(new SpecificationId(1L), new Specification( + final var specificationService = new SpecificationService(new MockSpecificationRepository(Map.of(new SpecificationId(1L), new Specification( planId, 1L, goalsByPriority, From d302c560b99d132cf9f6ce8aaa2ea04413fd100c Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 12:58:04 -0800 Subject: [PATCH 147/159] Update Postgres Actions - Make `ClaimRequestAction` return the request instead of requiring a separate DB call. - Insert additional request information in `CreateRequestAction` - Make `GetSpecificationConditionsAction` return `SchedulingConditionRecord`s instead of forcing the caller to do post-processing - Make `GetSpecificationGoalsAction` return `GoalRecord`s instead of forcing the caller to do post-processing - Rename `GlobalSchedulingConditionRecord` to `SchedulingConditionRecord` - Update GoalId, GoalRecord, RequestRecord, Specification, and SchedulingConditionRecord types - Update Satisfaction actions to reflect changes to GoalId --- .../GlobalSchedulingConditionRecord.java | 6 - .../aerie/scheduler/server/models/GoalId.java | 2 +- .../scheduler/server/models/GoalRecord.java | 6 +- .../server/models/SchedulingConditionId.java | 4 + .../models/SchedulingConditionRecord.java | 8 + ...ce.java => SchedulingConditionSource.java} | 2 +- .../server/models/Specification.java | 6 +- .../remotes/postgres/ClaimRequestAction.java | 62 +++- .../remotes/postgres/CreateRequestAction.java | 29 +- .../postgres/GetCreatedActivitiesAction.java | 3 +- .../postgres/GetGoalSatisfactionAction.java | 3 +- .../remotes/postgres/GetRequestAction.java | 16 +- .../GetSatisfyingActivitiesAction.java | 3 +- .../GetSpecificationConditionsAction.java | 33 ++- .../postgres/GetSpecificationGoalsAction.java | 35 ++- .../InsertCreatedActivitiesAction.java | 11 +- .../InsertGoalSatisfactionAction.java | 10 +- .../InsertSatisfyingActivitiesAction.java | 11 +- .../remotes/postgres/PostgresGoalRecord.java | 10 - .../PostgresResultsCellRepository.java | 8 +- .../PostgresSchedulingConditionRecord.java | 9 - .../PostgresSpecificationRepository.java | 38 +-- .../remotes/postgres/RequestRecord.java | 1 + .../remotes/postgres/SpecificationRecord.java | 2 +- .../services/SynchronousSchedulerAgent.java | 22 +- .../services/SchedulingIntegrationTests.java | 279 +++++++++--------- 26 files changed, 329 insertions(+), 290 deletions(-) delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GlobalSchedulingConditionRecord.java create mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionId.java create mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionRecord.java rename scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/{GlobalSchedulingConditionSource.java => SchedulingConditionSource.java} (69%) delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresGoalRecord.java delete mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSchedulingConditionRecord.java diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GlobalSchedulingConditionRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GlobalSchedulingConditionRecord.java deleted file mode 100644 index 83d86e6c83..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GlobalSchedulingConditionRecord.java +++ /dev/null @@ -1,6 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.models; - -public record GlobalSchedulingConditionRecord( - GlobalSchedulingConditionSource source, - boolean enabled -) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalId.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalId.java index ab6e9822e2..70aec3c026 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalId.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalId.java @@ -1,3 +1,3 @@ package gov.nasa.jpl.aerie.scheduler.server.models; -public record GoalId(long id) { } +public record GoalId(long id, long revision) { } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalRecord.java index 966839ae23..620745011e 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalRecord.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GoalRecord.java @@ -1,3 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.models; -public record GoalRecord(GoalId id, GoalSource definition, boolean enabled, boolean simulateAfter) {} +public record GoalRecord( + GoalId id, + String name, + GoalSource definition, + boolean simulateAfter) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionId.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionId.java new file mode 100644 index 0000000000..fba4464fb6 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionId.java @@ -0,0 +1,4 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +public record SchedulingConditionId(long id) { +} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionRecord.java new file mode 100644 index 0000000000..1eecda1db0 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionRecord.java @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +public record SchedulingConditionRecord( + SchedulingConditionId id, + long revision, + String name, + SchedulingConditionSource source +) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GlobalSchedulingConditionSource.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionSource.java similarity index 69% rename from scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GlobalSchedulingConditionSource.java rename to scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionSource.java index f265b28aa8..61424ad9d9 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/GlobalSchedulingConditionSource.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingConditionSource.java @@ -3,4 +3,4 @@ /** * @param source The typescript code describing this global scheduling condition. */ -public record GlobalSchedulingConditionSource(String source) {} +public record SchedulingConditionSource(String source) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/Specification.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/Specification.java index 049881b879..8f92a55986 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/Specification.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/Specification.java @@ -6,12 +6,14 @@ import java.util.Map; public record Specification( + SpecificationId specificationId, + long specificationRevision, PlanId planId, long planRevision, - List goalsByPriority, Timestamp horizonStartTimestamp, Timestamp horizonEndTimestamp, Map simulationArguments, boolean analysisOnly, - List globalSchedulingConditions + List goalsByPriority, + List schedulingConditions ) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/ClaimRequestAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/ClaimRequestAction.java index 683276baac..9b1b75bf42 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/ClaimRequestAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/ClaimRequestAction.java @@ -8,9 +8,16 @@ /*package local*/ public class ClaimRequestAction implements AutoCloseable { private static final @Language("SQL") String sql = """ update scheduling_request - set - status = 'incomplete' - where (specification_id = ? and status = 'pending' and not canceled); + set status = 'incomplete' + where (analysis_id = ? and status = 'pending' and not canceled) + returning + specification_id, + specification_revision, + plan_revision, + status, + reason, + canceled, + dataset_id; """; private final PreparedStatement statement; @@ -19,16 +26,51 @@ public ClaimRequestAction(final Connection connection) throws SQLException { this.statement = connection.prepareStatement(sql); } - public void apply(final long specificationId) throws SQLException, UnclaimableRequestException { - this.statement.setLong(1, specificationId); + public RequestRecord apply(final long analysisId) throws SQLException, UnclaimableRequestException { + this.statement.setLong(1, analysisId); - final var count = this.statement.executeUpdate(); - if (count < 1) { - throw new UnclaimableRequestException(specificationId); - } else if (count > 1) { + final var resultSet = this.statement.executeQuery(); + if (!resultSet.next()) { + throw new UnclaimableRequestException(analysisId); + } + + final var specificationId = resultSet.getLong("specification_id"); + final var specificationRevision = resultSet.getLong("specification_revision"); + final var planRevision = resultSet.getLong("plan_revision"); + + final RequestRecord.Status status; + try { + status = RequestRecord.Status.fromString(resultSet.getString("status")); + } catch (final RequestRecord.Status.InvalidRequestStatusException ex) { + throw new Error( + String.format( + "Scheduling request for specification with ID %d and revision %d has invalid state %s", + specificationId, + specificationRevision, + ex.invalidStatus)); + } + + final var failureReason$ = PreparedStatements.getFailureReason(resultSet, "reason"); + final var canceled = resultSet.getBoolean("canceled"); + final var datasetId = PreparedStatements.getDatasetId(resultSet, "dataset_id"); + + final var request = new RequestRecord( + specificationId, + analysisId, + specificationRevision, + planRevision, + status, + failureReason$, + canceled, + datasetId + ); + + if (resultSet.next()) { throw new SQLException( - String.format("Claiming a scheduling request for specification id %s returned more than one result row.", specificationId)); + String.format("Claiming a scheduling request with analysis id %s returned more than one result row.", analysisId)); } + + return request; } @Override diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/CreateRequestAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/CreateRequestAction.java index 4206c913fc..c95320ec4d 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/CreateRequestAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/CreateRequestAction.java @@ -5,13 +5,30 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import static gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PostgresParsers.simulationArgumentsP; import static gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.PreparedStatements.getDatasetId; /*package-local*/ final class CreateRequestAction implements AutoCloseable { + private static final DateTimeFormatter TIMESTAMP_FORMAT = + new DateTimeFormatterBuilder() + .appendPattern("uuuu-MM-dd HH:mm:ss") + .appendFraction(ChronoField.MICRO_OF_SECOND, 0, 6, true) + .appendOffset("+HH:mm:ss", "+00") + .toFormatter(); private final @Language("SQL") String sql = """ - insert into scheduling_request (specification_id, specification_revision, requested_by) - values (?, ?, ?) + insert into scheduling_request ( + specification_id, + specification_revision, + plan_revision, + horizon_start, + horizon_end, + simulation_arguments, + requested_by) + values (?, ?, ?, ?::timestamptz, ?::timestamptz, ?::jsonb, ?) returning analysis_id, status, @@ -29,7 +46,12 @@ public CreateRequestAction(final Connection connection) throws SQLException { public RequestRecord apply(final SpecificationRecord specification, final String requestedBy) throws SQLException { this.statement.setLong(1, specification.id()); this.statement.setLong(2, specification.revision()); - this.statement.setString(3, requestedBy); + this.statement.setLong(3, specification.planRevision()); + //TODO: extract PreparedStatements into a shared library and replace these calls + this.statement.setString(4, TIMESTAMP_FORMAT.format(specification.horizonStartTimestamp().time())); + this.statement.setString(5, TIMESTAMP_FORMAT.format(specification.horizonEndTimestamp().time())); + this.statement.setString(6, simulationArgumentsP.unparse(specification.simulationArguments()).toString()); + this.statement.setString(7, requestedBy); final var result = this.statement.executeQuery(); if (!result.next()) throw new FailedInsertException("scheduling_request"); @@ -50,6 +72,7 @@ public RequestRecord apply(final SpecificationRecord specification, final String specification.id(), analysis_id, specification.revision(), + specification.planRevision(), status, failureReason$, canceled, diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java index f39210d42e..7622976f3b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetCreatedActivitiesAction.java @@ -16,6 +16,7 @@ private static final @Language("SQL") String sql = """ select c.goal_id, + c.goal_revision, c.activity_id from scheduling_goal_analysis_created_activities as c where c.analysis_id = ? @@ -33,7 +34,7 @@ public Map> get(final long analysisId) throws final var createdActivities = new HashMap>(); while (resultSet.next()) { - final var goalId = new GoalId(resultSet.getLong("goal_id")); + final var goalId = new GoalId(resultSet.getLong("goal_id"), resultSet.getLong("goal_revision")); final var activityId = new ActivityDirectiveId(resultSet.getLong("activity_id")); if (!createdActivities.containsKey(goalId)) createdActivities.put(goalId, new ArrayList<>()); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java index d33d76473f..2294bb0ece 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetGoalSatisfactionAction.java @@ -12,6 +12,7 @@ private final static @Language("SQL") String sql = """ select goal.goal_id, + goal.goal_revision, goal.satisfied from scheduling_goal_analysis as goal where goal.analysis_id = ? @@ -30,7 +31,7 @@ public Map get(final long analysisId) throws SQLException { final var goals = new HashMap(); while (resultSet.next()) { goals.put( - new GoalId(resultSet.getLong("goal_id")), + new GoalId(resultSet.getLong("goal_id"), resultSet.getLong("goal_revision")), resultSet.getBoolean("satisfied") ); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetRequestAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetRequestAction.java index e92ffeb9f7..8ee5e8a575 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetRequestAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetRequestAction.java @@ -1,12 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; -import javax.json.Json; -import gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers; -import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleFailure; import org.intellij.lang.annotations.Language; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -21,9 +16,9 @@ r.canceled, r.dataset_id from scheduling_request as r - where - r.specification_id = ? and - r.specification_revision = ? + where r.specification_id = ? + and r.specification_revision = ? + and r.plan_revision = ? """; private final PreparedStatement statement; @@ -34,10 +29,12 @@ public GetRequestAction(final Connection connection) throws SQLException { public Optional get( final long specificationId, - final long specificationRevision + final long specificationRevision, + final long planRevision ) throws SQLException { this.statement.setLong(1, specificationId); this.statement.setLong(2, specificationRevision); + this.statement.setLong(3, planRevision); final var resultSet = this.statement.executeQuery(); if (!resultSet.next()) return Optional.empty(); @@ -62,6 +59,7 @@ public Optional get( specificationId, analysisId, specificationRevision, + planRevision, status, failureReason$, canceled, diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java index a58383267f..e049fc4593 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSatisfyingActivitiesAction.java @@ -16,6 +16,7 @@ private static final @Language("SQL") String sql = """ select s.goal_id, + s.goal_revision, s.activity_id from scheduling_goal_analysis_satisfying_activities as s where s.analysis_id = ? @@ -33,7 +34,7 @@ public Map> get(final long analysisId) throws final var satisfyingActivities = new HashMap>(); while (resultSet.next()) { - final var goalId = new GoalId(resultSet.getLong("goal_id")); + final var goalId = new GoalId(resultSet.getLong("goal_id"), resultSet.getLong("goal_revision")); final var activityId = new ActivityDirectiveId(resultSet.getLong("activity_id")); satisfyingActivities diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationConditionsAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationConditionsAction.java index d6ddfcecd4..61434da968 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationConditionsAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationConditionsAction.java @@ -1,5 +1,8 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; +import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionId; +import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionRecord; +import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionSource; import org.intellij.lang.annotations.Language; import java.sql.Connection; @@ -10,16 +13,17 @@ /*package-local*/ final class GetSpecificationConditionsAction implements AutoCloseable { private final @Language("SQL") String sql = """ - select - s.condition_id, - s.enabled, - c.name, - c.definition, - c.revision - from scheduling_specification_conditions as s - join scheduling_condition as c - on s.specification_id = ? - and s.condition_id = c.id + select s.condition_id, cd.revision, cm.name, cd.definition + from scheduling_specification_conditions s + left join scheduling_condition_definition cd using (condition_id) + left join scheduling_condition_metadata cm on s.condition_id = cm.id + where s.specification_id = ? + and s.enabled + and ((s.condition_revision is not null and s.condition_revision = cd.revision) + or (s.condition_revision is null and cd.revision = (select def.revision + from scheduling_condition_definition def + where def.condition_id = s.condition_id + order by def.revision desc limit 1))); """; private final PreparedStatement statement; @@ -28,21 +32,20 @@ public GetSpecificationConditionsAction(final Connection connection) throws SQLE this.statement = connection.prepareStatement(sql); } - public List get(final long specificationId) throws SQLException { + public List get(final long specificationId) throws SQLException { this.statement.setLong(1, specificationId); final var resultSet = this.statement.executeQuery(); - final var goals = new ArrayList(); + final var conditions = new ArrayList(); while (resultSet.next()) { final var id = resultSet.getLong("condition_id"); final var revision = resultSet.getLong("revision"); final var name = resultSet.getString("name"); final var definition = resultSet.getString("definition"); - final var enabled = resultSet.getBoolean("enabled"); - goals.add(new PostgresSchedulingConditionRecord(id, revision, name, definition, enabled)); + conditions.add(new SchedulingConditionRecord(new SchedulingConditionId(id), revision, name, new SchedulingConditionSource(definition))); } - return goals; + return conditions; } @Override diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java index 37eda38d6e..73ef450634 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GetSpecificationGoalsAction.java @@ -1,5 +1,8 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; +import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; +import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import org.intellij.lang.annotations.Language; import java.sql.Connection; @@ -10,18 +13,19 @@ /*package-local*/ final class GetSpecificationGoalsAction implements AutoCloseable { private final @Language("SQL") String sql = """ - select - s.goal_id, - g.name, - g.definition, - g.revision, - s.enabled, - s.simulate_after - from scheduling_specification_goals as s - left join scheduling_goal as g on s.goal_id = g.id - where s.specification_id = ? - order by s.priority; - """; + select s.goal_id, gd.revision, gm.name, gd.definition, s.simulate_after + from scheduling_specification_goals s + left join scheduling_goal_definition gd using (goal_id) + left join scheduling_goal_metadata gm on s.goal_id = gm.id + where s.specification_id = ? + and s.enabled + and ((s.goal_revision is not null and s.goal_revision = gd.revision) + or (s.goal_revision is null and gd.revision = (select def.revision + from scheduling_goal_definition def + where def.goal_id = s.goal_id + order by def.revision desc limit 1))) + order by s.priority; + """; private final PreparedStatement statement; @@ -29,19 +33,18 @@ public GetSpecificationGoalsAction(final Connection connection) throws SQLExcept this.statement = connection.prepareStatement(sql); } - public List get(final long specificationId) throws SQLException { + public List get(final long specificationId) throws SQLException { this.statement.setLong(1, specificationId); final var resultSet = this.statement.executeQuery(); - final var goals = new ArrayList(); + final var goals = new ArrayList(); while (resultSet.next()) { final var id = resultSet.getLong("goal_id"); final var revision = resultSet.getLong("revision"); final var name = resultSet.getString("name"); final var definition = resultSet.getString("definition"); - final var enabled = resultSet.getBoolean("enabled"); final var simulateAfter = resultSet.getBoolean("simulate_after"); - goals.add(new PostgresGoalRecord(id, revision, name, definition, enabled, simulateAfter)); + goals.add(new GoalRecord(new GoalId(id, revision), name, new GoalSource(definition), simulateAfter)); } return goals; diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java index 53c0691803..84647e1ca0 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertCreatedActivitiesAction.java @@ -13,8 +13,8 @@ /*package-local*/ final class InsertCreatedActivitiesAction implements AutoCloseable { private static final @Language("SQL") String sql = """ - insert into scheduling_goal_analysis_created_activities (analysis_id, goal_id, activity_id) - values (?, ?, ?) + insert into scheduling_goal_analysis_created_activities (analysis_id, goal_id, goal_revision, activity_id) + values (?, ?, ?, ?) """; private final PreparedStatement statement; @@ -28,11 +28,12 @@ public void apply( final Map> createdActivities ) throws SQLException { for (final var entry : createdActivities.entrySet()) { - final var goalId = entry.getKey().id(); + final var goal = entry.getKey(); for (final var activityId : entry.getValue()) { this.statement.setLong(1, analysisId); - this.statement.setLong(2, goalId); - this.statement.setLong(3, activityId.id()); + this.statement.setLong(2, goal.id()); + this.statement.setLong(3, goal.revision()); + this.statement.setLong(4, activityId.id()); this.statement.addBatch(); } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java index 1554e0eca5..873992e088 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertGoalSatisfactionAction.java @@ -11,8 +11,8 @@ /*package-local*/ final class InsertGoalSatisfactionAction implements AutoCloseable { private static final @Language("SQL") String sql = """ - insert into scheduling_goal_analysis (analysis_id, goal_id, satisfied) - values (?, ?, ?) + insert into scheduling_goal_analysis (analysis_id, goal_id, goal_revision, satisfied) + values (?, ?, ?, ?) """; private final PreparedStatement statement; @@ -23,9 +23,11 @@ public InsertGoalSatisfactionAction(final Connection connection) throws SQLExcep public void apply(final long analysisId, final Map goalSatisfactions) throws SQLException { for (final var entry : goalSatisfactions.entrySet()) { + final var goal = entry.getKey(); this.statement.setLong(1, analysisId); - this.statement.setLong(2, entry.getKey().id()); - this.statement.setBoolean(3, entry.getValue()); + this.statement.setLong(2, goal.id()); + this.statement.setLong(3, goal.revision()); + this.statement.setBoolean(4, entry.getValue()); this.statement.addBatch(); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java index 344e826e81..d08f7870a4 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/InsertSatisfyingActivitiesAction.java @@ -13,8 +13,8 @@ /*package-local*/ final class InsertSatisfyingActivitiesAction implements AutoCloseable { private static final @Language("SQL") String sql = """ - insert into scheduling_goal_analysis_satisfying_activities (analysis_id, goal_id, activity_id) - values (?, ?, ?) + insert into scheduling_goal_analysis_satisfying_activities (analysis_id, goal_id, goal_revision, activity_id) + values (?, ?, ?, ?) """; private final PreparedStatement statement; @@ -28,11 +28,12 @@ public void apply( final Map> satisfyingActivities ) throws SQLException { for (final var entry : satisfyingActivities.entrySet()) { - final var goalId = entry.getKey().id(); + final var goal = entry.getKey(); for (final var activityId : entry.getValue()) { this.statement.setLong(1, analysisId); - this.statement.setLong(2, goalId); - this.statement.setLong(3, activityId.id()); + this.statement.setLong(2, goal.id()); + this.statement.setLong(3, goal.revision()); + this.statement.setLong(4, activityId.id()); this.statement.addBatch(); } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresGoalRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresGoalRecord.java deleted file mode 100644 index e996ad02bf..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresGoalRecord.java +++ /dev/null @@ -1,10 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; - -public record PostgresGoalRecord( - long id, - long revision, - String name, - String definition, - boolean enabled, - boolean simulateAfter -) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java index 97a3866ec7..96e4330ae7 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java @@ -216,10 +216,10 @@ private static void postResults( final var createdActivities = new HashMap>(numGoals); final var satisfyingActivities = new HashMap>(numGoals); - results.goalResults().forEach((goalId, result) -> { - goalSatisfaction.put(goalId, result.satisfied()); - createdActivities.put(goalId, result.createdActivities()); - satisfyingActivities.put(goalId, result.satisfyingActivities()); + results.goalResults().forEach((goal, result) -> { + goalSatisfaction.put(goal, result.satisfied()); + createdActivities.put(goal, result.createdActivities()); + satisfyingActivities.put(goal, result.satisfyingActivities()); }); try ( diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSchedulingConditionRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSchedulingConditionRecord.java deleted file mode 100644 index bdb6664ab4..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSchedulingConditionRecord.java +++ /dev/null @@ -1,9 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; - -public record PostgresSchedulingConditionRecord( - long id, - long revision, - String name, - String definition, - boolean enabled -) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java index f9a26ff51b..355a4a6e7a 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java @@ -1,16 +1,12 @@ package gov.nasa.jpl.aerie.scheduler.server.remotes.postgres; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; -import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionRecord; -import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionSource; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; +import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; -import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.remotes.SpecificationRepository; -import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; import javax.sql.DataSource; import java.sql.SQLException; @@ -29,8 +25,8 @@ public Specification getSpecification(final SpecificationId specificationId) { final SpecificationRecord specificationRecord; final PlanId planId; - final List postgresGoalRecords; - final List postgresSchedulingConditionRecords; + final List goals; + final List schedulingConditions; try (final var connection = this.dataSource.getConnection(); final var getSpecificationAction = new GetSpecificationAction(connection); final var getSpecificationGoalsAction = new GetSpecificationGoalsAction(connection); @@ -40,39 +36,23 @@ public Specification getSpecification(final SpecificationId specificationId) .get(specificationId.id()) .orElseThrow(() -> new NoSuchSpecificationException(specificationId)); planId = new PlanId(specificationRecord.planId()); - postgresGoalRecords = getSpecificationGoalsAction.get(specificationId.id()); - postgresSchedulingConditionRecords = getSpecificationConditionsAction.get(specificationId.id()); + goals = getSpecificationGoalsAction.get(specificationId.id()); + schedulingConditions = getSpecificationConditionsAction.get(specificationId.id()); } catch (final SQLException ex) { throw new DatabaseException("Failed to get scheduling specification", ex); } - final var goals = postgresGoalRecords - .stream() - .map((PostgresGoalRecord pgGoal) -> new GoalRecord( - new GoalId(pgGoal.id()), - new GoalSource(pgGoal.definition()), - pgGoal.enabled(), - pgGoal.simulateAfter() - )) - .toList(); - - final var globalSchedulingConditions = postgresSchedulingConditionRecords - .stream() - .map((PostgresSchedulingConditionRecord pgCondition) -> new GlobalSchedulingConditionRecord( - new GlobalSchedulingConditionSource(pgCondition.definition()), - pgCondition.enabled() - )) - .toList(); - return new Specification( + specificationId, + specificationRecord.revision(), planId, specificationRecord.planRevision(), - goals, specificationRecord.horizonStartTimestamp(), specificationRecord.horizonEndTimestamp(), specificationRecord.simulationArguments(), specificationRecord.analysisOnly(), - globalSchedulingConditions + goals, + schedulingConditions ); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/RequestRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/RequestRecord.java index 108a801e63..6d3fc19355 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/RequestRecord.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/RequestRecord.java @@ -7,6 +7,7 @@ public record RequestRecord( long specificationId, long analysisId, long specificationRevision, + long planRevision, Status status, Optional reason, boolean canceled, diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRecord.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRecord.java index 9870891293..c97bca9a34 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRecord.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRecord.java @@ -5,7 +5,7 @@ import java.util.Map; -public final record SpecificationRecord( +public record SpecificationRecord( long id, long revision, long planId, diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index 637286c7f7..1d2893c6d1 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -30,7 +30,6 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; -import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraint; import gov.nasa.jpl.aerie.scheduler.goals.Goal; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; import gov.nasa.jpl.aerie.scheduler.model.Plan; @@ -147,8 +146,7 @@ public void schedule( //apply constraints/goals to the problem final var compiledGlobalSchedulingConditions = new ArrayList(); final var failedGlobalSchedulingConditions = new ArrayList>(); - specification.globalSchedulingConditions().forEach($ -> { - if (!$.enabled()) return; + specification.schedulingConditions().forEach($ -> { final var result = schedulingDSLCompilationService.compileGlobalSchedulingCondition( merlinService, planMetadata.planId(), @@ -182,7 +180,6 @@ public void schedule( final var compiledGoals = new ArrayList>(); final var failedGoals = new ArrayList>>(); for (final var goalRecord : specification.goalsByPriority()) { - if (!goalRecord.enabled()) continue; final var result = compileGoalDefinition( merlinService, planMetadata.planId(), @@ -376,23 +373,6 @@ private void ensureRequestIsCurrent(final ScheduleRequest request) throws NoSuch } } - /** - * collects the scheduling goals that apply to the current scheduling run on the target plan - * - * @param planMetadata details of the plan container whose associated goals should be collected - * @param mission the mission model that the plan adheres to, possibly associating additional relevant goals - * @return the list of goals relevant to the target plan - * @throws ResultsProtocolFailure when the constraints could not be loaded, or the data stores could not be - * reached - */ - private List loadConstraints(final PlanMetadata planMetadata, final MissionModel mission) { - //TODO: is the plan and mission model enough to find the relevant constraints? (eg what about sandbox toggling?) - //TODO: load global constraints from scheduler data store? - //TODO: load activity type constraints from somewhere (scheduler store? mission model?) - //TODO: somehow apply user control over which constraints to enforce during scheduling - return List.of(); - } - /** * load the activity instance content of the specified merlin plan into scheduler-ready objects * diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index 9ff6bb6ed1..7c600303cd 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -40,8 +40,9 @@ import gov.nasa.jpl.aerie.scheduler.server.config.PlanOutputMode; import gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers; import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; -import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionRecord; -import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionSource; +import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionId; +import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionRecord; +import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingConditionSource; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; @@ -50,8 +51,8 @@ import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.models.Timestamp; +import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.SpecificationRevisionData; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; -import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; import gov.nasa.jpl.aerie.scheduler.model.Plan; @@ -104,7 +105,7 @@ void testEmptyPlanEmptySpecification() { @Test void testEmptyPlanSimpleRecurrenceGoal() { - final var results = runScheduler(BANANANATION, List.of(), List.of(new SchedulingGoal(new GoalId(0L), """ + final var results = runScheduler(BANANANATION, List.of(), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: "fromStem", @@ -113,7 +114,7 @@ export default () => Goal.ActivityRecurrenceGoal({ }) """, true)), PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(4, goalResult.createdActivities().size()); for (final var activity : goalResult.createdActivities()) { @@ -133,7 +134,7 @@ export default () => Goal.ActivityRecurrenceGoal({ @Test void testRecurrenceGoalNegative() { try { - runScheduler(BANANANATION, List.of(), List.of(new SchedulingGoal(new GoalId(0L), """ + runScheduler(BANANANATION, List.of(), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: "fromStem", @@ -153,7 +154,7 @@ export default () => Goal.ActivityRecurrenceGoal({ @Test void testEmptyPlanDurationCardinalityGoal() { - final var results = runScheduler(BANANANATION, List.of(), List.of(new SchedulingGoal(new GoalId(0L), """ + final var results = runScheduler(BANANANATION, List.of(), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default function myGoal() { return Goal.CardinalityGoal({ activityTemplate: ActivityTemplates.GrowBanana({ @@ -165,7 +166,7 @@ export default function myGoal() { } """, true)),List.of(createAutoMutex("GrowBanana")), PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(10, goalResult.createdActivities().size()); @@ -194,7 +195,7 @@ export default function myGoal() { void testEmptyPlanOccurrenceCardinalityGoal() { final var results = runScheduler(BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default function myGoal() { return Goal.CardinalityGoal({ activityTemplate: ActivityTemplates.GrowBanana({ @@ -207,7 +208,7 @@ export default function myGoal() { """, true)),List.of(createAutoMutex("GrowBanana")),PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(10, goalResult.createdActivities().size()); @@ -237,7 +238,7 @@ void testEmptyPlanOccurrenceUnitaryGoalTimeInstant() { final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: Temporal.Instant.from("2021-01-01T05:00:00.000Z"), activityTemplate: (span) => ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -247,7 +248,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -272,7 +273,7 @@ void testEmptyPlanOccurrenceUnitaryGoalTimeInterval() { final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: Interval.Between(Temporal.Instant.from("2021-01-01T05:00:00.000Z"), Temporal.Instant.from("2021-01-01T10:00:00.000Z"), Inclusivity.Inclusive, Inclusivity.Exclusive), activityTemplate: (span) => ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -282,7 +283,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -314,7 +315,7 @@ void testSingleActivityPlanSimpleRecurrenceGoal() { Map.of("biteSize", SerializedValue.of(1)), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), interval: Temporal.Duration.from({days: 1}) @@ -323,7 +324,7 @@ export default () => Goal.ActivityRecurrenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(4, goalResult.createdActivities().size()); @@ -361,7 +362,7 @@ void testSingleActivityPlanSimpleCoexistenceGoalWithValueAtParams() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (growBananaActivity) => ActivityTemplates.ChangeProducer({producer: Discrete.Resource("/producer").valueAt(growBananaActivity.span().starts())}), @@ -371,7 +372,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -418,7 +419,7 @@ void testCoexistencePartialAct() { true ) ), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.BiteBanana), activityFinder: ActivityExpression.ofType(ActivityTypes.GrowBanana), @@ -429,7 +430,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(0, goalResult.createdActivities().size()); @@ -480,7 +481,7 @@ void testCoexistencePartialActWithParameter() { true ) ), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.BiteBanana), activityFinder: ActivityExpression.build(ActivityTypes.GrowBanana, {quantity: 1}), @@ -491,7 +492,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -520,6 +521,7 @@ export default () => Goal.CoexistenceGoal({ for(final var actAtTime10: planByTime.get(MINUTES.times(10))){ if(actAtTime10.serializedActivity().equals(expectedCreation)){ lookingFor = true; + break; } } assertTrue(lookingFor); @@ -558,7 +560,7 @@ void testRecurrenceWithActivityFinder() { null, true) ), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.GrowBanana({quantity: 2, growingDuration: Temporal.Duration.from({seconds:1})}), activityFinder: ActivityExpression.build(ActivityTypes.GrowBanana, {quantity: 2}), @@ -568,7 +570,7 @@ export default () => Goal.ActivityRecurrenceGoal({ PLANNING_HORIZON ); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); var foundFirst = false; var foundSecond = false; for(final var satisfyingActivity: goalResult.satisfyingActivities()){ @@ -598,7 +600,7 @@ void testCardinalityGoalWithActivityFinder() { "growingDuration", SerializedValue.of(Duration.of(5, SECONDS).in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default function myGoal() { return Goal.CardinalityGoal({ @@ -612,7 +614,7 @@ export default function myGoal() { } """, true)),List.of(createAutoMutex("GrowBanana")), PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(5, goalResult.createdActivities().size()); @@ -636,7 +638,7 @@ void testSingleActivityPlanSimpleCoexistenceGoalWithFunctionalParameters() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (growBananaActivity) => ActivityTemplates.PickBanana({quantity: growBananaActivity.parameters.quantity}), @@ -646,7 +648,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -693,7 +695,7 @@ void testSingleActivityPlanSimpleCoexistenceGoalWithWindowReference() { true ) ), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: Real.Resource("/fruit").lessThan(4), activityTemplate: (interval) => ActivityTemplates.GrowBanana({quantity: 10, growingDuration: interval.duration() }), @@ -703,7 +705,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -742,7 +744,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -752,7 +754,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -791,7 +793,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_constrainEndTime() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -801,7 +803,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -844,7 +846,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenBefore() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -854,7 +856,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -898,7 +900,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenEquals() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.DurationParameterActivity({duration: Temporal.Duration.from({ hours : 1})}), @@ -909,7 +911,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -955,7 +957,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenMeets() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -965,7 +967,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -1008,7 +1010,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenOverlaps() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.DurationParameterActivity({duration: Temporal.Duration.from({ hours : 1})}), @@ -1018,7 +1020,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -1059,7 +1061,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenContains() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.DurationParameterActivity({duration: Temporal.Duration.from({ minutes : 50})}), @@ -1070,7 +1072,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -1119,7 +1121,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenStarts() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1129,7 +1131,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -1174,7 +1176,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenFinishesAt() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.DurationParameterActivity({duration: Temporal.Duration.from({ minutes : 50})}), @@ -1184,7 +1186,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -1228,7 +1230,7 @@ void testSingleActivityPlanSimpleCoexistenceGoal_AllenFinishesWithin() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (span) => ActivityTemplates.DurationParameterActivity({duration: Temporal.Duration.from({ minutes : 50})}), @@ -1238,7 +1240,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -1287,7 +1289,7 @@ void testStateCoexistenceGoal_greaterThan() { "PickBanana", Map.of("quantity", SerializedValue.of(100)), null, - true)), List.of(new SchedulingGoal(new GoalId(0L), """ + true)), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1331,7 +1333,7 @@ void testLessThan() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1370,7 +1372,7 @@ void testLinear_atChangePoints() { Map.of("biteSize", SerializedValue.of(1.0)), null, true) - ), List.of(new SchedulingGoal(new GoalId(0L), """ + ), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1407,7 +1409,7 @@ void testLinear_interpolated() { null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromTip"}), @@ -1453,7 +1455,7 @@ void testEqualTo_satsified() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1498,7 +1500,7 @@ void testEqualTo_neverSatisfied() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1541,7 +1543,7 @@ void testNotEqualTo_satisfied() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1586,7 +1588,7 @@ void testBetweenInTermsOfAnd() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1634,7 +1636,7 @@ void testWindowsOr() { "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1669,7 +1671,7 @@ void testWindowsTransition() { "ChangeProducer", Map.of(), null, - true)), List.of(new SchedulingGoal(new GoalId(0L), """ + true)), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1698,7 +1700,7 @@ void testWindowsTransition_unsatisfied() { "ChangeProducer", Map.of("producer", SerializedValue.of("Fyffes")), null, - true)), List.of(new SchedulingGoal(new GoalId(0L), """ + true)), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1726,7 +1728,7 @@ void testExternalResource() { final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -1790,7 +1792,7 @@ void testApplyWhen() { null, true) ), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.ChangeProducer({producer: "Morpheus"}), interval: Temporal.Duration.from({ hours : 24 }) @@ -1816,7 +1818,7 @@ void testGlobalSchedulingConditions_conditionNeverOccurs() { final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.ChangeProducer({producer: "Morpheus"}), interval: Temporal.Duration.from({days: 1}) @@ -1824,12 +1826,13 @@ export default () => Goal.ActivityRecurrenceGoal({ """, true) ), List.of( - new GlobalSchedulingConditionRecord( - new GlobalSchedulingConditionSource(""" + new SchedulingConditionRecord( + new SchedulingConditionId(0L), + 0L, + "fruit greater than 5", + new SchedulingConditionSource(""" export default () => GlobalSchedulingCondition.scheduleActivitiesOnlyWhen(Real.Resource(\"/fruit\").greaterThan(5.0)) - """), - true - ), + """)), createAutoMutex("ChangeProducer") ), PLANNING_HORIZON); @@ -1841,7 +1844,7 @@ void testGlobalSchedulingConditions_conditionAlwaysTrue() { final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.ChangeProducer({producer: "Morpheus"}), interval: Temporal.Duration.from({days: 1}) @@ -1849,10 +1852,11 @@ export default () => Goal.ActivityRecurrenceGoal({ """, true) ), List.of( - new GlobalSchedulingConditionRecord( - new GlobalSchedulingConditionSource("export default () => GlobalSchedulingCondition.scheduleActivitiesOnlyWhen(Real.Resource(\"/fruit\").lessThan(5.0))"), - true - ) + new SchedulingConditionRecord( + new SchedulingConditionId(0L), + 0L, + "fruit less than 5", + new SchedulingConditionSource("export default () => GlobalSchedulingCondition.scheduleActivitiesOnlyWhen(Real.Resource(\"/fruit\").lessThan(5.0))")) ), PLANNING_HORIZON); assertEquals(4, results.updatedPlan().size()); @@ -1870,7 +1874,7 @@ void testGlobalSchedulingConditions_conditionSometimesTrue() { null, true) ), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.ChangeProducer({producer: "Morpheus"}), interval: Temporal.Duration.from({days: 1}) @@ -1878,10 +1882,11 @@ export default () => Goal.ActivityRecurrenceGoal({ """, true) ), List.of( - new GlobalSchedulingConditionRecord( - new GlobalSchedulingConditionSource("export default () => GlobalSchedulingCondition.scheduleActivitiesOnlyWhen(Real.Resource(\"/fruit\").greaterThan(3.5))"), - true - ) + new SchedulingConditionRecord( + new SchedulingConditionId(0L), + 0L, + "fruit greater than 3.5", + new SchedulingConditionSource("export default () => GlobalSchedulingCondition.scheduleActivitiesOnlyWhen(Real.Resource(\"/fruit\").greaterThan(3.5))")) ), PLANNING_HORIZON); assertEquals(2, results.updatedPlan().size()); @@ -1930,15 +1935,17 @@ private static File getLatestJarFile(final Path libPath) { return files[0]; } - public static GlobalSchedulingConditionRecord createAutoMutex(String activityType){ - return new GlobalSchedulingConditionRecord( - new GlobalSchedulingConditionSource(""" + public static SchedulingConditionRecord createAutoMutex(String activityType){ + return new SchedulingConditionRecord( + new SchedulingConditionId(0L), + 0L, + "auto-mutex activity type "+activityType, + new SchedulingConditionSource(""" export default function myCondition() { return GlobalSchedulingCondition.mutex([ActivityTypes.%s], [ActivityTypes.%s]) } - """.formatted(activityType, activityType)), - true - ); + """.formatted(activityType, activityType))); + } private SchedulingRunResults runScheduler( @@ -1970,7 +1977,7 @@ private SchedulingRunResults runScheduler( final MissionModelDescription desc, final List plannedActivities, final Iterable goals, - final List globalSchedulingConditions, + final List globalSchedulingConditions, final PlanningHorizon planningHorizon ){ return runScheduler(desc, plannedActivities, goals, globalSchedulingConditions, planningHorizon, Optional.empty()); @@ -1980,7 +1987,7 @@ private SchedulingRunResults runScheduler( final MissionModelDescription desc, final List plannedActivities, final Iterable goals, - final List globalSchedulingConditions, + final List globalSchedulingConditions, final PlanningHorizon planningHorizon, final Optional externalProfiles ){ @@ -1996,7 +2003,7 @@ private SchedulingRunResults runScheduler( final MissionModelDescription desc, final Map plannedActivities, final Iterable goals, - final List globalSchedulingConditions, + final List globalSchedulingConditions, final PlanningHorizon planningHorizon, final Optional externalProfiles ) { @@ -2009,17 +2016,19 @@ private SchedulingRunResults runScheduler( final var goalsByPriority = new ArrayList(); for (final var goal : goals) { - goalsByPriority.add(new GoalRecord(goal.goalId(), new GoalSource(goal.definition()), goal.enabled(), goal.simulateAfter())); + goalsByPriority.add(new GoalRecord(goal.goalId(), "test goal", new GoalSource(goal.definition()), goal.simulateAfter())); } final var specificationService = new SpecificationService(new MockSpecificationRepository(Map.of(new SpecificationId(1L), new Specification( + new SpecificationId(1L), + 1L, planId, 1L, - goalsByPriority, new Timestamp(planningHorizon.getStartInstant()), new Timestamp(planningHorizon.getEndInstant()), Map.of(), false, - globalSchedulingConditions))); + goalsByPriority, + globalSchedulingConditions)))); final var agent = new SynchronousSchedulerAgent( specificationService, mockMerlinService, @@ -2103,7 +2112,7 @@ void testAndFailure(){ "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CardinalityGoal({ activityTemplate: ActivityTemplates.GrowBanana({ @@ -2164,7 +2173,7 @@ void testOrFailure(){ "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CardinalityGoal({ activityTemplate: ActivityTemplates.GrowBanana({ @@ -2216,7 +2225,7 @@ void testOr(){ "growingDuration", SerializedValue.of(growBananaDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.CoexistenceGoal({ activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2272,7 +2281,7 @@ export default function myGoal() { Map.of(), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), goalDefinition, true)), + List.of(new SchedulingGoal(new GoalId(0L, 0L), goalDefinition, true)), planningHorizon); final var planByActivityType = partitionByActivityType(results.updatedPlan()); final var biteBanana = planByActivityType.get("BiteBanana").stream().map((bb) -> bb.startOffset()).toList(); @@ -2307,7 +2316,7 @@ export default function myGoal() { Map.of(), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), goalDefinition, true)), + List.of(new SchedulingGoal(new GoalId(0L, 0L), goalDefinition, true)), List.of(), planningHorizon); final var planByActivityType = partitionByActivityType(results.updatedPlan()); @@ -2329,7 +2338,7 @@ void testDurationParameter() { final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.DurationParameterActivity({ @@ -2351,7 +2360,7 @@ void testUnfinishedActivity(){ final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => { return Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.parent({ @@ -2363,7 +2372,7 @@ export default (): Goal => { """, true)), PLANNING_HORIZON); //parent takes much more than 134 - 90 = 44 days to finish assertEquals(0, results.updatedPlan.size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertFalse(goalResult.satisfied()); } @@ -2375,7 +2384,7 @@ public void testBugDurationInMicroseconds(){ final var results = runScheduler( BANANANATION, List.of(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.BakeBananaBread({ temperature: 325.0, tbSugar: 2, glutenFree: false }), @@ -2386,7 +2395,7 @@ export default (): Goal => runScheduler( BANANANATION, results.updatedPlan.stream().toList(), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.BakeBananaBread({ temperature: 325.0, tbSugar: 2, glutenFree: false }), @@ -2407,20 +2416,20 @@ void test_inf_loop(){ Map.of("biteSize", SerializedValue.of(10)), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default (): Goal => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.BakeBananaBread({ temperature: 325.0, tbSugar: 2, glutenFree: false }), interval: Temporal.Duration.from({ hours: 2 }), }); """, true)), - List.of(new GlobalSchedulingConditionRecord(new GlobalSchedulingConditionSource( + List.of(new SchedulingConditionRecord(new SchedulingConditionId(0), 0L, "fruit less than 3", new SchedulingConditionSource( """ export default function myFirstSchedulingCondition(): GlobalSchedulingCondition { return GlobalSchedulingCondition.scheduleActivitiesOnlyWhen(Real.Resource('/fruit').lessThan(3.0)); } """ - ), true)), + ))), new PlanningHorizon( TimeUtility.fromDOY("2022-318T00:00:00"), TimeUtility.fromDOY("2022-319T00:00:00"))); @@ -2467,7 +2476,7 @@ void testRelativeActivityPlanZeroStartOffsetEnd() { "duration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), new ActivityDirectiveId(2L), false)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2477,7 +2486,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -2551,7 +2560,7 @@ void testRelativeActivityPlanZeroStartOffsetStart() { "duration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), new ActivityDirectiveId(2L), false)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2561,7 +2570,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -2627,7 +2636,7 @@ void testRelativeActivityPlanNegativeStartOffsetStart() { "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), new ActivityDirectiveId(1L), true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2637,7 +2646,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -2701,7 +2710,7 @@ void testRelativeActivityPlanPositiveStartOffsetStart() { "quantity", SerializedValue.of(1)), new ActivityDirectiveId(1L), true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.PickBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2711,7 +2720,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -2772,7 +2781,7 @@ void testJustAfter(String timepoint, Duration resultingStartTime) { "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default function(){ TimingConstraint.defaultPadding = Temporal.Duration.from({milliseconds:1}) return Goal.CoexistenceGoal({ @@ -2785,7 +2794,7 @@ export default function(){ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -2842,7 +2851,7 @@ void testJustBefore(String timepoint, Duration resultingStartTime) { "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), null, true)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2852,7 +2861,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -2913,7 +2922,7 @@ void testRelativeActivityPlanPositiveEndOffsetEnd() { "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), new ActivityDirectiveId(1L), false)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2923,7 +2932,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); assertEquals(1, goalResult.createdActivities().size()); @@ -2985,7 +2994,7 @@ void testDontScheduleFromOutsidePlanBounds(){ "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), null, false)), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -2995,7 +3004,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); // The goal is satisfied, but placed no activities, // because all activities it could've matched on are outside the plan bounds @@ -3049,7 +3058,7 @@ void testOptionalSimulationAfterGoal_unsimulatedActivities() { true) ), List.of( - new SchedulingGoal(new GoalId(0L), """ + new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.BananaNap(), @@ -3057,7 +3066,7 @@ export default () => Goal.CoexistenceGoal({ }) """, true, config.getKey() ), - new SchedulingGoal(new GoalId(1L), """ + new SchedulingGoal(new GoalId(1L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.BananaNap), activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), @@ -3068,8 +3077,8 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(2, results.scheduleResults.goalResults().size()); - final var goalResult1 = results.scheduleResults.goalResults().get(new GoalId(0L)); - final var goalResult2 = results.scheduleResults.goalResults().get(new GoalId(1L)); + final var goalResult1 = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); + final var goalResult2 = results.scheduleResults.goalResults().get(new GoalId(1L, 0L)); assertTrue(goalResult1.satisfied()); assertTrue(goalResult2.satisfied()); @@ -3112,7 +3121,7 @@ void testOptionalSimulationAfterGoal_staleResources() { true) ), List.of( - new SchedulingGoal(new GoalId(0L), """ + new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), @@ -3120,7 +3129,7 @@ export default () => Goal.CoexistenceGoal({ }) """, true, config.getKey() ), - new SchedulingGoal(new GoalId(1L), """ + new SchedulingGoal(new GoalId(1L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: Real.Resource("/fruit").greaterThan(5), activityTemplate: ActivityTemplates.BananaNap(), @@ -3131,8 +3140,8 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(2, results.scheduleResults.goalResults().size()); - final var goalResult1 = results.scheduleResults.goalResults().get(new GoalId(0L)); - final var goalResult2 = results.scheduleResults.goalResults().get(new GoalId(1L)); + final var goalResult1 = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); + final var goalResult2 = results.scheduleResults.goalResults().get(new GoalId(1L, 0L)); assertTrue(goalResult1.satisfied()); assertTrue(goalResult2.satisfied()); @@ -3169,7 +3178,7 @@ void daemonTaskTest(){ null, true) ), - List.of(new SchedulingGoal(new GoalId(0L), """ + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.ZeroDurationUncontrollableActivity), activityTemplate: ActivityTemplates.DaemonCheckerActivity({ @@ -3180,7 +3189,7 @@ export default () => Goal.CoexistenceGoal({ """, true)), PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - assertTrue(results.scheduleResults.goalResults().get(new GoalId(0L)).satisfied()); + assertTrue(results.scheduleResults.goalResults().get(new GoalId(0L, 0L)).satisfied()); assertEquals(2, results.updatedPlan.size()); @@ -3204,7 +3213,7 @@ export default () => Goal.CoexistenceGoal({ */ @Test void testEmptyPlanMinimalMissionModelSimpleRecurrenceGoal() { - runScheduler(MINIMAL_MISSION_MODEL, List.of(), List.of(new SchedulingGoal(new GoalId(0L), """ + runScheduler(MINIMAL_MISSION_MODEL, List.of(), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.SingleActivity(), interval: Temporal.Duration.from({ milliseconds: 24 * 60 * 60 * 1000 }) @@ -3268,7 +3277,7 @@ void testForEachWithActivityArguments() { true) ), List.of( - new SchedulingGoal(new GoalId(0L), """ + new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.build(ActivityTypes.GrowBanana, {quantity: 1}), activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), @@ -3280,7 +3289,7 @@ export default () => Goal.CoexistenceGoal({ PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); @@ -3294,7 +3303,7 @@ export default () => Goal.CoexistenceGoal({ @Test void testListOfListParam() { - final var results = runScheduler(FOO, List.of(), List.of(new SchedulingGoal(new GoalId(0L), """ + final var results = runScheduler(FOO, List.of(), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default function myGoal() { return Goal.CardinalityGoal({ activityTemplate: ActivityTemplates.foo({ @@ -3308,7 +3317,7 @@ export default function myGoal() { } """, true)),List.of(), PLANNING_HORIZON); assertEquals(1, results.scheduleResults.goalResults().size()); - final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); assertTrue(goalResult.satisfied()); From 4fe7e3d7172a00f76e4198e3c42f882084466647 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 13:04:11 -0800 Subject: [PATCH 148/159] Retrieve Plan Revision from Request Notifications --- .../worker/postgres/PostgresNotificationJsonParsers.java | 2 ++ .../postgres/PostgresSchedulingRequestNotificationPayload.java | 1 + 2 files changed, 3 insertions(+) diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresNotificationJsonParsers.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresNotificationJsonParsers.java index abc2203f23..6bf3f060f9 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresNotificationJsonParsers.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresNotificationJsonParsers.java @@ -11,11 +11,13 @@ public final class PostgresNotificationJsonParsers { public static final JsonParser postgresSchedulingRequestNotificationP = productP . field("specification_revision", longP) + . field("plan_revision", longP) . field("specification_id", longP) . field("analysis_id", longP) . map( untuple(PostgresSchedulingRequestNotificationPayload::new), $ -> tuple($.specificationRevision(), + $.planRevision(), $.specificationId(), $.analysisId())); } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresSchedulingRequestNotificationPayload.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresSchedulingRequestNotificationPayload.java index 133cdf5b3c..7175561a0d 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresSchedulingRequestNotificationPayload.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/postgres/PostgresSchedulingRequestNotificationPayload.java @@ -2,6 +2,7 @@ public record PostgresSchedulingRequestNotificationPayload( long specificationRevision, + long planRevision, long specificationId, long analysisId ) { } From 4de3b486ba30b6012ad7ee8dd01fbeb61535a347 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 13:02:23 -0800 Subject: [PATCH 149/159] Expand `SpecificationRevisionData` to include Plan Revision data - Use `SpecificationRevisionData` in place of `RevisionData` --- .../remotes/SpecificationRepository.java | 4 ++-- .../PostgresResultsCellRepository.java | 19 +++++++++++++------ .../PostgresSpecificationRepository.java | 9 ++++----- .../postgres/SpecificationRevisionData.java | 2 +- .../server/services/ScheduleAction.java | 9 ++++----- .../server/services/ScheduleRequest.java | 5 +++-- .../worker/SchedulerWorkerAppDriver.java | 3 ++- .../services/SchedulingIntegrationTests.java | 2 +- 8 files changed, 30 insertions(+), 23 deletions(-) diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java index 9479808150..fbd62ab67b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/SpecificationRepository.java @@ -4,11 +4,11 @@ import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; -import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; +import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.SpecificationRevisionData; public interface SpecificationRepository { // Queries Specification getSpecification(SpecificationId specificationId) throws NoSuchSpecificationException, SpecificationLoadException; - RevisionData getSpecificationRevisionData(SpecificationId specificationId) throws NoSuchSpecificationException; + SpecificationRevisionData getSpecificationRevisionData(SpecificationId specificationId) throws NoSuchSpecificationException; } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java index 96e4330ae7..90ccff0b4e 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java @@ -43,6 +43,7 @@ public ResultsProtocol.OwnerRole allocate(final SpecificationId specificationId, this.dataSource, new SpecificationId(request.specificationId()), request.specificationRevision(), + request.planRevision(), request.analysisId()); } catch (final SQLException ex) { throw new DatabaseException("Failed to get schedule specification", ex); @@ -70,6 +71,7 @@ public Optional claim(final SpecificationId specifica this.dataSource, new SpecificationId(request.specificationId()), request.specificationRevision(), + request.planRevision(), request.analysisId())); } catch (UnclaimableRequestException ex) { return Optional.empty(); @@ -130,11 +132,11 @@ private static SpecificationRecord getSpecification( private static Optional getRequest( final Connection connection, final SpecificationId specificationId, - final long specificationRevision + final long specificationRevision, + final long planRevision ) throws SQLException { try (final var getRequestAction = new GetRequestAction(connection)) { - return getRequestAction - .get(specificationId.id(), specificationRevision); + return getRequestAction.get(specificationId.id(), specificationRevision, planRevision); } } @@ -262,9 +264,10 @@ private static ScheduleResults getResults( private static Optional getRequestState( final Connection connection, final SpecificationId specId, - final long specRevision + final long specRevision, + final long planRevision ) throws SQLException { - final var request$ = getRequest(connection, specId, specRevision); + final var request$ = getRequest(connection, specId, specRevision, planRevision); if (request$.isEmpty()) return Optional.empty(); final var request = request$.get(); @@ -289,17 +292,20 @@ public static final class PostgresResultsCell implements ResultsProtocol.OwnerRo private final DataSource dataSource; private final SpecificationId specId; private final long specRevision; + private final long planRevision; private final long analysisId; public PostgresResultsCell( final DataSource dataSource, final SpecificationId specId, final long specRevision, + final long planRevision, final long analysisId ) { this.dataSource = dataSource; this.specId = specId; this.specRevision = specRevision; + this.planRevision = planRevision; this.analysisId = analysisId; } @@ -309,7 +315,8 @@ public ResultsProtocol.State get() { return getRequestState( connection, specId, - specRevision) + specRevision, + planRevision) .orElseThrow(() -> new Error("Scheduling request no longer exists")); } catch (final SQLException ex) { throw new DatabaseException("Failed to get scheduling request status", ex); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java index 355a4a6e7a..b09b690abd 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresSpecificationRepository.java @@ -57,17 +57,16 @@ public Specification getSpecification(final SpecificationId specificationId) } @Override - public RevisionData getSpecificationRevisionData(final SpecificationId specificationId) + public SpecificationRevisionData getSpecificationRevisionData(final SpecificationId specificationId) throws NoSuchSpecificationException { try (final var connection = this.dataSource.getConnection()) { try (final var getSpecificationAction = new GetSpecificationAction(connection)) { - final var specificationRevision = getSpecificationAction + final var spec = getSpecificationAction .get(specificationId.id()) - .orElseThrow(() -> new NoSuchSpecificationException(specificationId)) - .revision(); + .orElseThrow(() -> new NoSuchSpecificationException(specificationId)); - return new SpecificationRevisionData(specificationRevision); + return new SpecificationRevisionData(spec.revision(), spec.planRevision()); } } catch (final SQLException ex) { throw new DatabaseException("Failed to get scheduling specification revision data", ex); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRevisionData.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRevisionData.java index b9cf82bc36..8176a3b903 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRevisionData.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/SpecificationRevisionData.java @@ -2,7 +2,7 @@ import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; -public record SpecificationRevisionData(long specificationRevision) implements RevisionData { +public record SpecificationRevisionData(long specificationRevision, long planRevision) implements RevisionData { @Override public MatchResult matches(final RevisionData other) { if (!(other instanceof SpecificationRevisionData o)) return MatchResult.failure(""); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleAction.java index 56ac81661b..47f315fced 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleAction.java @@ -55,12 +55,11 @@ record Complete(ScheduleResults results, long analysisId, Optional dataset public Response run(final SpecificationId specificationId, final HasuraAction.Session session) throws NoSuchSpecificationException, IOException { - //record the plan revision as of the scheduling request time (in case work commences much later eg in worker thread) - //TODO may also need to verify the model revision / other volatile metadata matches one from request - final var specificationRev = this.specificationService.getSpecificationRevisionData(specificationId); + //record the plan and specification revision as of the scheduling request time (in case work commences much later in worker thread) + final var request = new ScheduleRequest(specificationId, this.specificationService.getSpecificationRevisionData(specificationId)); - //submit request to run scheduler (possibly asynchronously or even cached depending on service) - final var response = this.schedulerService.getScheduleResults(new ScheduleRequest(specificationId, specificationRev), session.hasuraUserId()); + //submit request to run scheduler workers + final var response = this.schedulerService.getScheduleResults(request, session.hasuraUserId()); return repackResponse(response); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleRequest.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleRequest.java index 002e86ba57..2a0ddcfeea 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleRequest.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ScheduleRequest.java @@ -2,14 +2,15 @@ import java.util.Objects; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; +import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.SpecificationRevisionData; /** * details of a scheduling request, including the target schedule specification version and goals to operate on * * @param specificationId target schedule specification to read as schedule configuration - * @param specificationRev the revision of the schedule specification when the schedule request was placed (to determine if stale) + * @param specificationRev the revision of the schedule specification and plan when the schedule request was placed (to determine if stale) */ -public record ScheduleRequest(SpecificationId specificationId, RevisionData specificationRev) { +public record ScheduleRequest(SpecificationId specificationId, SpecificationRevisionData specificationRev) { public ScheduleRequest { Objects.requireNonNull(specificationId); Objects.requireNonNull(specificationRev); diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index 79361bd1ca..f67a739832 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -83,6 +83,7 @@ public static void main(String[] args) throws Exception { final var notification = notificationQueue.poll(1, TimeUnit.MINUTES); if (notification == null) continue; final var specificationRevision = notification.specificationRevision(); + final var planRevision = notification.planRevision(); final var specificationId = new SpecificationId(notification.specificationId()); // Register as early as possible to avoid potentially missing a canceled signal @@ -94,7 +95,7 @@ public static void main(String[] args) throws Exception { continue; } - final var revisionData = new SpecificationRevisionData(specificationRevision); + final var revisionData = new SpecificationRevisionData(specificationRevision, planRevision); final ResultsProtocol.WriterRole writer = owner.get(); try { scheduleAgent.schedule(new ScheduleRequest(specificationId, revisionData), writer, canceledListener); diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index 7c600303cd..e78247d804 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -2038,7 +2038,7 @@ private SchedulingRunResults runScheduler( schedulingDSLCompiler); // Scheduling Goals -> Scheduling Specification final var writer = new MockResultsProtocolWriter(); - agent.schedule(new ScheduleRequest(new SpecificationId(1L), $ -> RevisionData.MatchResult.success()), writer, () -> false); + agent.schedule(new ScheduleRequest(new SpecificationId(1L), new SpecificationRevisionData(1L, 1L)), writer, () -> false); assertEquals(1, writer.results.size()); final var result = writer.results.get(0); if (result instanceof MockResultsProtocolWriter.Result.Failure e) { From 71a784d5811e707021f783c930ee8ef190ec02ac Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 13:09:23 -0800 Subject: [PATCH 150/159] Have `claim` use analysisId instead of SpecificationId - This is a simpler key. Additionally, if the specification is deleted, the request will fail to be claimable (as it would've also been deleted), which removes a call to the database. --- .../server/remotes/ResultsCellRepository.java | 2 +- .../postgres/PostgresResultsCellRepository.java | 16 +++------------- .../worker/SchedulerWorkerAppDriver.java | 3 ++- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java index 14ba31abc4..966595a0e3 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java @@ -7,7 +7,7 @@ public interface ResultsCellRepository { ResultsProtocol.OwnerRole allocate(SpecificationId specificationId, final String requestedBy); - Optional claim(SpecificationId specificationId); + Optional claim(long analysisId); Optional lookup(SpecificationId specificationId); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java index 90ccff0b4e..c795f3fc6f 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java @@ -53,20 +53,14 @@ public ResultsProtocol.OwnerRole allocate(final SpecificationId specificationId, } @Override - public Optional claim(final SpecificationId specificationId) + public Optional claim(final long analysisId) { try ( final var connection = this.dataSource.getConnection(); final var claimSimulationAction = new ClaimRequestAction(connection) ) { - claimSimulationAction.apply(specificationId.id()); - - final var spec = getSpecification(connection, specificationId); - final var request$ = getRequest(connection, specificationId, spec.revision()); - if (request$.isEmpty()) return Optional.empty(); - final var request = request$.get(); - - logger.info("Claimed scheduling request with specification id {}", specificationId); + final var request = claimSimulationAction.apply(analysisId); + logger.info("Claimed scheduling request with analysis id {}", analysisId); return Optional.of(new PostgresResultsCell( this.dataSource, new SpecificationId(request.specificationId()), @@ -75,10 +69,6 @@ public Optional claim(final SpecificationId specifica request.analysisId())); } catch (UnclaimableRequestException ex) { return Optional.empty(); - } catch (final NoSuchSpecificationException ex) { - throw new Error(String.format( - "Cannot process scheduling request for nonexistent specification %s%n", - specificationId), ex); } catch (final SQLException | DatabaseException ex) { throw new Error(ex.getMessage()); } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index f67a739832..253105dcaa 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -85,11 +85,12 @@ public static void main(String[] args) throws Exception { final var specificationRevision = notification.specificationRevision(); final var planRevision = notification.planRevision(); final var specificationId = new SpecificationId(notification.specificationId()); + final var analysisId = notification.analysisId(); // Register as early as possible to avoid potentially missing a canceled signal canceledListener.register(specificationId); - final Optional owner = stores.results().claim(specificationId); + final Optional owner = stores.results().claim(analysisId); if (owner.isEmpty()) { canceledListener.unregister(); continue; From 733e58138d6a78ce8bde077f874b499469666d73 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 13:12:40 -0800 Subject: [PATCH 151/159] Have `lookup` use SchedulingRequest instead of SpecificationId - The request already contains all the data that was being previously lookup up, saving a DB query and avoiding race conditions --- .../server/remotes/ResultsCellRepository.java | 3 ++- .../PostgresResultsCellRepository.java | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java index 966595a0e3..1d64ec949b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/ResultsCellRepository.java @@ -3,13 +3,14 @@ import java.util.Optional; import gov.nasa.jpl.aerie.scheduler.server.ResultsProtocol; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; +import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; public interface ResultsCellRepository { ResultsProtocol.OwnerRole allocate(SpecificationId specificationId, final String requestedBy); Optional claim(long analysisId); - Optional lookup(SpecificationId specificationId); + Optional lookup(ScheduleRequest request); void deallocate(ResultsProtocol.OwnerRole resultsCell); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java index c795f3fc6f..4e80ce37cd 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/PostgresResultsCellRepository.java @@ -18,6 +18,7 @@ import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.remotes.ResultsCellRepository; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleFailure; +import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults.GoalResult; import org.slf4j.Logger; @@ -75,23 +76,21 @@ public Optional claim(final long analysisId) } @Override - public Optional lookup(final SpecificationId specificationId) + public Optional lookup(final ScheduleRequest request) { try (final var connection = this.dataSource.getConnection()) { - final var spec = getSpecification(connection, specificationId); - final var request$ = getRequest(connection, specificationId, spec.revision()); + final var request$ = getRequest(connection, request); if (request$.isEmpty()) return Optional.empty(); - final var request = request$.get(); + final var r = request$.get(); return Optional.of(new PostgresResultsCell( this.dataSource, - new SpecificationId(request.specificationId()), - request.specificationRevision(), - request.analysisId())); + new SpecificationId(r.specificationId()), + r.specificationRevision(), + r.planRevision(), + r.analysisId())); } catch (final SQLException ex) { throw new DatabaseException("Failed to get schedule specification", ex); - } catch (final NoSuchSpecificationException ex) { - return Optional.empty(); } } @@ -119,6 +118,16 @@ private static SpecificationRecord getSpecification( } } + private static Optional getRequest( + final Connection connection, + final ScheduleRequest request + ) throws SQLException { + return getRequest(connection, + request.specificationId(), + request.specificationRev().specificationRevision(), + request.specificationRev().planRevision()); + } + private static Optional getRequest( final Connection connection, final SpecificationId specificationId, From f77708c721798775516954682f13da5bd2acf3a6 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Thu, 25 Jan 2024 13:16:29 -0800 Subject: [PATCH 152/159] Avoid needlessly reobtaining specification data --- .../services/SynchronousSchedulerAgent.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index 1d2893c6d1..072667c14a 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -62,7 +62,6 @@ import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.GoalBuilder; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinServiceException; -import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; import gov.nasa.jpl.aerie.scheduler.server.services.SchedulerAgent; @@ -118,8 +117,8 @@ public void schedule( final var specification = specificationService.getSpecification(request.specificationId()); final var planMetadata = merlinService.getPlanMetadata(specification.planId()); - ensureRequestIsCurrent(request); ensurePlanRevisionMatch(specification, planMetadata.planRev()); + ensureRequestIsCurrent(specification, request); //create scheduler problem seeded with initial plan final var schedulerMissionModel = loadMissionModel(planMetadata); final var planningHorizon = new PlanningHorizon( @@ -359,17 +358,19 @@ private long getMerlinPlanRev(final PlanId planId) { return merlinService.getPlanRevision(planId); } + /** - * confirms that specification revision still matches that expected by the scheduling request + * confirms that the scheduling request is still relevant + * (spec hasn't been updated between request being made and now) * * @param request the original request for scheduling, containing an intended starting specification revision * @throws ResultsProtocolFailure when the requested specification revision does not match the actual revision */ - private void ensureRequestIsCurrent(final ScheduleRequest request) throws NoSuchSpecificationException { - final var currentRevisionData = specificationService.getSpecificationRevisionData(request.specificationId()); - if (currentRevisionData.matches(request.specificationRev()) instanceof final RevisionData.MatchResult.Failure failure) { - throw new ResultsProtocolFailure("schedule specification with id %s is stale: %s".formatted( - request.specificationId(), failure)); + private void ensureRequestIsCurrent(final Specification specification, final ScheduleRequest request) + throws NoSuchSpecificationException { + if (specification.specificationRevision() != request.specificationRev().specificationRevision()) { + throw new ResultsProtocolFailure("schedule specification with id %s is no longer at revision %d".formatted( + request.specificationId(), request.specificationRev().specificationRevision())); } } From 3ef94c620c6d006bc2ef842e16ef0c3204a8cedf Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Wed, 31 Jan 2024 14:50:24 -0800 Subject: [PATCH 153/159] Add E2E Tests - Update queries used in tests - Collapse "insert goal" and "add goal to scheduling spec" into a single function --- .../nasa/jpl/aerie/e2e/SchedulingTests.java | 179 +++++++++++++----- .../gov/nasa/jpl/aerie/e2e/utils/GQL.java | 67 ++++--- .../jpl/aerie/e2e/utils/HasuraRequests.java | 97 ++++++---- 3 files changed, 232 insertions(+), 111 deletions(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java index 38c4ea588a..2955842836 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java @@ -11,11 +11,13 @@ import gov.nasa.jpl.aerie.e2e.utils.HasuraRequests; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.opentest4j.AssertionFailedError; import javax.json.Json; import javax.json.JsonObject; @@ -143,11 +145,11 @@ private void insertActivities() throws IOException { @Test void twoInARow() throws IOException { // Setup: Add Goal - final int bakeBananaBreadGoalId = hasura.insertSchedulingGoal( + final int bakeBananaBreadGoalId = hasura.createSchedulingSpecGoal( "BakeBanana Scheduling Test Goal", - modelId, - bakeBananaGoalDefinition); - hasura.createSchedulingSpecGoal(bakeBananaBreadGoalId, schedulingSpecId, 0); + bakeBananaGoalDefinition, + schedulingSpecId, + 0); try { // Schedule and get Plan hasura.awaitScheduling(schedulingSpecId); @@ -174,14 +176,14 @@ void getSchedulingDSLTypeScript() throws IOException { @Test void schedulingRecurrenceGoal() throws IOException { // Setup: Add Goal - final int recurrenceGoalId = hasura.insertSchedulingGoal( + final int recurrenceGoalId = hasura.createSchedulingSpecGoal( "Recurrence Scheduling Test Goal", - modelId, - recurrenceGoalDefinition); - hasura.createSchedulingSpecGoal(recurrenceGoalId, schedulingSpecId, 0); + recurrenceGoalDefinition, + schedulingSpecId, + 0); try { // Schedule and get Plan - hasura.awaitScheduling(schedulingSpecId); + hasura.awaitScheduling(schedulingSpecId, 100000000); final var plan = hasura.getPlan(planId); final var activities = plan.activityDirectives(); @@ -198,11 +200,11 @@ void schedulingRecurrenceGoal() throws IOException { void schedulingCoexistenceGoal() throws IOException { // Setup: Add Goal and Activities insertActivities(); - final int coexistenceGoalId = hasura.insertSchedulingGoal( + final int coexistenceGoalId = hasura.createSchedulingSpecGoal( "Coexistence Scheduling Test Goal", - modelId, - coexistenceGoalDefinition); - hasura.createSchedulingSpecGoal(coexistenceGoalId, schedulingSpecId, 0); + coexistenceGoalDefinition, + schedulingSpecId, + 0); try { // Schedule and get Plan @@ -235,16 +237,16 @@ void schedulingCoexistenceGoal() throws IOException { void schedulingMultipleGoals() throws IOException { // Setup: Add Goals insertActivities(); - final int recurrenceGoalId = hasura.insertSchedulingGoal( + final int recurrenceGoalId = hasura.createSchedulingSpecGoal( "Recurrence Scheduling Test Goal", - modelId, - recurrenceGoalDefinition); - hasura.createSchedulingSpecGoal(recurrenceGoalId, schedulingSpecId, 0); - final int coexistenceGoalId = hasura.insertSchedulingGoal( + recurrenceGoalDefinition, + schedulingSpecId, + 0); + final int coexistenceGoalId = hasura.createSchedulingSpecGoal( "Coexistence Scheduling Test Goal", - modelId, - coexistenceGoalDefinition); - hasura.createSchedulingSpecGoal(coexistenceGoalId, schedulingSpecId, 1); + coexistenceGoalDefinition, + schedulingSpecId, + 1); try { // Schedule and get Plan hasura.awaitScheduling(schedulingSpecId); @@ -360,11 +362,11 @@ void outdatedPlanRevision() throws IOException { hasura.insertActivity(planId, "GrowBanana", "5h", JsonObject.EMPTY_JSON_OBJECT); // Setup: Add Goal - final int coexistenceGoalId = hasura.insertSchedulingGoal( + final int coexistenceGoalId = hasura.createSchedulingSpecGoal( "Coexistence Scheduling Test Goal", - modelId, - coexistenceGoalDefinition); - hasura.createSchedulingSpecGoal(coexistenceGoalId, schedulingSpecId, 0); + coexistenceGoalDefinition, + schedulingSpecId, + 0); try { hasura.updatePlanRevisionSchedulingSpec(planId); @@ -411,11 +413,11 @@ void outdatedSimConfig() throws IOException { hasura.awaitSimulation(planId); hasura.deleteSimTemplate(templateId); // Return to blank sim config args - final int plantGoal = hasura.insertSchedulingGoal( + final int plantGoal = hasura.createSchedulingSpecGoal( "Scheduling Test: When Plant < 300", - modelId, - plantCountGoalDefinition); - hasura.createSchedulingSpecGoal(plantGoal, schedulingSpecId, 0); + plantCountGoalDefinition, + schedulingSpecId, + 0); try { hasura.awaitScheduling(schedulingSpecId); @@ -458,11 +460,11 @@ void injectedResultsLoaded() throws IOException{ List.of(new ProfileSegment("0h", false, Json.createValue(400)))); // Insert Goal - final int plantGoal = hasura.insertSchedulingGoal( + final int plantGoal = hasura.createSchedulingSpecGoal( "Scheduling Test: When Plant < 300", - modelId, - plantCountGoalDefinition); - hasura.createSchedulingSpecGoal(plantGoal, schedulingSpecId, 0); + plantCountGoalDefinition, + schedulingSpecId, + 0); try { hasura.awaitScheduling(schedulingSpecId); @@ -486,11 +488,11 @@ void temporalSubsetExcluded() throws IOException { hasura.awaitSimulation(planId); // Setup: Add Goal - final int coexistenceGoalId = hasura.insertSchedulingGoal( + final int coexistenceGoalId = hasura.createSchedulingSpecGoal( "Coexistence Scheduling Test Goal", - modelId, - coexistenceGoalDefinition); - hasura.createSchedulingSpecGoal(coexistenceGoalId, schedulingSpecId, 0); + coexistenceGoalDefinition, + schedulingSpecId, + 0); try { // Schedule and get Plan @@ -544,16 +546,16 @@ void beforeEach() throws IOException { false); // Add Goal - cardinalityGoalId = hasura.insertSchedulingGoal( + cardinalityGoalId = hasura.createSchedulingSpecGoal( "Cardinality and Decomposition Scheduling Test Goal", - modelId, """ export default function cardinalityGoalExample() { return Goal.CardinalityGoal({ activityTemplate: ActivityTemplates.parent({ label: "unlabeled"}), specification: { duration: Temporal.Duration.from({ seconds: 10 }) }, - });}"""); - hasura.createSchedulingSpecGoal(cardinalityGoalId, schedulingSpecId, 0); + });}""", + schedulingSpecId, + 0); } @AfterEach @@ -632,9 +634,8 @@ void beforeEach() throws IOException { List.of(myBooleanProfile)); // Insert Goal - edGoalId = hasura.insertSchedulingGoal( + edGoalId = hasura.createSchedulingSpecGoal( "On my_boolean true", - modelId, """ export default function myGoal() { return Goal.CoexistenceGoal({ @@ -642,9 +643,9 @@ export default function myGoal() { activityTemplate: ActivityTemplates.BiteBanana({ biteSize: 1, }), startsAt:TimingConstraint.singleton(WindowProperty.END) }) - }"""); - // Add the goal - hasura.createSchedulingSpecGoal(edGoalId, schedulingSpecId, 0); + }""", + schedulingSpecId, + 0); } @AfterEach @@ -730,7 +731,7 @@ void beforeEach() throws IOException, InterruptedException { gateway.uploadFooJar(), "Foo (e2e tests)", "aerie_e2e_tests", - "Simulation Tests"); + "Scheduling Tests"); } // Insert the Plan fooPlan = hasura.createPlan( @@ -749,17 +750,17 @@ void beforeEach() throws IOException, InterruptedException { false); // Add Goal - fooGoalId = hasura.insertSchedulingGoal( + fooGoalId = hasura.createSchedulingSpecGoal( "Foo Recurrence Test Goal", - fooId, """ export default function recurrenceGoalExample() { return Goal.ActivityRecurrenceGoal({ activityTemplate: ActivityTemplates.bar(), interval: Temporal.Duration.from({ hours: 2 }), }); - }"""); - hasura.createSchedulingSpecGoal(fooGoalId, fooSchedulingSpecId, 0); + }""", + fooSchedulingSpecId, + 0); } @AfterEach @@ -795,4 +796,80 @@ void cancelingSchedulingUpdatesRequestReason() throws IOException { assertEquals("Scheduling was interrupted while "+ reasonData.getString("location"), reasonData.getString("message")); } } + + @Nested + class VersioningSchedulingGoals { + @Test + void goalVersionLocking() throws IOException { + final int goalId = hasura.createSchedulingSpecGoal( + "coexistence goal", + coexistenceGoalDefinition, + schedulingSpecId, + 0); + + try { + // Update the plan's constraint specification to use a specific version + hasura.updateSchedulingSpecVersion(schedulingSpecId, goalId, 0); + + // Update definition to have invalid syntax + final int newRevision = hasura.updateGoalDefinition( + goalId, + "error :-("); + + // Schedule -- should succeed + final var initResults = hasura.awaitScheduling(schedulingSpecId); + assertEquals("complete", initResults.status()); + + // Update scheduling spec to use invalid definition + hasura.updateSchedulingSpecVersion(schedulingSpecId, goalId, newRevision); + + // Schedule -- should fail + final var error = Assertions.assertThrows( + AssertionFailedError.class, + () -> hasura.awaitScheduling(schedulingSpecId)); + final var expectedMsg = "Scheduling returned bad status failed with reason {\"data\":[{\"errors\":[" + + "{\"location\":{\"column\":1,\"line\":1},\"message\":\"TypeError: TS2306 No default " + + "export. Expected a default export function with the signature:"; + if (!error.getMessage().contains(expectedMsg)) { + throw error; + } + } finally { + hasura.deleteSchedulingGoal(goalId); + } + } + + @Test + void schedulingIgnoreDisabledGoals() throws IOException { + // Add a problematic goal to the spec, then disable it + final int problemGoalId = hasura.createSchedulingSpecGoal( + "bad goal", + "error :-(", + "Goal that won't compile", + schedulingSpecId, + 0); + try { + hasura.updateSchedulingSpecEnabled(schedulingSpecId, problemGoalId, false); + + // Schedule -- Validate that the plan didn't change + hasura.awaitScheduling(schedulingSpecId); + assertEquals(0, hasura.getPlan(planId).activityDirectives().size()); + + // Enable disabled constraint + hasura.updateSchedulingSpecEnabled(schedulingSpecId, problemGoalId, true); + + // Schedule -- Assert Fail + final var error = Assertions.assertThrows( + AssertionFailedError.class, + () -> hasura.awaitScheduling(schedulingSpecId)); + final var expectedMsg = "Scheduling returned bad status failed with reason {\"data\":[{\"errors\":[" + + "{\"location\":{\"column\":1,\"line\":1},\"message\":\"TypeError: TS2306 No default " + + "export. Expected a default export function with the signature:"; + if (!error.getMessage().contains(expectedMsg)) { + throw error; + } + } finally { + hasura.deleteSchedulingGoal(problemGoalId); + } + } + } } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index a2dda9d2d1..fd9b550dd1 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -27,15 +27,13 @@ mutation AssignTemplateToSimulation($simulation_id: Int!, $simulation_template_i }"""), CANCEL_SCHEDULING(""" mutation cancelScheduling($analysis_id: Int!) { - update_scheduling_request(where: {analysis_id: {_eq: $analysis_id}}, _set: {canceled: true}) { - returning { - analysis_id - specification_id - specification_revision - canceled - reason - status - } + update_scheduling_request_by_pk(pk_columns: {analysis_id: $analysis_id}, _set: {canceled: true}) { + analysis_id + specification_id + specification_revision + canceled + reason + status } }"""), CANCEL_SIMULATION(""" @@ -97,21 +95,6 @@ mutation CreatePlan($plan: plan_insert_input!) { revision } }"""), - CREATE_SCHEDULING_GOAL(""" - mutation CreateSchedulingGoal($goal: scheduling_goal_insert_input!) { - goal: insert_scheduling_goal_one(object: $goal) { - author - created_date - definition - description - id - last_modified_by - model_id - modified_date - name - revision - } - }"""), CREATE_SCHEDULING_SPEC_GOAL(""" mutation CreateSchedulingSpecGoal($spec_goal: scheduling_specification_goals_insert_input!) { insert_scheduling_specification_goals_one(object: $spec_goal) { @@ -184,9 +167,11 @@ mutation DeletePlan($id: Int!) { }"""), DELETE_SCHEDULING_GOAL(""" mutation DeleteSchedulingGoal($goalId: Int!) { - delete_scheduling_goal_by_pk(id: $goalId) { + delete_scheduling_specification_goals(where: {goal_id: {_eq: $goalId}}){ + affected_rows + } + delete_scheduling_goal_metadata_by_pk(id: $goalId) { name - definition } }"""), DELETE_SIMULATION_PRESET(""" @@ -374,8 +359,8 @@ query GetSchedulingDslTypeScript($missionModelId: Int!, $planId: Int) { } }"""), GET_SCHEDULING_REQUEST(""" - query GetSchedulingRequest($specificationId: Int!, $specificationRev: Int!) { - scheduling_request_by_pk(specification_id: $specificationId, specification_revision: $specificationRev) { + query GetSchedulingRequest($analysisId: Int!) { + scheduling_request_by_pk(analysis_id: $analysisId) { specification_id specification_revision analysis_id @@ -561,6 +546,12 @@ mutation updateConstraintSpecVersion($plan_id: Int!, $constraint_id: Int!, $enab enabled } }"""), + UPDATE_GOAL_DEFINITION(""" + mutation updateGoalDefinition($goal_id: Int!, $definition: String!) { + definition: insert_scheduling_goal_definition_one(object: {goal_id: $goal_id, definition: $definition}) { + revision + } + }"""), UPDATE_ROLE_ACTION_PERMISSIONS(""" mutation updateRolePermissions($role: user_roles_enum!, $action_permissions: jsonb!) { permissions: update_user_role_permission_by_pk( @@ -570,6 +561,26 @@ mutation updateRolePermissions($role: user_roles_enum!, $action_permissions: jso action_permissions } }"""), + UPDATE_SCHEDULING_SPEC_GOALS_ENABLED(""" + mutation updateSchedulingSpecGoalVersion($spec_id: Int!, $goal_id: Int!, $enabled: Boolean!) { + update_scheduling_specification_goals_by_pk( + pk_columns: {specification_id: $spec_id, goal_id: $goal_id}, + _set: {enabled: $enabled}) + { + goal_revision + enabled + } + }"""), + UPDATE_SCHEDULING_SPEC_GOALS_VERSION(""" + mutation updateSchedulingSpecGoalVersion($spec_id: Int!, $goal_id: Int!, $goal_revision: Int!) { + update_scheduling_specification_goals_by_pk( + pk_columns: {specification_id: $spec_id, goal_id: $goal_id}, + _set: {goal_revision: $goal_revision}) + { + goal_revision + enabled + } + }"""), UPDATE_SCHEDULING_SPECIFICATION_PLAN_REVISION(""" mutation updateSchedulingSpec($planId: Int!, $planRev: Int!) { update_scheduling_specification( diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java index 6d66ea2333..29f55cff88 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/HasuraRequests.java @@ -455,18 +455,12 @@ private SchedulingResponse schedule(int schedulingSpecId) throws IOException { private SchedulingRequest cancelSchedulingRun(int analysisId, int timeout) throws IOException { final var variables = Json.createObjectBuilder().add("analysis_id", analysisId).build(); - //assert that we only canceled one task - final var cancelRequest = makeRequest(GQL.CANCEL_SCHEDULING, variables) - .getJsonObject("update_scheduling_request") - .getJsonArray("returning"); - assertEquals(1, cancelRequest.size()); - final int specId = cancelRequest.getJsonObject(0).getInt("specification_id"); - final int specRev = cancelRequest.getJsonObject(0).getInt("specification_revision"); + makeRequest(GQL.CANCEL_SCHEDULING, variables); for(int i = 0; i Date: Fri, 1 Mar 2024 09:15:33 -0800 Subject: [PATCH 154/159] Restore dropped relationship "analyses" from scheduling goal tables --- .../tables/public_scheduling_goal_definition.yaml | 9 +++++++++ .../tables/public_scheduling_goal_metadata.yaml | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml index 9f57b83529..d6cd74353e 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_definition.yaml @@ -6,6 +6,15 @@ object_relationships: using: foreign_key_constraint_on: goal_id array_relationships: + - name: analyses + using: + foreign_key_constraint_on: + columns: + - goal_id + - goal_revision + table: + name: scheduling_goal_analysis + schema: public - name: tags using: foreign_key_constraint_on: diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml index ddfe1b6a0b..cbd9c47f25 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_goal_metadata.yaml @@ -2,6 +2,14 @@ table: name: scheduling_goal_metadata schema: public array_relationships: + - name: analyses + using: + manual_configuration: + column_mapping: + id: goal_id + remote_table: + name: scheduling_goal_analysis + schema: public - name: tags using: foreign_key_constraint_on: From ca5ecf9d049f085142249e8d31e12bc0b4fcb625 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 5 Mar 2024 09:02:02 -0800 Subject: [PATCH 155/159] Restore dropped permission to insert/update "simulate_after" in the scheduling spec --- .../tables/public_scheduling_specification_goals.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml index c71679a788..8af1f31c22 100644 --- a/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml +++ b/deployment/hasura/metadata/databases/AerieScheduler/tables/public_scheduling_specification_goals.yaml @@ -33,20 +33,20 @@ select_permissions: insert_permissions: - role: aerie_admin permission: - columns: [specification_id, goal_id, goal_revision, priority, enabled] + columns: [specification_id, goal_id, goal_revision, priority, enabled, simulate_after] check: {} - role: user permission: - columns: [specification_id, goal_id, goal_revision, priority, enabled] + columns: [specification_id, goal_id, goal_revision, priority, enabled, simulate_after] check: {} update_permissions: - role: aerie_admin permission: - columns: [goal_revision, priority, enabled] + columns: [goal_revision, priority, enabled, simulate_after] filter: {} - role: user permission: - columns: [goal_revision, priority, enabled] + columns: [goal_revision, priority, enabled, simulate_after] filter: {} delete_permissions: - role: aerie_admin From 156b173041b03b8becc26cb416a8eaad2dff331c Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Tue, 19 Mar 2024 16:16:18 -0700 Subject: [PATCH 156/159] enable continuous validation thread by default --- .../java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java index 0da35c7219..81b0289bab 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/AerieAppDriver.java @@ -180,7 +180,7 @@ private static AppConfiguration loadConfiguration() { Instant.parse(getEnv("UNTRUE_PLAN_START", "")), URI.create(getEnv("HASURA_GRAPHQL_URL", "http://localhost:8080/v1/graphql")), getEnv("HASURA_GRAPHQL_ADMIN_SECRET", ""), - Boolean.parseBoolean(getEnv("ENABLE_CONTINUOUS_VALIDATION_THREAD", "false")), + Boolean.parseBoolean(getEnv("ENABLE_CONTINUOUS_VALIDATION_THREAD", "true")), Integer.parseInt(getEnv("VALIDATION_THREAD_POLLING_PERIOD", "500")) ); } From 53c20bd7913c755653bee3ae9fe69a65e6478888 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Tue, 19 Mar 2024 16:19:02 -0700 Subject: [PATCH 157/159] remove `ENABLE_CONTINUOUS_VALIDATION_THREAD: true` from docker-compose.yml this is now the default behavior --- deployment/docker-compose.yml | 1 - docker-compose.yml | 1 - e2e-tests/docker-compose-test.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 2733516e79..18f0d4748e 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -35,7 +35,6 @@ services: MERLIN_DB_USER: "${AERIE_USERNAME}" MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store MERLIN_PORT: 27183 - ENABLE_CONTINUOUS_VALIDATION_THREAD: true JAVA_OPTS: > -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN -Dorg.slf4j.simpleLogger.logFile=System.err diff --git a/docker-compose.yml b/docker-compose.yml index a1c323f1ad..9e1f6a80cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,6 @@ services: -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err UNTRUE_PLAN_START: "2000-01-01T11:58:55.816Z" - ENABLE_CONTINUOUS_VALIDATION_THREAD: true image: aerie_merlin ports: ["27183:27183", "5005:5005"] restart: always diff --git a/e2e-tests/docker-compose-test.yml b/e2e-tests/docker-compose-test.yml index 14a3fa991b..4d2c8f1867 100644 --- a/e2e-tests/docker-compose-test.yml +++ b/e2e-tests/docker-compose-test.yml @@ -46,7 +46,6 @@ services: -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err UNTRUE_PLAN_START: '2000-01-01T11:58:55.816Z' - ENABLE_CONTINUOUS_VALIDATION_THREAD: true image: aerie_merlin ports: ['27183:27183', '5005:5005'] restart: always From ea4ab70921626425afe8f32a27cb29d58e1fce54 Mon Sep 17 00:00:00 2001 From: David Legg Date: Thu, 1 Feb 2024 09:52:56 -0800 Subject: [PATCH 158/159] Use dynamics in LBCS, not resources The LinearBoundaryConsistencySolver, a linear solver for polynomial resources driven by a boundary-consistency algorithm, currently computes its results using resources. This involves some more indirection than is necessary. By changing LBCS to use dynamics instead of resources, we avoid that extra overhead, and improve performance somewhat. --- .../polynomial/LinearBoundaryConsistencySolver.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java index 11515d3be9..8eea6498ef 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java @@ -121,7 +121,7 @@ private void solve() { while ((constraint = remainingConstraints.poll()) != null) { var V = constraint.constrainedVariable; var D = domains.get(V); - var newBound = constraint.bound.apply(domains).getDynamics().getOrThrow(); + var newBound = constraint.bound.apply(domains); boolean domainChanged = switch (constraint.comparison) { case LessThanOrEquals -> D.restrictUpper(newBound); case GreaterThanOrEquals -> D.restrictLower(newBound); @@ -313,23 +313,23 @@ private Stream directionalConstraints(Variable constraine // Expiry for driven terms is captured by re-solving rather than expiring the solution. // If solver has a feedback loop from last iteration (which is common) // feeding that expiry in here can loop the solver forever. - var result = drivenTerm; + var result = drivenTerm.getDynamics().getOrThrow(); for (var drivingVariable : drivingVariables) { var scale = controlledTerm.get(drivingVariable); var domain = domains.get(drivingVariable); var useLowerBound = (scale > 0) == (c == LessThanOrEquals); var domainBound = ExpiringMonad.map( useLowerBound ? domain.lowerBound() : domain.upperBound(), - b -> b.multiply(polynomial(-scale))); - result = add(result, () -> success(domainBound)); + polynomial(-scale)::multiply); + result = ExpiringMonad.map(result, domainBound, Polynomial::add); } - return multiply(result, constant(inverseScale)); + return ExpiringMonad.map(result, polynomial(inverseScale)::multiply); }, drivingVariables)); } } // Directional constraints are useful for arc consistency, since they have input (driving) and output (constrained) variables. // However, many directional constraints are required in general to express one General constraint. - private record DirectionalConstraint(Variable constrainedVariable, InequalityComparison comparison, Function, Resource> bound, Set drivingVariables) {} + private record DirectionalConstraint(Variable constrainedVariable, InequalityComparison comparison, Function, Expiring> bound, Set drivingVariables) {} public static final class Domain { public final Variable variable; From f719c32c469f15e59f8e21286f2724bb25263724 Mon Sep 17 00:00:00 2001 From: joswig Date: Wed, 20 Mar 2024 17:33:25 +0000 Subject: [PATCH 159/159] Release v2.6.0 --- gradle.properties | 2 +- sequencing-server/package-lock.json | 4 ++-- sequencing-server/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 73e2c71b4d..8a311af462 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ publishing.version= # Override for releases # Change the version number here -version.number=2.5.0 +version.number=2.6.0 # If you are publishing a release *manually* (i.e. not through github actions), # override this on the command line with `./gradlew publish -Pversion.isRelease=true`. diff --git a/sequencing-server/package-lock.json b/sequencing-server/package-lock.json index 49a80b1c0a..b0501f68a3 100644 --- a/sequencing-server/package-lock.json +++ b/sequencing-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "sequencing-server", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sequencing-server", - "version": "2.5.0", + "version": "2.6.0", "license": "MIT", "dependencies": { "@js-temporal/polyfill": "~0.4.3", diff --git a/sequencing-server/package.json b/sequencing-server/package.json index 6dfaa2f26f..8129c7a724 100644 --- a/sequencing-server/package.json +++ b/sequencing-server/package.json @@ -1,6 +1,6 @@ { "name": "sequencing-server", - "version": "2.5.0", + "version": "2.6.0", "description": "Aerie sequencing server", "type": "module", "license": "MIT",

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 v1gNV{Ja+P3=G?JM2r6ed^egy From f388abacc1f91995faf3681a4d038c62ee261f66 Mon Sep 17 00:00:00 2001 From: Ryan Goetz Date: Wed, 28 Feb 2024 13:05:14 -1000 Subject: [PATCH 075/159] Allow for the passing of 'null' value Establishing a default behavior to gracefully handle schemas with null values. Instead of encountering server-side errors that disrupt sequence expansion, null values will now be implicitly passed through. This decision places the onus on the expansion logic to implement appropriate null checks and handle potential null cases accordingly. --- .../batchLoaders/simulatedActivityBatchLoader.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts index e5915ec8ec..074b7258d5 100644 --- a/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts +++ b/sequencing-server/src/lib/batchLoaders/simulatedActivityBatchLoader.ts @@ -231,6 +231,9 @@ export function mapGraphQLActivityInstance( function convertType(value: any, schema: Schema): any { switch (schema.type) { case SchemaTypes.Int: + if (value === null) { + return value; + } if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) { return value.toString(); } @@ -238,7 +241,10 @@ function convertType(value: any, schema: Schema): any { case SchemaTypes.Real: return value; case SchemaTypes.Duration: - return Temporal.Duration.from(parse(value).toISOString()); + if (value !== null) { + return Temporal.Duration.from(parse(value).toISOString()); + } + return value; case SchemaTypes.Boolean: return value; case SchemaTypes.Path: @@ -246,15 +252,21 @@ function convertType(value: any, schema: Schema): any { case SchemaTypes.String: return value; case SchemaTypes.Series: + if (value === null) { + return value; + } return value.map((value: any) => convertType(value, schema.items)); case SchemaTypes.Struct: + if (value === null) { + return value; + } const struct: { [attributeName: string]: any } = {}; for (const [attributeKey, attributeSchema] of Object.entries(schema.items)) { struct[attributeKey] = convertType(value[attributeKey], attributeSchema); } return struct; case SchemaTypes.Variant: - if (schema.variants.length === 1 && schema.variants[0]?.key === 'VOID') { + if (value === null || (schema.variants.length === 1 && schema.variants[0]?.key === 'VOID')) { return null; } return value; From 70d1aff928c0d2bf11bb63517e1e95c19591553f Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 5 Feb 2024 17:32:09 -0800 Subject: [PATCH 076/159] Make Expiry comparable --- .../java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java index f8242572a9..f7e245fa16 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Expiry.java @@ -8,7 +8,7 @@ /** * The time at which a value expires. */ -public record Expiry(Optional value) { +public record Expiry(Optional value) implements Comparable { public static Expiry NEVER = expiry(Optional.empty()); public static Expiry at(Duration t) { From 06e840c0e2be0206e4c890d2f54a8a7c385339c0 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 5 Feb 2024 17:33:43 -0800 Subject: [PATCH 077/159] Account for expiry in dynamicsChange condition There's an edge case that was missed by dynamicsChange conditions: when a cell is set to a dynamics with the same data, but a later expiry. This was discovered while debugging clamped integrals whose integrands changed from one limited-out value to another. The solver would emit the same effective rate for the integral (0), but with an expiry that changed from zero time to some positive time. By only reacting to the first change, the zero expiry time was propagated forward, but the corrected non-zero time wasn't. --- .../gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java index 3ed1821ec3..998851f285 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/Resources.java @@ -99,9 +99,11 @@ public static > Condition dynamicsChange(Resource re final Duration startTime = currentTime(); Condition result = (positive, atEarliest, atLatest) -> { var currentDynamics = resource.getDynamics(); + var elapsedTime = currentTime().minus(startTime); boolean haveChanged = startingDynamics.match( start -> currentDynamics.match( - current -> !current.data().equals(start.data().step(currentTime().minus(startTime))), + current -> !current.data().equals(start.data().step(elapsedTime)) || + !current.expiry().equals(start.expiry().minus(elapsedTime)), ignored -> true), startException -> currentDynamics.match( ignored -> true, From c783a868ab57e138a9fc244e8b64e10ac05f04ad Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 5 Feb 2024 17:39:13 -0800 Subject: [PATCH 078/159] Take expiry into account for approximation interval When solving for an interval over which to take an approximation, we take the expiry of the dynamics to be approximated into account. In the case when no solution is found, we should still take expiry into account. This is especially important when that expiry is much shorter than the typical approximation interval. In this case, we can often choose the full expiry as the approximation interval without reaching our maximum error tolerance, resulting in a NoBracketingException from the solver. In this case, we should choose the expiry, not the maximum approximation interval, to be consistent with the time range we searched. --- .../modeling/black_box/IntervalFunctions.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java index 3024d654d9..6e91504987 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/black_box/IntervalFunctions.java @@ -59,23 +59,26 @@ public static > Function, Duration> byBound exp.data(), t, maximumError)); + var effectiveMinSamplePeriod = Duration.min(e, minimumSamplePeriod); + var effectiveMaxSamplePeriod = Duration.min(e, maximumSamplePeriod); + try { double intervalSize = solver.solve( 100, errorFn, - Duration.min(e, minimumSamplePeriod).ratioOver(SECOND), - Duration.min(e, maximumSamplePeriod).ratioOver(SECOND)); + effectiveMinSamplePeriod.ratioOver(SECOND), + effectiveMaxSamplePeriod.ratioOver(SECOND)); return Duration.roundNearest(intervalSize, SECOND); } catch (NoBracketingException x) { if (errorFn.value(minimumSamplePeriod.ratioOver(SECOND)) > 0) { // maximum error > estimated error, best case - return maximumSamplePeriod; + return effectiveMaxSamplePeriod; } else { // maximum error < estimated error, worst case - return minimumSamplePeriod; + return effectiveMinSamplePeriod; } } catch (TooManyEvaluationsException | NumberIsTooLargeException x) { - return minimumSamplePeriod; + return effectiveMinSamplePeriod; } }; } From 8f9ebbda9243c5b55d8f5e06c8af8cc76823af84 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 5 Feb 2024 17:53:56 -0800 Subject: [PATCH 079/159] Prevent overflow in Polynomial root-finding --- .../contrib/streamline/modeling/polynomial/Polynomial.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java index 7e00dd83a9..ea8db7c3ff 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java @@ -195,7 +195,7 @@ private Expiry findExpiryNearRoot(Predicate expires) { // Do a binary search to find the exact transition time while (end.longerThan(start.plus(EPSILON))) { - Duration midpoint = start.plus(end).dividedBy(2); + Duration midpoint = start.plus(end.minus(start).dividedBy(2)); if (expires.test(midpoint)) { end = midpoint; } else { From 762a534a51a5d5f90425c0bccf625f804acb88b2 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 5 Feb 2024 17:54:22 -0800 Subject: [PATCH 080/159] Improve performance and stability for Polynomial root-finding Improve performance of polynomial root-finding by solving linear polynomials directly, instead of handing off to the Laguerre solver. For nonlinear polynomials, improve the stability of root-finding for polynomials with very large or very small coefficients by first conditioning the problem, normalizing the constant coefficient to 1. --- .../modeling/polynomial/Polynomial.java | 27 ++++++- .../modeling/polynomial/ComparisonsTest.java | 74 +++++++++++++++++++ 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java index ea8db7c3ff..bc7be13927 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/Polynomial.java @@ -256,19 +256,38 @@ public Expiring max(Polynomial other) { * Finds all roots of this function in the future */ private Stream findFutureRoots() { + // TODO: In some sense, isn't having an infinite coefficient the same as a vertical line, + // hence the same as having a root at x = 0? + // Unless the value itself is non-finite, that is... // If this polynomial can never have a root, fail immediately if (this.isNonFinite() || this.isConstant()) { return Stream.empty(); } - // Defining epsilon keeps the Laguerre solver fast and stable for poorly-behaved polynomials. - final double epsilon = 2 * Arrays.stream(coefficients).map(Math::ulp).max().orElseThrow(); + if (coefficients[0] == 0.0) { + return Stream.of(ZERO); + } + + // If the polynomial is linear, solve it analytically for performance + if (this.degree() <= 1) { + double t = -getCoefficient(0) / getCoefficient(1); + if (t >= -ABSOLUTE_ACCURACY_FOR_DURATIONS / 2 && t <= MAX_SECONDS_FOR_DURATION) { + return Stream.of(Duration.roundNearest(t, SECOND)); + } else { + return Stream.empty(); + } + } + + // Condition the problem by dividing through by the first coefficient: + double[] conditionedCoefficients = Arrays.stream(coefficients).map(c -> c / coefficients[0]).toArray(); + // Defining epsilon keeps the Laguerre solver faster and more stable for poorly-behaved polynomials. + final double epsilon = 2 * Arrays.stream(conditionedCoefficients).map(Math::ulp).max().orElseThrow(); final Complex[] solutions = new LaguerreSolver(0, ABSOLUTE_ACCURACY_FOR_DURATIONS, epsilon) - .solveAllComplex(coefficients, 0); + .solveAllComplex(conditionedCoefficients, 0); return Arrays.stream(solutions) .filter(solution -> Math.abs(solution.getImaginary()) < epsilon) .map(Complex::getReal) - .filter(t -> t >= 0 && t <= MAX_SECONDS_FOR_DURATION) + .filter(t -> t >= -ABSOLUTE_ACCURACY_FOR_DURATIONS / 2 && t <= MAX_SECONDS_FOR_DURATION) .sorted() .map(t -> Duration.roundNearest(t, SECOND)); } diff --git a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java index f707482e72..a7cad57998 100644 --- a/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java +++ b/contrib/src/test/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/ComparisonsTest.java @@ -299,6 +299,80 @@ void comparing_converging_nonlinear_terms_with_fine_precision() { check_extrema(false, true); } + // Unrepresentable convergence: + // These tests reflect polynomials that in theory converge, but do so in timespans + // that are too large to represent. Thus, they should be treated as non-converging. + + @Test + void comparing_linear_terms_with_convergence_unrepresentable_by_double() { + setup(() -> { + set(p, polynomial(Double.MAX_VALUE)); + set(q, polynomial(0, 0.1)); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, false, false); + check_comparison(p_gt_q, true, false); + check_comparison(p_gte_q, true, false); + check_extrema(true, false); + } + + @Test + void comparing_linear_terms_with_convergence_unrepresentable_by_duration() { + setup(() -> { + set(p, polynomial(Duration.MAX_VALUE.ratioOver(SECOND))); + set(q, polynomial(0, 0.1)); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, false, false); + check_comparison(p_gt_q, true, false); + check_comparison(p_gte_q, true, false); + check_extrema(true, false); + } + + @Test + void comparing_nonlinear_terms_with_convergence_unrepresentable_by_double() { + setup(() -> { + set(p, polynomial(Double.MAX_VALUE)); + set(q, polynomial(0, 0, 0.1)); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, false, false); + check_comparison(p_gt_q, true, false); + check_comparison(p_gte_q, true, false); + check_extrema(true, false); + } + + @Test + void comparing_nonlinear_terms_with_convergence_unrepresentable_by_duration() { + setup(() -> { + set(p, polynomial(Duration.MAX_VALUE.ratioOver(SECOND) * Duration.MAX_VALUE.ratioOver(SECOND))); + set(q, polynomial(0, 0, 0.1)); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, false, false); + check_comparison(p_gt_q, true, false); + check_comparison(p_gte_q, true, false); + check_extrema(true, false); + } + + @Test + void comparing_pathological_nonlinear_terms_with_convergence_unrepresentable_by_duration() { + setup(() -> { + set(p, polynomial(Duration.MAX_VALUE.ratioOver(SECOND) * Duration.MAX_VALUE.ratioOver(SECOND))); + set(q, polynomial(0, Duration.MIN_VALUE.ratioOver(SECOND), 1.0 + Math.ulp(1.0))); + }); + + check_comparison(p_lt_q, false, false); + check_comparison(p_lte_q, false, false); + check_comparison(p_gt_q, true, false); + check_comparison(p_gte_q, true, false); + check_extrema(true, false); + } + private void check_comparison(Resource> result, boolean expectedValue, boolean expectCrossover) { reset(); var resultDynamics = result.getDynamics().getOrThrow(); From fac658253faed0f3feca2df2f65c89bd5d42b326 Mon Sep 17 00:00:00 2001 From: David Legg Date: Mon, 5 Feb 2024 18:09:01 -0800 Subject: [PATCH 081/159] Pass expiry through LBCS and clampedIntegrate Removes the use of eraseExpiry in LinearBoundaryConsistencySolver. This was a patch put in place to support feedback loops involving the solver, but it erases too much information. Leaving the expiry in place and erasing it only when actually building a feedback loop lets downstream components like approximations take that expiry into account. Also reconfigures the use of eraseExpiry in clampedIntegrate to pass expiry information through. --- .../polynomial/LinearBoundaryConsistencySolver.java | 3 +-- .../modeling/polynomial/PolynomialResources.java | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java index 69cbdb5c1d..11515d3be9 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/LinearBoundaryConsistencySolver.java @@ -27,7 +27,6 @@ import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiring.neverExpiring; import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever; import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ExpiringMonad.bind; -import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.eraseExpiry; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Context.contextualized; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.getName; @@ -314,7 +313,7 @@ private Stream directionalConstraints(Variable constraine // Expiry for driven terms is captured by re-solving rather than expiring the solution. // If solver has a feedback loop from last iteration (which is common) // feeding that expiry in here can loop the solver forever. - var result = eraseExpiry(drivenTerm); + var result = drivenTerm; for (var drivingVariable : drivingVariables) { var scale = controlledTerm.get(drivingVariable); var domain = domains.get(drivingVariable); diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java index fc12ae44e2..40c26b3ecb 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/modeling/polynomial/PolynomialResources.java @@ -364,6 +364,7 @@ public static ClampedIntegrateResult clampedIntegrate( Resource integrand, Resource lowerBound, Resource upperBound, double startingValue) { LinearBoundaryConsistencySolver rateSolver = new LinearBoundaryConsistencySolver("clampedIntegrate rate solver"); var integral = resource(polynomial(startingValue)); + var neverExpiringIntegral = eraseExpiry(integral); // Solve for the rate as a function of value var overflowRate = rateSolver.variable("overflowRate", Domain::lowerBound); @@ -377,11 +378,11 @@ public static ClampedIntegrateResult clampedIntegrate( // Set up rate clamping conditions var integrandUB = choose( - greaterThanOrEquals(integral, upperBound), + greaterThanOrEquals(neverExpiringIntegral, upperBound), differentiate(upperBound), constant(Double.POSITIVE_INFINITY)); var integrandLB = choose( - lessThanOrEquals(integral, lowerBound), + lessThanOrEquals(neverExpiringIntegral, lowerBound), differentiate(lowerBound), constant(Double.NEGATIVE_INFINITY)); @@ -390,10 +391,10 @@ public static ClampedIntegrateResult clampedIntegrate( // Use a simple feedback loop on volumes to do the integration and clamping. // Clamping here takes care of discrete under-/overflows and overshooting bounds due to discrete time steps. - var clampedCell = clamp(integral, lowerBound, upperBound); + var clampedCell = clamp(neverExpiringIntegral, lowerBound, upperBound); var correctedCell = map(clampedCell, rate.resource(), (v, r) -> r.integral(v.extract())); // Use the corrected integral values to set volumes, but erase expiry information in the process to avoid loops - forward(eraseExpiry(correctedCell), integral); + forward(correctedCell, integral); name(integral, "Clamped Integral (%s)", integrand); name(overflowRate.resource(), "Overflow of %s", integral); From 27525c746f1e5f6adc3053a8d8dec50bed7d3c72 Mon Sep 17 00:00:00 2001 From: David Legg Date: Thu, 1 Feb 2024 09:56:03 -0800 Subject: [PATCH 082/159] Add more utilities for profiling resources. Adds more support for profiling resource performance, especially around mutable resources. Most importantly, it adds the static method `Resource.profileAllResources()`. When called before constructing the model, this method turns on profiling for virtually every resource in the model. This casts a broad net for early stages in a performance investigation, to highlight resources that are called unexpectedly frequently. --- .../streamline/core/MutableResource.java | 35 ++++++++-- .../contrib/streamline/core/Resource.java | 23 ++++++ .../streamline/core/monads/ResourceMonad.java | 28 +++++++- .../contrib/streamline/debugging/Naming.java | 41 +++++++---- .../streamline/debugging/Profiling.java | 70 +++++++++++-------- 5 files changed, 147 insertions(+), 50 deletions(-) diff --git a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java index 2c93aed0c5..fb450d3320 100644 --- a/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java +++ b/contrib/src/main/java/gov/nasa/jpl/aerie/contrib/streamline/core/MutableResource.java @@ -12,6 +12,8 @@ import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects; import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.pure; import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profile; +import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profileEffects; import static java.util.stream.Collectors.joining; /** @@ -62,7 +64,10 @@ private void augmentEffectName(DynamicsEffect effect) { } }; if (MutableResourceFlags.DETECT_BUSY_CELLS) { - result = Profiling.profileEffects(result); + result = profileEffects(result); + } + if (MutableResourceFlags.PROFILE_GET_DYNAMICS) { + result = profile(result); } return result; } @@ -79,12 +84,12 @@ static > void set(MutableResource resource, Expiring * Turn on busy cell detection. * *

p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static Unstructured map(Unstructured a, Unstructured b, Unstructured c, Unstructured d, Unstructured e, Unstructured f, Unstructured g, Unstructured h, Unstructured i, Unstructured j, Unstructured k, Unstructured l, Unstructured m, Unstructured n, Unstructured o, Unstructured

p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static Discrete map(Discrete a, Discrete b, Discrete c, Discrete d, Discrete e, Discrete f, Discrete g, Discrete h, Discrete i, Discrete j, Discrete k, Discrete l, Discrete m, Discrete n, Discrete o, Discrete

p, Function16 function) { + return map(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, curry(function)); + } + + public static ErrorCatching map(ErrorCatching a, ErrorCatching b, ErrorCatching c, ErrorCatching d, ErrorCatching e, ErrorCatching f, ErrorCatching g, ErrorCatching h, ErrorCatching i, ErrorCatching j, ErrorCatching k, ErrorCatching l, ErrorCatching m, ErrorCatching n, ErrorCatching o, ErrorCatching