API Exploration

Spaces

x = Dimension(int, "x")
y = Dimension(int, "y")
z = Dimension(int, "z")
Z = Dimension(int, "z") # Note the capitalization of this variable

# When we try to create a space from our four dimensions, an exception
# throws due to z.name and Z.name being equal
s = Space((x, y, z, Z))

# To resolve this, we can supply a key override dictionary as our
# second parameter
s = Space((x, y, z, Z), names = ["x", "y", "z", "zed"])

# Alternatively, we can first create a space without collisions and
# then append a single dimension with an explict name. By default,
# spaces are immutable and a new space will be created for this reason
# whenever we append a new dimension
s = Space((x, y, z))
s2 = s.append(Z, "zed") # A new space

# Python allows us to reuse variable names so we are able to do:
s = Space((x, y, z))
s = s.append(Z, "zed")

# For maximally explicit declaration of a new space, it can be created
# from a dictionary:
space_dict = {"x": x, "y": y, "z": z, "zed": Z}
s = Space.from_dict(space_dict)

print(s.dimensions.keys()) # ('x', 'y', 'z', 'zed')

# Consider the case of many collisions

a = Dimension(int, "a")
b = Dimension(int, "a")
c = Dimension(int, "a")
d = Dimension(int, "a")

s = Space((a, b, c, d)) # Throws exception

# Fixed with...
s = Space((a, b, c, d), ["a", "my_a", "my_other_a", "my_other_other_a"])

# Or by using the from_dict method which is passed a predetermined dict
# without key/name conflicts
space_dict = {"a": a, "my_a": b, "my_other_a": c, "my_other_other_a": d}
s = Space.from_dict(space_dict)

# A dimensions local name (Dimension.name) will remain 'a' while only its
# key in the dimensions dict of a space will be updated to avoid conflicts
print([d.name for d in s.dimensions]) # ('a', 'a', 'a', 'a')

print(s.dimensions.keys()) # ('a', 'my_a', 'my_other_a', 'my_other_other_a')

# While providing alternative keys/names to our space does avoid conflicts
# it doesn't always result in clarity. It could be confusing to have many
# dimensions with the same local name (Dimension.name). To address this, you
# may override the local name by providing an override boolean a the time of
# space instantiation:
s = Space((a, b, c, d), names = ["a", "my_a", "my_other_a", "my_other_other_a"], override = True)
print(s.my_other_a.name) # "my_other_a"

# The same override option is available with the from_dict() method:
space_dict = {"a": a, "my_a": b, "my_other_a": c, "my_other_other_a": d}
s = Space.from_dict(space_dict, override = True)

print(s.my_other_a.name) # "my_other_a"

Blocks

# A collection of ages will define our initial state space
d1 = Dimension(int, "Alice", "Age of Alice")
d2 = Dimension(int, "Bob", "Age of Bob")
d3 = Dimension(int, "Carol", "Age of Carol")
d4 = Dimension(int, "David", "Age of David")

# And an average of all the above ages will define our updated state space
d5 = Dimension(float, "Average", "Average age")

s1 = Space((d1, d2, d3, d4)) # Represents a domain (initial state space)
s2 = Space((d5)) # Represents a codomain (updated state space)

p1 = Space() # Represents our param space (empty)

def fn(point, codomain):
   return Point(codomain, {"Average": sum(point.values()) / len(point)})

# Domain and codomain arguments can be single space objects or collections of space objects
b1 = Block(fn, s1, s2, p1) # Average age

init_point = Point(s1, {"Alice": 12, "Bob": 54, "Carol": 76, "David": 25}) # Conforms to s1

print(init_point.space == s1) # true
print(init_point.space == s2) # false

next_point = b1.map(init_point)

print(next_point.space == s2) # true
print(next_point.space == s1) # false

Points

# Note: Instantiation of a point MUST validate that supplied point data
# satisfies the schema of the supplied space; if the point data does NOT
# satisfy the space schema, we raise an exception -- otherwise, return
# the point as per normal

# Setup a single-dimension space to create points of
d1 = Dimension(int, "Age", "My Age")
s1 = Space((d1), "My Space", "My Space is a social network lol")

# A point is returned as the supplied point data satisfies s1
p1 = Point(s1, {"Age": 35})

# This would raise an exception since the supplied point data does NOT satisfy s1
p2 = Point(s1, {"Name": "Tyler"})

# The space that was supplied at the time of instantiation should be added to the
# point object
print(p1.space) # <Space>
print(p1.space.name) # 'My Space'
print(p1.space.description) # 'My Space is a social network lol'

# The reason we add the space to the point itself is so that we can easily check
# any point against a space for equality
print(p1.space == s1) # True

s2 = Space(())
print(p1.space == s2) # False

# Because our point data must have satisfied the space schema during instantiation
# we dont need to do any sort of type checks afterwards

print(p1.space.dimensions.Age.dtype == type(p1.Age)) # No need to do this!

# Lastly, we should be able to access all data on the point like so
print(p1.Age) # 35

Trajectories

d1 = Dimension(int, "A")
d2 = Dimension(str, "S")
g3 = Dimension(str, "L")

s1 = Space((d1), "A")
s2 = Space((d1, d2), "A/S")
s3 = Space((d1, d2, d3), "A/S/L")

p1 = Point(s1, {"A": 35})
p2 = Point(s2, {"A": 35, "S": "Male"})

def fn(point, codomain):
   return Point(codomain, {"A": point.A, "S": point.S, "L": "Utah"})

# Create a block that takes an s2 point (we created one called p2 above) and
# adds a hardcoded location of "Utah" to create a new s3 point which is returned.
b1 = Block(fn, s2, s3) # fn, domain, codomain

t1 = Trajectory(p1) # You must initialize with one or more point; at this point t1 can only contain s1 type points
t2 = Trajectory(p2) # Every point in this trajectory must be of type s2

print(t1) # (<point>)

# We can add to our t2 trajectory another s2 point by running a block!
# p2 is a point that conforms to s2; the blocks .map() runs fn() which
# returns a new point that conforms to our codomain of s3
t1.add_point(b1.map(p2)) # The s3 point just added: Point{"A": 35, "S", "Male", "L": "Utah"}

print(t1) # (<point>, <point>)