OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace-rocks / examples / storage_tutorial.py

282 lines · python ref:
#! /usr/bin/env python3
#
# storage_tutorial.py
#
"""
Tutorial on how to use RocksStorageNode to save selectively save
individual Atoms or entire AtomSpaces to disk. This is a fairly basic
demo; the scheme files in this directory provide a larger variety of
examples. As the scheme examples are written in a direct fashion, it
should be fairly obvious how to port those examples to Python, as
desired.
"""

# Boilerplate
from opencog.atomspace import AtomSpace
from opencog.type_constructors import *
from opencog.storage_rocks import *

# AtomSpace cne be given a name; if not supplied, one is autogenerated.
space = AtomSpace("my basic space")
set_thread_atomspace(space)

# Basic Atomese tutorial: a labelled directed graph edge.
# In ASCII graphics:
#
#                      "some edge label"
#    "from vertex" ------------------------> "to vertex"
#
# which in Atomese, becomes
#
#    (Edge (Predicate "some edge label")
#            (List (Item "from vertex") (Item "to vertex")))
#
# and for python, the parens get re-arranged and commas are inserted:
#
#    Edge (Predicate ("some edge label"),
#          List (Item ("from vertex"), Item ("to vertex")))
#
p = Predicate("My collection of URLs")
e = Edge(p,
	List(
		Item("file:///Home Computer/folders/My photo album"),
		Item("Fantastic Sunset on Sunday.jpg")))

print("Here's your data:", e)

# To make the demo more fun, we'll also attach some static data to the
# Predicate. This is done by using set_value to specify a triplet:
# an Atom, a key, and the Value to attach at that key. The Atom can be
# any Atom, and so can the key. By convention, PredicateNodes are used
# for simple keys with simple string names. The Values can be any Value,
# which includes Atoms as a special case.
space.set_value(p, Predicate("floaty things"), FloatValue([1,2,3]))
space.set_value(p, Predicate("stringy thing"), StringValue(["a","bb","king"]))
space.set_value(p, Predicate("atomic thing"), Concept("gadget"))

# The list of keys is readily available:
KeysOf(p).execute()

# Values can be fetched individually:
ValueOf(p, Predicate("atomic thing")).execute()
ValueOf(p, Predicate("stringy thing")).execute()
ValueOf(p, Predicate("floaty things")).execute()

# Note that Values can be gotten directly, while sets must go through
# the space. This requirement comes into play when multiple AtomSpaces
# are in use. Such multiple spaces can be stacked on one-another, or
# kept disjoint; dispatching the setter through the space is required
# for proper copy-on-write semantics, atom hiding and other more
# advanced multi-space operations.

# -------------------------------------------
# AtomSpace contents can be stored and loaded from disk, and sent over
# the net (to other AtomSpaces). All such operations use exactly the
# same API, given by the StorageNode. This demo illustrates the API by
# using it with RocksStorageNode, which is built on top of the RocksDB
# disk database.  Rocks is nice, because it requires no config to use.

# Create a RocksDB StorageNode.
# The rocks:// URL specifies a directory in the local filesystem.
storage = RocksStorage("rocks:///tmp/foo")

# Open a connection to storage using message-passing.
# The StorageNode API works by sending messages to StorageNodes via
# set_value(). All messages are PredicateNodes naming the operation.
space.set_value(storage, Predicate("*-open-*"), VoidValue())

# Store the one and only edge created above.
# (This uses pure Atomese style; see immediately below.)
SetValue(storage, Predicate("*-store-atom-*"), e).execute()

# This could have been done in the same way as before. However, pure
# Atomese style is an important part of working with Atomese, and so
# was illustrated above.)
space.set_value(storage, Predicate("*-store-atom-*"), e)

# Also store the predicate. Although it is implicitly stored, when the
# edge above is stored, it is stored without the attached key-value
# pairs. To store these, the Atom must be explicitly named.
SetValue(storage, Predicate("*-store-atom-*"), p).execute()

# Close the connection to storage.
space.set_value(storage, Predicate("*-close-*"), VoidValue())

# The entire contents of the AtomSpace could have been stored with
# just one simple command:
#    space.set_value(storage, PredicateNode("*-store-atomspace-*"), space)
# This was not done, mostly just to show that one can also do granular
# save and restore.

# -------------------------------------------
# The rest of this demo is about restoring the Atom that was just saved.
# To prove that this works, the AtomSpace will be cleared. To prove that
# it was cleared, look at the contents before and after.

# Define a utility printer
def prt_atomspace_contents(asp) :
	print("AtomSpace contains a total of " + str(len(asp)) + " Atoms")
	if 0 < len(asp) :
		print("These are:")
	count = 0
	for atom in asp:
		count += 1
		print("Atom " + str(count) + ".... " + str(atom))

print("The AtomSpace before clearing:")
prt_atomspace_contents(space)

print("\nWill now clear the AtomSpace.")
space.clear()
print("The AtomSpace size after clearing: ", len(space))
prt_atomspace_contents(space)

# -------------------------------------------
# The clear clobbers the StorageNode; create it again.
storage = RocksStorageNode("rocks:///tmp/foo")
space.set_value(storage, Predicate("*-open-*"), VoidValue())

# Bulk restore: load the entire AtomSpace from storage in one step.
# This restores everything, and is the easiest way to load data.
# space.set_value(storage, Predicate("*-load-atomspace-*"), space)
SetValue(storage, Predicate("*-load-atomspace-*"), space).execute()

