Table of contents
General Structure
HyperC is composed of a core library hyperc, the relational data interpretation layer hyper-etable, database interoperability hyperc-psql-proxy and a nice Python integration module called ordered
HyperC has a layered approach (see architecture) centered around Python and PDDL.
You define the data in objects of types (or rows of tables in database terminology), define procedures or actions that can modify the data, and the goal state to reach. HyperC will then select the correct procedures to run to get to that state.
You can choose to write procedures directly as pure Python files and run with ordered or you can work with a pre-packaged PostgreSQL interface that offers built-in data management, procedure storage and planning in one package. The ability to write and test procedures in Python may be helpful to construct and debug larger models.
Gentle Intro Example
To understand goal-oriented programming, let’s look at the simplest example in Python:
from hyperc import solve
class Numbers:
i: int
n0 = Numbers()
n0.i = 0
def inc(n: Numbers):
n.i += 1
def goal(n: Numbers):
assert n.i == 3
solve(goal)
print(n0.i) # => 3
This code defines the end goal to solve for a state where some object of type Numbers
will have <object Numbers>.i == 3
. Given is the function that takes object and increments the property i
and one object with a star value .i=0
. The resulting plan when HyperC solves this is:
inc(n0)
inc(n0)
inc(n0)
The same task in the database will look like this:
CREATE TABLE numbers ( id integer PRIMARY KEY, i integer );
INSERT INTO numbers VALUES (0, 0);
CREATE PROCEDURE inc(n numbers) LANGUAGE 'hyperc' AS $$
n.I += 1
$$;
TRANSIT UPDATE numbers SET i = 3;
the TRANSIT UPDATE
command tells HyperC to reach the state of the table numbers
that would have been set by the proposed UPDATE
.
Note that we always have to have some KEY column in the table that will not be modified, so we had to create an additional id
column. Also as databases don’t distinguish column name case, uppercase column names is a requirement.
You will get the output
step_num | proc_name | op_type
-----------+------------+---------
0 | inc | STEP
1 | inc | STEP
2 | inc | STEP
-1 | | UPDATE
and as expected,
SELECT * FROM numbers;
id | i
----+---
0 | 3
(1 row)
we can try to roll back using TRANSIT UPDATE
:
TRANSIT UPDATE numbers SET i = 0;
ERROR: hyperc.exceptions.SchedulingError: Obtained proof that task has no solution
which obviously fails as there is no set of procedures that could be applied to set the column i
to 0
.
Sending a normal UPDATE
will fix the table back to original state:
UPDATE numbers SET i = 0;
UPDATE 1
SELECT * FROM numbers;
id | i
----+---
0 | 0
(1 row)
Procedures Programming
HyperC accepts writing procedures in a subset of Python language. By writing the procedures, you define the allowed transitions within the database. We refer to a complete set of procedures as the “business schema”.
Let’s use this code as an example:
def move_truck(truck: Trucks, waypoint_pair: Waypoints):
assert truck.LOCATION == waypoint_pair.FROM_LOCATION
truck.LOCATION = waypoint_pair.TO_LOCATION
truck.ODOMETER += waypoint_pair.POINTS_DISTANCE
When adding the code to the database, the procedure can be created with:
CREATE PROCEDURE move_truck(t trucks, l location_adjacency)
LANGUAGE 'hyperc'
AS $BODY$
assert truck.LOCATION == waypoint_pair.FROM_LOCATION
truck.LOCATION = waypoint_pair.TO_LOCATION
truck.ODOMETER += waypoint_pair.POINTS_DISTANCE
$BODY$;
Which is almost exact equivalent aside from that it’s not indented.
The line with assert
defines the condition when this function can be run - when objects truck
and waypoint_pair
are related in a way that LOCATION
property of truck
equals FROM_LOCATION
of waypoint_pair. You can think of this constraint as a JOIN between tables Trucks
and Waypoints
but with only a single pair of them taken from the join when a function is executed.
The last two lines update the respective values of a row (object).
There is a limit of what can be done in HyperC’ Python dialect: only plain Python is supported with if/assert, integers, bools and strings.
Query Language
HyperCDB is based on PostgreSQL database v.14 and most functions of the database work as expected.
We extend SQL language with the TRANSIT *
set of commands:
Initializing Tables
TRANSIT INIT
Prepares the database for planning function.
TRANSIT
[ EXPLAIN [ TO table_name1[.column], table_name2, ... ]] TRANSIT UPDATE table_name
SET { column = { expression | DEFAULT } |
( column [, ...] ) = ( { expression | DEFAULT } [, ...] ) } [, ...]
[ WHERE condition ]
Returns
A table with the plan.
Description
TRANSIT UPDATE
initiates transition to the state defined by UPDATE statement with familiar SQL syntax of UPDATE. It returns the table of the plan with unique plan_id that can be remembered and used to query hc_plan
table to recall this plan at any later time.
EXPLAIN TRANSIT ...
- initiates calculation of the plan, stores and outputs the plan table but does not do any actual updates to the state.
EXPLAIN TO *table_name*, ... TRANSIT ...
- instructs the solver to only write down changes to tables (and possibly columns) specified after TO
keyword.
Examples:
Calculate transition plan but only write down odometer
reading, leaving truck at its original location:
EXPLAIN TO trucks.odometer TRANSIT UPDATE trucks SET location = 'Office';
hc_plan table
HyperCDB defines a special table hc_plan
to incrementally store all plans with called procedure names and input/output parameters in JSONB objects.
The purpose of hc_plan table is to easily extract additional information from the plans like tracing the truck travel path, measuring fuel consumption, etc.
When TRANSIT command completes, it outputs the plan table back to the user connection. plan_id
can be extracted and remembered by the client application to select this plan from later.