# Close storage for now.
space.set_value(storage, Predicate("*-close-*"), VoidValue())

print("After bulk restore:")
prt_atomspace_contents(space)

# -------------------------------------------
# Bulk restore of everything can be impractical, if the dataset is
# large. In this case, individual Atoms, and even Values, can be loaded,
# one at a time, or in groups. Storae can even be queried (!); see the
# scheme examples for details; they port over easily to python.
#
# Clear and start over.
space.clear()
print("Cleared AtomSpace again, size is now:", len(space))

# Reopen storage
storage = RocksStorage("rocks:///tmp/foo")
space.set_value(storage, Predicate("*-open-*"), VoidValue())

# Restore one atom: the predicate, whose name "My collection" we already
# know, as if by magic.  The fetch-atom message argument must be either
# a ListLink or a LinkValue containing the AtomSpace and the atom to
# fetch. Since Links are *always* stored in the AtomSpace, and Values
# are *never* stored in the AtomSpace, ths gives you some flexibility
# in how to represent the arguments. As a general rule, one should not
# pollute the AtomSpace with suprious transient temporary junk, and
# for this, the LinkValue is the obvious choice.

print("\nSelective fetch: restore just one atom by name.")
hurly = Predicate("My collection of URLs")
space.set_value(storage, Predicate("*-fetch-atom-*"),
	LinkValue([space, hurly]))

# The requested Atom, and all of the Values attached to it, have been
# placed directly into the AtomSpace. That is why there is no need to
# say 'my_stuff = space.set_value(storage, ...)' -- everything goes
# straight into the current AtomSpace.

# Verify that all the key-value pairs were fetched:
KeysOf(hurly).execute()

for key in KeysOf(hurly).execute().to_list():
	val = ValueOf(hurly, key).execute()
	print(f"{key} points at {val}")

# Note that the keyes were also automatically placed in the AtomSpace:
prt_atomspace_contents(space)

# Restore all Edges held in storage, by fetching the incoming set.
space.set_value(storage, Predicate("*-fetch-incoming-set-*"),
	LinkValue([space, url_pred]))

# Close the connection to storage.
space.set_value(storage, Predicate("*-close-*"), VoidValue())

print("\nAfter restorint the Incoming Set:")
prt_atomspace_contents(space)

print("Good bye!")
#
# The StorageNode API works via message-passing. Messages that have
# mandatory arugments are sent by saying
#
#    `atomspace.set_value(storage, message, args)`.
#
# A small handful of messages do not require arguments; these work by
# saying
#
#    result = `atomspace.get_value(storage, message)`.
#
# Messages are always PredicateNodes naming the operation.
#
# The StorageNode API is a message-passing API. It uses exactly the
# same generic 'atomspace.set_value(atom, key, value)' interface
# available for all Atoms. However, for StorageNodes (and more
# generally, for ObjectNodes, and more narrowly, for SensoryNodes)
# certain keys are interpreted as messages, requesting that some action
# be taken, rather than just static keys specifying a location.
#
# Such message-passing API's can be understood as a form of dynamic
# object-oriented programming: the messages can be thought of as being
# methods on a class instance, which are "called" when the message is
# is sent, whereas a plain key corresponds to a member or field: a named
# location on the class instance, where things can be stored.
#
# Common messages:
#
# * `*-open-*` and `*-close-*` to open/close storage connections.
#   Args: VoidValue()
#
# * `*-store-atom-*` to store an atom and all its values.
#   Args: the atom to store
#
# * `*-fetch-atom-*` to fetch an atom and all its values.
#   Args: LinkValue([atomspace, atom])
#
# * `*-store-atomspace-*` and `*-load-atomspace-*` for bulk operations.
#   Args: the AtomSpace
#
# * `*-fetch-incoming-set-*` to fetch all links containing an atom.
#   Args: LinkValue([atomspace, atom])
#
# * `*-fetch-incoming-by-type-*` to fetch links of a specific type.
#   Args: LinkValue([atomspace, atom, TypeNode("EdgeLink")])
#
# * `*-store-value-*` and `*-fetch-value-*` for individual values.
#   Args: LinkValue([atom, key])
#
# * `*-update-value-*` performs an atomic read-modify-write (increment).
#   Args: LinkValue([atom, key, delta])
#
# * `*-delete-*` and `*-delete-recursive-*` to remove atoms from storage.
#   Args: LinkValue([atomspace, atom])
#
# * `*-barrier-*` for multi-thread synchronization.
#   Args: atomspace
#
# * `*-connected?-*` to check connection status.
#   Use: atomspace.get_value(storage, PredicateNode("*-connected?-*"))
#
# * `*-monitor-*` to get performance/debugging info.
#   Use: atomspace.get_value(storage, PredicateNode("*-monitor-*"))
#
# See https://wiki.opencog.org/w/StorageNode for API documentation and
# longer description of the messages. A complete list of messages for
# a given StorageNode can be optained by saying
#
#     MessagesOf(storage).execute()
#
# The validity of a message can be tested by saying
#
#     IsMessage(Predicate("*-open-*")).execute()
#     IsMessage(Predicate("foobar")).execute()
#     IsMessage(Concept("*-dingdong-*")).execute()
#
# THE END.
# ---------------------------------------------------------------------