diff --git a/etc/codegen/Cargo.toml b/etc/codegen/Cargo.toml index a4699bfa..66fc2ea1 100644 --- a/etc/codegen/Cargo.toml +++ b/etc/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scallop-codegen" -version = "0.2.1" +version = "0.2.2" authors = ["Ziyang Li "] edition = "2018" diff --git a/etc/scallop-cli/examples/vision/tag_faces.scl b/etc/scallop-cli/examples/vision/tag_faces.scl index 3bf7a8d0..59882d2a 100644 --- a/etc/scallop-cli/examples/vision/tag_faces.scl +++ b/etc/scallop-cli/examples/vision/tag_faces.scl @@ -1,6 +1,6 @@ -// Input; an image directory -rel img_dir = { - "etc/scallop-cli/res/testing/images/face_example_3.jpg", +// Input; an image url +rel img_url = { + "https://m.media-amazon.com/images/M/MV5BOWFhYjE4NzMtOWJmZi00NzEyLTg5NTctYmIxMTU1ZDIxMDAyXkEyXkFqcGdeQXVyNTE1NjY5Mg@@._V1_.jpg", } // Input; a face tagging prompt @@ -8,7 +8,7 @@ rel prompt = { "the 7 main characters of Star Trek: The Next Generation" } -rel image($load_image(img_dir)) = img_dir(img_dir) +rel image($load_image_url(img_url)) = img_url(img_url) // Use face detection model to extract faces from the image @face_detection(["cropped-image", "bbox-x", "bbox-y", "bbox-w", "bbox-h"], enlarge_face_factor=1.2, dump_image=false) @@ -32,8 +32,11 @@ rel identity(id, name) = name := top<1>(name: face_name(img, list, name), face_i // Tag the faces rel tag_image(0, img) = image(img) rel tag_image(id + 1, $tag_image(img, x, y, w, h, name, "green", 10, 32)) = tag_image(id, img), identity(id, name), face_bbox(id, x, y, w, h) -rel save_image($save_image(img)) = tag_image(n as u32, img), n := count!(id: identity(id, name)) +rel output_image_url($upload_imgur(img)) = tag_image(n as u32, img), n := count!(id: identity(id, name)) +// Upload the image (Scallop is lazy) +query output_image_url + +// Inspect intermediate relations query identity -query face_bbox -query save_image +query face_bbox \ No newline at end of file diff --git a/etc/scallop-cli/makefile b/etc/scallop-cli/makefile new file mode 100644 index 00000000..2cf7e92d --- /dev/null +++ b/etc/scallop-cli/makefile @@ -0,0 +1,11 @@ +build: + python -m build + +install: build + find dist -name "*.whl" -print | xargs pip install --force-reinstall + +develop: + pip install --editable . + +clean: + rm -rf dist diff --git a/etc/scallop-cli/scallop/cli.py b/etc/scallop-cli/scallop/cli.py index 44e4d5a2..47487eb8 100644 --- a/etc/scallop-cli/scallop/cli.py +++ b/etc/scallop-cli/scallop/cli.py @@ -1,168 +1,12 @@ -import sys - -# Import torch (optional) -try: import torch -except: pass - -# Argument parser import argparse -# Prompting for REPL -import prompt_toolkit -from tabulate import tabulate - -# Scallop modules -import scallopy -import scallopy_ext - - -TABLE_FMT = "rounded_outline" -MULTILINE_TABLE_FMT = "fancy_grid" -MAX_COL_WIDTH = 120 - - -def argument_parser(plugin_registry: scallopy_ext.PluginRegistry): - parser = argparse.ArgumentParser("scallop", description="Scallop language command line interface") - parser.add_argument("file", nargs="?", default=None, help="The file to execute") - parser.add_argument("-p", "--provenance", type=str, default="unit", help="The provenance to pick") - parser.add_argument("-k", "--top-k", default=3, type=int, help="The `k` to use when applying `top-k` related provenance") - parser.add_argument("-m", "--module", type=str, default=None, help="Load module in interactive mode") - parser.add_argument("--iter-limit", type=int, default=100, help="Iteration limit") - parser.add_argument("--debug-front", action="store_true", help="Dump Front IR") - parser.add_argument("--debug-back", action="store_true", help="Dump Back IR") - parser.add_argument("--debug-ram", action="store_true", help="Dump RAM IR") - parser.add_argument("--dump-loaded-plugins", action="store_true", help="Dump loaded scallopy plugins") - - # Setup using plugin registry - plugin_registry.setup_argument_parser(parser) - - # Return the final parser - return parser - - -def cmd_args(plugin_registry: scallopy_ext.PluginRegistry): - parser = argument_parser(plugin_registry) - return parser.parse_known_args() - - -def print_relation(ctx: scallopy.ScallopContext, relation_name: str): - # First get the relation - if ctx.is_probabilistic(): - relation = [(f"{prob:.4f}", *tup) for (prob, tup) in ctx.relation(relation_name)] - else: - relation = list(ctx.relation(relation_name)) - - # Check if relation is empty - if len(relation) == 0: - print("{}") - return - - # Get arity and max column width - arity = len(relation[0]) - max_column_width = [MAX_COL_WIDTH for _ in range(arity)] - - # Then get the headers - headers = ctx.relation_field_names(relation_name) - if headers and ctx.is_probabilistic(): - headers = ["prob"] + headers - max_column_width = [None] + max_column_width - - # Check if we need multi-line table - need_multi_line_table = False - for tup in relation: - for val in tup: - if type(val) == str and len(val) > MAX_COL_WIDTH: - need_multi_line_table = True - - table_fmt = MULTILINE_TABLE_FMT if need_multi_line_table else TABLE_FMT - if headers: to_print = tabulate(relation, headers=headers, tablefmt=table_fmt, maxcolwidths=max_column_width) - else: to_print = tabulate(relation, tablefmt=table_fmt, maxcolwidths=max_column_width) - print(to_print) - - -def interpret(ctx, args): - try: - ctx.import_file(args.file) - except Exception as e: - print(e, file=sys.stderr) - - # Run the context - ctx.run() - - # Print the results - to_output_relations = [r for r in ctx.relations() if ctx.has_relation(r)] - for relation in sorted(to_output_relations): - print(f"{relation}:") - print_relation(ctx, relation) - - -def repl(ctx, args): - # Initialize key bindings - from prompt_toolkit.key_binding import KeyBindings - - bindings = KeyBindings() - - @bindings.add('c-c') - def _(event): print(); exit() - - @bindings.add('c-d') - def _(event): print(); exit() - - # If module is specified in args, load the module - if args.module is not None: - ctx.import_file(args.module) - - # Create a prompt session - prompt = prompt_toolkit.PromptSession(key_bindings=bindings) - while True: - user_input = prompt.prompt('scl> ') - try: - queries = ctx.add_item(user_input) - if len(queries) > 0: - ctx.run() - for query in queries: - if len(queries) > 1: - print(f"{query}:") - print_relation(ctx, query) - except Exception as err: - print(err, file=sys.stderr) - - -def setup_context(ctx: scallopy.ScallopContext, args): - # Iteration limit - ctx.set_iter_limit(args.iter_limit) - - # Debug - if args.debug_front: - ctx.set_debug_front() - if args.debug_back: - ctx.set_debug_back() - if args.debug_ram: - ctx.set_debug_ram() - - def main(): - plugin_registry = scallopy_ext.PluginRegistry() - - # Parse command line arguments - args, unknown_args = cmd_args(plugin_registry) - - # Configure environments - plugin_registry.configure(args, unknown_args) - if args.dump_loaded_plugins: - plugin_registry.dump_loaded_plugins() - - # Create a scallopy context - ctx = scallopy.ScallopContext(provenance=args.provenance, k=args.top_k) - setup_context(ctx, args) - - # Load the scallopy extension library - plugin_registry.load_into_ctx(ctx) - - # Check if the user has provided a file - if args.file is not None: - # If so, interpret the file directly - interpret(ctx, args) + parser = argparse.ArgumentParser("scallop", description="Scallop language command line interface") + parser.add_argument("first_arg", nargs="?", default=None) + args, _ = parser.parse_known_args() + if args.first_arg == "create-plugin": + from .create_plugin import main as create_plugin_main + create_plugin_main() else: - # Otherwise, enter REPL - repl(ctx, args) + from .run_scallop import main as run_scallop_main + run_scallop_main() diff --git a/etc/scallop-cli/scallop/create_plugin.py b/etc/scallop-cli/scallop/create_plugin.py new file mode 100644 index 00000000..4e733948 --- /dev/null +++ b/etc/scallop-cli/scallop/create_plugin.py @@ -0,0 +1,169 @@ +from typing import Optional +import os +import subprocess +import re +import argparse + +PY_PROJECT_SRC = """\ +[project] +name = "scallop-{dashed_name}" +version = "{version}" +dependencies = [] + +[tool.setuptools.packages.find] +where = ["src"] + +[project.entry-points."scallop.plugin.setup_arg_parser"] +{dashed_name} = "scallop_{underscore_name}:setup_arg_parser" + +[project.entry-points."scallop.plugin.configure"] +{dashed_name} = "scallop_{underscore_name}:configure" + +[project.entry-points."scallop.plugin.load_into_context"] +{dashed_name} = "scallop_{underscore_name}:load_into_context" +""" + + +MAKEFILE_SRC = """\ +develop: +\tpip install --editable . + +install: build +\tfind dist -name "*.whl" -print | xargs pip install --force-reinstall + +build: +\tpython -m build + +clean: +\trm -rf dist +""" + + +GITIGNORE_SRC = """\ +*.egg-info +dist +""" + + +INIT_SRC = """\ +import argparse +import scallopy + +def setup_arg_parser(parser: argparse.ArgumentParser): + pass + +def configure(args): + pass + +def load_into_context(ctx: scallopy.Context): + pass +""" + + +DONE_PRINT = """\ +Initialized empty Scallop plugin in {path}""" + + +ALLOWED_DIR_OBJS = set([ + ".DS_Store", + "Makefile", + "makefile", + "readme.md", + "Readme.md", + "readme", + "Readme", + ".vscode", + ".git", + ".github", +]) + + +def do_prompt_and_get_answer( + prompt: str, + require_non_empty: bool = True, + default_value: Optional[str] = None, + pattern: Optional[re.Pattern[str]] = None, + empty_string_print: str = "[Error] The answer cannot be empty; please try again", + pattern_fail_print: str = "[Error] Does not match the pattern {pattern}", + failed_print: str = "[Error] Failed to get a proper answer; please try `create-scallopy-plugin` again", + trim_whitespace: bool = True, + num_try: Optional[int] = 3 +): + try_count = 0 + while num_try is None or try_count < num_try: + try_count += 1 + ans = input(prompt) + if trim_whitespace: + ans = ans.strip() + if ans == "": + if require_non_empty: + print(empty_string_print) + else: + return default_value + else: + if re.search(pattern, ans) is not None: + return ans + else: + print(pattern_fail_print.format(pattern=str(pattern))) + print(failed_print) + exit(1) + + +def is_git_repo(): + """Check whether this is a git repo""" + with open(os.devnull, "w") as d: + return not bool(subprocess.call('git rev-parse', shell=True, stdout=d, stderr=d)) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("create-plugin") + parser.add_argument("path", nargs="?", default=None) + args = parser.parse_args() + + # Check whether path is provided in the arguments + if args.path is not None: + subprocess.run(["mkdir", "-p", args.path]) + cwd = args.path + else: + cwd = "." + + # Check if the current folder does not contain `pyproject.toml`, `makefile`, and `src` + curr_dir_objs = set(os.listdir(cwd)) + disallowed_dir_objs = curr_dir_objs.difference(ALLOWED_DIR_OBJS) + if len(disallowed_dir_objs) > 0: + print(f"Cannot create a scallopy plugin in non-empty directory. Found object `{disallowed_dir_objs.pop()}`") + exit(2) + + # Collect essential information + print("This script will guide you creating a new scallopy plugin.") + print("Please help answer the following questions:") + name = do_prompt_and_get_answer("Plugin Name: scallop-", pattern=r"^[a-zA-Z][a-zA-Z\-0-9]*$") + version = do_prompt_and_get_answer("Version (1.0.0): ", pattern=r"^\d+\.\d+\.\d+$", require_non_empty=False, default_value="1.0.0") + + # Run git init + if not is_git_repo(): + with open(os.devnull, "w") as d: + subprocess.call("git init", shell=True, stdout=d, stderr=d, cwd=cwd) + + # Write .gitignore file + with open(os.path.join(cwd, ".gitignore"), "w") as gitignore_file: + gitignore_file.write(GITIGNORE_SRC) + + # Create pyproject.toml + underscore_name = name.replace("-", "_") + py_project_src = PY_PROJECT_SRC.format(dashed_name=name, underscore_name=underscore_name, version=version) + with open(os.path.join(cwd, "pyproject.toml"), "w") as pyproject_file: + pyproject_file.write(py_project_src) + + # Create makefile + with open(os.path.join(cwd, "makefile"), "w") as makefile_file: + makefile_file.write(MAKEFILE_SRC) + + # Create folders + subprocess.run(["mkdir", "-p", f"src/scallop_{name}"], cwd=cwd) + with open(os.path.join(cwd, "src", f"scallop_{name}", "__init__.py"), "w") as init_file: + init_file.write(INIT_SRC) + + # Final prints + print(DONE_PRINT.format(path=os.path.abspath(os.path.join(cwd)))) diff --git a/etc/scallop-cli/scallop/run_scallop.py b/etc/scallop-cli/scallop/run_scallop.py new file mode 100644 index 00000000..903d200b --- /dev/null +++ b/etc/scallop-cli/scallop/run_scallop.py @@ -0,0 +1,176 @@ +import sys + +# Argument parser +import argparse + +# Prompting for REPL +import prompt_toolkit +from tabulate import tabulate + +# Scallop modules +import scallopy +import scallopy_ext + + +TABLE_FMT = "rounded_outline" +MULTILINE_TABLE_FMT = "fancy_grid" +MAX_COL_WIDTH = 120 + + +def argument_parser(plugin_registry: scallopy_ext.PluginRegistry): + parser = argparse.ArgumentParser("scallop", description="Scallop language command line interface") + parser.add_argument("file", nargs="?", default=None, help="The file to execute") + parser.add_argument("-p", "--provenance", type=str, default="unit", help="The provenance to pick") + parser.add_argument("-k", "--top-k", default=3, type=int, help="The `k` to use when applying `top-k` related provenance") + parser.add_argument("--wmc-with-disjunctions", action="store_true", help="Whether to use disjunctions when performing weighted model counting (WMC)") + parser.add_argument("-m", "--module", type=str, default=None, help="Load module in interactive mode") + parser.add_argument("--iter-limit", type=int, default=100, help="Iteration limit") + parser.add_argument("--debug-front", action="store_true", help="Dump Front IR") + parser.add_argument("--debug-back", action="store_true", help="Dump Back IR") + parser.add_argument("--debug-ram", action="store_true", help="Dump RAM IR") + parser.add_argument("--dump-loaded-plugins", action="store_true", help="Dump loaded scallopy plugins") + parser.add_argument("--no-plugin", action="store_true", help="Do not load any plugin") + + # Get the basic arguments + base_args, _ = parser.parse_known_args() + + # Setup using plugin registry + if not base_args.no_plugin: + plugin_registry.setup_argument_parser(parser) + else: + plugin_registry.remove_all_plugins() + + # Return the final parser + return parser + + +def cmd_args(plugin_registry: scallopy_ext.PluginRegistry): + parser = argument_parser(plugin_registry) + return parser.parse_known_args() + + +def print_relation(ctx: scallopy.ScallopContext, relation_name: str): + # First get the relation + if ctx.is_probabilistic(): + relation = [(f"{prob:.4f}", *tup) for (prob, tup) in ctx.relation(relation_name)] + else: + relation = list(ctx.relation(relation_name)) + + # Check if relation is empty + if len(relation) == 0: + print("{}") + return + + # Get arity and max column width + arity = len(relation[0]) + + # Check if the tuple is empty + if arity == 0 and not ctx.is_probabilistic(): + relation[0] = ("()",) + elif arity == 1 and ctx.is_probabilistic(): + relation[0] = (relation[0][0], "()") + + # Compute column width + max_column_width = [MAX_COL_WIDTH for _ in range(arity)] + + # Then get the headers + headers = ctx.relation_field_names(relation_name) + if headers and ctx.is_probabilistic(): + headers = ["prob"] + headers + max_column_width = [None] + max_column_width + + # Check if we need multi-line table + need_multi_line_table = False + for tup in relation: + for val in tup: + if type(val) == str and len(val) > MAX_COL_WIDTH: + need_multi_line_table = True + + table_fmt = MULTILINE_TABLE_FMT if need_multi_line_table else TABLE_FMT + if headers: to_print = tabulate(relation, headers=headers, tablefmt=table_fmt, maxcolwidths=max_column_width) + else: to_print = tabulate(relation, tablefmt=table_fmt, maxcolwidths=max_column_width) + print(to_print) + + +def interpret(ctx, args): + try: + ctx.import_file(args.file) + except Exception as e: + print(e, file=sys.stderr) + + # Run the context + ctx.run() + + # Print the results + to_output_relations = [r for r in ctx.relations() if ctx.has_relation(r)] + for relation in sorted(to_output_relations): + print(f"{relation}:") + print_relation(ctx, relation) + + +def repl(ctx, args): + # If module is specified in args, load the module + if args.module is not None: + ctx.import_file(args.module) + + # Create a prompt session + prompt = prompt_toolkit.PromptSession() + while True: + try: + user_input = prompt.prompt('scl> ') + queries = ctx.add_item(user_input) + if len(queries) > 0: + ctx.run() + for query in queries: + if len(queries) > 1: + print(f"{query}:") + print_relation(ctx, query) + except EOFError: + exit() + except KeyboardInterrupt: + exit() + except Exception as err: + print(err, file=sys.stderr) + + +def setup_context(ctx: scallopy.ScallopContext, args): + # Iteration limit + ctx.set_iter_limit(args.iter_limit) + + # Debug + if args.debug_front: + ctx.set_debug_front() + if args.debug_back: + ctx.set_debug_back() + if args.debug_ram: + ctx.set_debug_ram() + + +def main(): + plugin_registry = scallopy_ext.PluginRegistry() + + # Parse command line arguments + args, unknown_args = cmd_args(plugin_registry) + + # Configure environments + plugin_registry.configure(args, unknown_args) + if args.dump_loaded_plugins: + plugin_registry.dump_loaded_plugins() + + # Create a scallopy context + ctx = scallopy.ScallopContext( + provenance=args.provenance, + k=args.top_k, + wmc_with_disjunctions=args.wmc_with_disjunctions) + setup_context(ctx, args) + + # Load the scallopy extension library + plugin_registry.load_into_ctx(ctx) + + # Check if the user has provided a file + if args.file is not None: + # If so, interpret the file directly + interpret(ctx, args) + else: + # Otherwise, enter REPL + repl(ctx, args) diff --git a/etc/scallop-cli/setup.cfg b/etc/scallop-cli/setup.cfg index 88b4401a..27facf30 100644 --- a/etc/scallop-cli/setup.cfg +++ b/etc/scallop-cli/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = scallop -version = 0.2.1 +version = 0.2.2 author = Ziyang Li author_email = liby99@seas.upenn.edu description = Scallop CLI @@ -8,7 +8,8 @@ long_description = file: readme.md long_description_content_type = text/markdown [options] -packages = scallop +packages = + scallop python_requires = >=3.7 install_requires = prompt_toolkit diff --git a/etc/scallop-wasm/Cargo.toml b/etc/scallop-wasm/Cargo.toml index 97e915f7..ef77a34f 100644 --- a/etc/scallop-wasm/Cargo.toml +++ b/etc/scallop-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scallop-wasm" -version = "0.2.1" +version = "0.2.2" authors = ["Ziyang Li"] edition = "2018" diff --git a/etc/scallopy-ext/makefile b/etc/scallopy-ext/makefile new file mode 100644 index 00000000..2cf7e92d --- /dev/null +++ b/etc/scallopy-ext/makefile @@ -0,0 +1,11 @@ +build: + python -m build + +install: build + find dist -name "*.whl" -print | xargs pip install --force-reinstall + +develop: + pip install --editable . + +clean: + rm -rf dist diff --git a/etc/scallopy-ext/setup.cfg b/etc/scallopy-ext/setup.cfg index 12ada4de..d01a6a4d 100644 --- a/etc/scallopy-ext/setup.cfg +++ b/etc/scallopy-ext/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = scallopy_ext -version = 0.2.1 +version = 0.2.2 author = Ziyang Li author_email = liby99@seas.upenn.edu description = Scallopy Extension diff --git a/etc/scallopy-ext/src/scallopy_ext/constants.py b/etc/scallopy-ext/src/scallopy_ext/constants.py new file mode 100644 index 00000000..69a5a3ff --- /dev/null +++ b/etc/scallopy-ext/src/scallopy_ext/constants.py @@ -0,0 +1,10 @@ +group_name = lambda s: f"scallop.plugin.{s}" + +SETUP_ARG_PARSER = "setup_arg_parser" +SETUP_ARG_PARSER_GROUP = group_name(SETUP_ARG_PARSER) + +CONFIGURE = "configure" +CONFIGURE_GROUP = group_name(CONFIGURE) + +LOAD_INTO_CONTEXT = "load_into_context" +LOAD_INTO_CONTEXT_GROUP = group_name(LOAD_INTO_CONTEXT) diff --git a/etc/scallopy-ext/src/scallopy_ext/registry.py b/etc/scallopy-ext/src/scallopy_ext/registry.py index 87bb1608..edb5793d 100644 --- a/etc/scallopy-ext/src/scallopy_ext/registry.py +++ b/etc/scallopy-ext/src/scallopy_ext/registry.py @@ -1,50 +1,137 @@ -from typing import List, Optional, Any -from importlib.metadata import entry_points +from typing import List, Optional, Any, Callable +from importlib import metadata from argparse import ArgumentParser import scallopy from . import utils +from . import constants class PluginRegistry: - def __init__(self): - self.setup_argparse_functions = utils.dedup(entry_points(group="scallop.plugin.setup_arg_parser")) - self.configure_functions = utils.dedup(entry_points(group="scallop.plugin.configure")) - self.loading_functions = utils.dedup(entry_points(group="scallop.plugin.load_into_context")) - self.unknown_args = {} + setup_argparse_functions: List[utils.EntryPoint] + configure_functions: List[utils.EntryPoint] + loading_functions: List[utils.EntryPoint] + + def __init__( + self, + load_from_entry_points: bool = True, + filter: Optional[Callable[[str], bool]] = None + ): + """ + Create a new plugin registry. Depending on the arguments, we either + load the registry from entry points, or we initialize an empty + registry. + """ + self.setup_argparse_functions = [] + self.configure_functions = [] + self.loading_functions = [] + self.filter = filter + + # Load plugins + if load_from_entry_points: self.load_plugins_from_entry_points() + + # The unknown arguments is set to empty by default + self.unknown_args = [] + + def load_plugin( + self, + name: str, + setup_arg_parser: Optional[Callable] = None, + configure: Optional[Callable] = None, + load_into_context: Optional[Callable] = None, + ): + """ + Manually load a plugin, given its `name`, and `setup_arg_parser`, + `configure`, `load_into_context` functions. + + Note: Calling this function will bypass the filter provided when + constructing the plugin registry. + """ + if setup_arg_parser is not None: + self.setup_argparse_functions.append(utils.CustomEntryPoint(name, constants.SETUP_ARG_PARSER, setup_arg_parser)) + if configure is not None: + self.configure_functions.append(utils.CustomEntryPoint(name, constants.CONFIGURE, configure)) + if load_into_context is not None: + self.loading_functions.append(utils.CustomEntryPoint(name, constants.LOAD_INTO_CONTEXT, load_into_context)) + + def load_plugins_from_entry_points(self): + """ + Load multiple plugins from entry points obtained by `importlib` + """ + eps = metadata.entry_points() + utils._dedup_extend(self.setup_argparse_functions, utils._find_entry_points(eps, constants.SETUP_ARG_PARSER_GROUP, self.filter)) + utils._dedup_extend(self.configure_functions, utils._find_entry_points(eps, constants.CONFIGURE_GROUP, self.filter)) + utils._dedup_extend(self.loading_functions, utils._find_entry_points(eps, constants.LOAD_INTO_CONTEXT_GROUP, self.filter)) + + def remove_all_plugins(self): + """ + Remove all the loaded plugins in this registry + """ + self.setup_argparse_functions = [] + self.configure_functions = [] + self.loading_functions = [] def loaded_plugins(self) -> List[str]: + """ + Obtain the list of loaded plugins. Each plugin is represented + as a string of the form `{{plugin_name}}::{{function_name}}`. + """ all_plugins = set() for setup_module in self.setup_argparse_functions: - all_plugins.add(f"{setup_module.name}::setup_arg_parser") + all_plugins.add(f"{setup_module.name}::{constants.SETUP_ARG_PARSER}") for config_module in self.configure_functions: - all_plugins.add(f"{config_module.name}::configure") + all_plugins.add(f"{config_module.name}::{constants.CONFIGURE}") for loading_module in self.loading_functions: - all_plugins.add(f"{loading_module.name}::load_into_context") + all_plugins.add(f"{loading_module.name}::{constants.LOAD_INTO_CONTEXT}") return list(all_plugins) def dump_loaded_plugins(self): + """ + Print the list of loaded plugins + """ print("[scallopy-ext] Loaded plugins:", ", ".join(self.loaded_plugins())) def setup_argument_parser(self, parser: ArgumentParser): + """ + Apply the `setup_arg_parser` function from all plugins on a new + argument parser. + """ for setup_module in self.setup_argparse_functions: setup_fn = setup_module.load() setup_fn(parser) - def configure(self, args, unknown_args): + def configure(self, args={}, unknown_args=[]): + """ + Configure the plugins using the parsed arguments + """ + # Preprocess arguments + args = utils._process_args(args) + + # Configure the plugins self.unknown_args = unknown_args for configure_module in self.configure_functions: configure_fn = configure_module.load() configure_fn(args) - def load_into_ctx(self, ctx: scallopy.ScallopContext): + def load_into_ctx(self, ctx: scallopy.Context): + """ + Apply the `load_into_context` function from all plugins on a newly + created scallopy context + """ + + # First register two built-in attributes self.register_cmd_arg_fa(ctx) self.register_py_eval_fa(ctx) + + # Then load all the plugins for loader_module in self.loading_functions: loading_fn = loader_module.load() loading_fn(ctx) - def register_cmd_arg_fa(self, ctx: scallopy.ScallopContext): + def register_cmd_arg_fa(self, ctx: scallopy.Context): + """ + Register a `@cmd_arg` foreign attribute to the Context + """ unknown_args = self.unknown_args @scallopy.foreign_attribute @@ -78,7 +165,11 @@ def get_arg(): ctx.register_foreign_attribute(cmd_arg) - def register_py_eval_fa(self, ctx: scallopy.ScallopContext): + def register_py_eval_fa(self, ctx: scallopy.Context): + """ + Register a `@py_eval` foreign attribute to the Context + """ + @scallopy.foreign_attribute def py_eval(item, *, suppress_warning=True): # Check if the annotation is on function type decl diff --git a/etc/scallopy-ext/src/scallopy_ext/utils.py b/etc/scallopy-ext/src/scallopy_ext/utils.py index 54f4672f..6991b18a 100644 --- a/etc/scallopy-ext/src/scallopy_ext/utils.py +++ b/etc/scallopy-ext/src/scallopy_ext/utils.py @@ -1,2 +1,48 @@ -def dedup(elems): - return list(set(elems)) +from typing import Callable, Optional, Dict, List, Union +from importlib import metadata +import argparse + + +def _process_args(args): + if isinstance(args, argparse.Namespace): + return {str(key): args.__dict__[key] for key in args.__dict__.keys()} + elif type(args) == dict: + return args + else: + return {} + + +def _find_entry_points( + entry_points: Dict[str, List[metadata.EntryPoint]], + group: str, + filter: Optional[Callable[[str], bool]] = None, +): + if group in entry_points: + eps = list(set(entry_points[group])) + if filter is not None: + eps = [ep for ep in eps if filter(ep.name)] + return eps + else: + return [] + + +def _dedup_extend(array: List, new_array: List): + for elem in new_array: + if elem not in array: + array.append(elem) + + +class CustomEntryPoint: + """ + A custom entry point as a replacement of `importlib.metadata` entry point + """ + def __init__(self, name: str, fn_name: str, fn: Callable): + self.name = name + self.group = f"scallop.plugin.{fn_name}" + self.fn = fn + + def load(self): + return self.fn + + +EntryPoint = Union[CustomEntryPoint, metadata.EntryPoint] diff --git a/etc/scallopy-ext/tests/basics.py b/etc/scallopy-ext/tests/basics.py new file mode 100644 index 00000000..e92e2ff0 --- /dev/null +++ b/etc/scallopy-ext/tests/basics.py @@ -0,0 +1,19 @@ +import unittest + +import scallopy +import scallopy_ext + +class TestConfigure(unittest.TestCase): + def test_loading_no_plugin(self): + plugin_registry = scallopy_ext.PluginRegistry(load_from_entry_points=False) + plugins = plugin_registry.loaded_plugins() + assert len(plugins) == 0, "there should be no loaded plugins" + + def test_load_a_plugin(self): + def configure(args): pass + plugin_registry = scallopy_ext.PluginRegistry(load_from_entry_points=False) + plugins = plugin_registry.loaded_plugins() + assert len(plugins) == 0, "there should be no loaded plugins" + plugin_registry.load_plugin("temp", configure=configure) + plugins = plugin_registry.loaded_plugins() + assert plugins == ["temp::configure"], "there should one configure plugin" diff --git a/etc/scallopy-ext/tests/test.py b/etc/scallopy-ext/tests/test.py new file mode 100644 index 00000000..cd33a21a --- /dev/null +++ b/etc/scallopy-ext/tests/test.py @@ -0,0 +1,6 @@ +import unittest + +from basics import * + +if __name__ == '__main__': + unittest.main() diff --git a/etc/scallopy-plugins/.gitignore b/etc/scallopy-plugins/.gitignore index 11041c78..00efab29 100644 --- a/etc/scallopy-plugins/.gitignore +++ b/etc/scallopy-plugins/.gitignore @@ -1 +1,2 @@ *.egg-info +dist diff --git a/etc/scallopy-plugins/clip/src/scallop_clip/clip.py b/etc/scallopy-plugins/clip/src/scallop_clip/clip.py index 8a8f7dcb..8be9fb54 100644 --- a/etc/scallopy-plugins/clip/src/scallop_clip/clip.py +++ b/etc/scallopy-plugins/clip/src/scallop_clip/clip.py @@ -1,7 +1,7 @@ from typing import Tuple, List, Optional import scallopy -import clip +import clip as clip_lib import torch from PIL import Image @@ -12,69 +12,102 @@ ERR_HEAD = f"[@clip]" DELIMITER = ";" + @scallopy.foreign_attribute def clip( - item, - labels: Optional[List[str]] = None, - *, - prompt: Optional[str] = None, - score_threshold: float = 0, - unknown_class: str = "?", - debug: bool = False, + item, + labels: Optional[List[str]] = None, + *, + prompt: Optional[str] = None, + score_threshold: float = 0, + unknown_class: str = "?", + debug: bool = False, ): - # Check if the annotation is on relation type decl - assert item.is_relation_decl(), f"{ERR_HEAD} has to be an attribute of a relation type declaration" - assert len(item.relation_decls()) == 1, f"{ERR_HEAD} cannot be an attribute on multiple relations" + # Check if the annotation is on relation type decl + assert ( + item.is_relation_decl() + ), f"{ERR_HEAD} has to be an attribute of a relation type declaration" + assert ( + len(item.relation_decls()) == 1 + ), f"{ERR_HEAD} cannot be an attribute on multiple relations" - # Get the relation name and check argument types - relation_decl = item.relation_decl(0) - args = [arg for arg in relation_decl.arg_bindings] + # Get the relation name and check argument types + relation_decl = item.relation_decl(0) + args = [arg for arg in relation_decl.arg_bindings] - # Check the argument types - assert len(args) >= 1 and args[0].ty.is_tensor() and args[0].adornment.is_bound(), f"{ERR_HEAD} first argument has to be a bounded type `Tensor`" - if labels is not None: - assert len(args) == 2, f"{ERR_HEAD} relation has to be of arity-2 provided the labels" - assert args[1].ty.is_string() and (args[1].adornment is None or args[1].adornment.is_free()), f"{ERR_HEAD} second argument has to be of free type `String`" - else: - assert len(args) == 3, f"{ERR_HEAD} relation has to be of arity-3 given that labels need to be passed in dynamically" - assert args[1].ty.is_string() and args[1].adornment.is_bound(), f"{ERR_HEAD} second argument has to be a bounded type `String`" - assert args[2].ty.is_string() and (args[2].adornment is None or args[2].adornment.is_free()), f"{ERR_HEAD} third argument has to be of free type `String`" + # Check the argument types + assert ( + len(args) >= 1 and args[0].ty.is_tensor() and args[0].adornment.is_bound() + ), f"{ERR_HEAD} first argument has to be a bounded type `Tensor`" + if labels is not None: + assert ( + len(args) == 2 + ), f"{ERR_HEAD} relation has to be of arity-2 provided the labels" + assert args[1].ty.is_string() and ( + args[1].adornment is None or args[1].adornment.is_free() + ), f"{ERR_HEAD} second argument has to be of free type `String`" + else: + assert ( + len(args) == 3 + ), f"{ERR_HEAD} relation has to be of arity-3 given that labels need to be passed in dynamically" + assert ( + args[1].ty.is_string() and args[1].adornment.is_bound() + ), f"{ERR_HEAD} second argument has to be a bounded type `String`" + assert args[2].ty.is_string() and ( + args[2].adornment is None or args[2].adornment.is_free() + ), f"{ERR_HEAD} third argument has to be of free type `String`" - @scallopy.foreign_predicate(name=relation_decl.name.name) - def clip_classify(img: scallopy.Tensor) -> scallopy.Generator[float, Tuple[str]]: - device = get_device() - maybe_clip_model = get_clip_model(debug=debug) - if maybe_clip_model is None: - return + @scallopy.foreign_predicate(name=relation_decl.name.name) + def clip_classify(img: scallopy.Tensor) -> scallopy.Facts[float, Tuple[str]]: + device = get_device() + maybe_clip_model = get_clip_model(debug=debug) + if maybe_clip_model is None: + return - # If successfully loaded clip, then initialize the clip models - (clip_model, clip_preprocess) = maybe_clip_model + # If successfully loaded clip, then initialize the clip models + (clip_model, clip_preprocess) = maybe_clip_model - # Enter non-training mode - with torch.no_grad(): - class_prompts = [get_class_prompt(prompt, class_str) for class_str in labels] - txt_tokens = clip.tokenize(class_prompts).to(device=device) - img_proc = clip_preprocess(Image.fromarray(img.numpy())).unsqueeze(0).to(device=device) - logits_per_image, _ = clip_model(img_proc, txt_tokens) - probs = logits_per_image.softmax(dim=-1).cpu()[0] - for prob, class_prompt, class_str in zip(probs, class_prompts, labels): - if debug: - print(f"[@clip_classifier] {prob:.4f} :: {class_prompt} (Class Name: {class_str})") - if prob >= score_threshold: yield (prob, (class_str,)) - else: yield (0.0, (unknown_class,)) + # Enter non-training mode + with torch.no_grad(): + class_prompts = [ + get_class_prompt(prompt, class_str) for class_str in labels + ] + txt_tokens = clip_lib.tokenize(class_prompts).to(device=device) + img_proc = ( + clip_preprocess(Image.fromarray(img.numpy())) + .unsqueeze(0) + .to(device=device) + ) + logits_per_image, _ = clip_model(img_proc, txt_tokens) + probs = logits_per_image.softmax(dim=-1).cpu()[0] + for prob, class_prompt, class_str in zip(probs, class_prompts, labels): + if debug: + print( + f"[@clip_classifier] {prob:.4f} :: {class_prompt} (Class Name: {class_str})" + ) + if prob >= score_threshold: + yield (prob, (class_str,)) + else: + yield (0.0, (unknown_class,)) - # Generate the foreign predicate for dynamic labels - @scallopy.foreign_predicate(name=relation_decl.name.name) - def clip_classify_with_labels(img: scallopy.Tensor, list: scallopy.String) -> scallopy.Generator[float, Tuple[str]]: - nonlocal labels - labels = [item.strip() for item in list.split(DELIMITER)] - return clip_classify(img) + # Generate the foreign predicate for dynamic labels + @scallopy.foreign_predicate(name=relation_decl.name.name) + def clip_classify_with_labels( + img: scallopy.Tensor, list: scallopy.String + ) -> scallopy.Facts[float, Tuple[str]]: + nonlocal labels + labels = [item.strip() for item in list.split(DELIMITER)] + return clip_classify(img) - # Return the appropriate foreign predicate - if labels is not None: return clip_classify - else: return clip_classify_with_labels + # Return the appropriate foreign predicate + if labels is not None: + return clip_classify + else: + return clip_classify_with_labels def get_class_prompt(prompt: Optional[str], class_name: str): - if prompt: return prompt.replace("{{}}", class_name) - else: return class_name + if prompt: + return prompt.replace("{{}}", class_name) + else: + return class_name diff --git a/etc/scallopy-plugins/clip/src/scallop_clip/config.py b/etc/scallopy-plugins/clip/src/scallop_clip/config.py index a358a7a6..1c93b075 100644 --- a/etc/scallopy-plugins/clip/src/scallop_clip/config.py +++ b/etc/scallopy-plugins/clip/src/scallop_clip/config.py @@ -14,7 +14,7 @@ def setup_arg_parser(parser): def configure(args): global _CLIP_MODEL_CHECKPOINT - _CLIP_MODEL_CHECKPOINT = args.clip_model_checkpoint + _CLIP_MODEL_CHECKPOINT = args["clip_model_checkpoint"] def get_clip_model(debug=False): diff --git a/etc/scallopy-plugins/face-detection/src/scallop_face_detection/face_detection.py b/etc/scallopy-plugins/face-detection/src/scallop_face_detection/face_detection.py index 96b12e65..1823b6f7 100644 --- a/etc/scallopy-plugins/face-detection/src/scallop_face_detection/face_detection.py +++ b/etc/scallopy-plugins/face-detection/src/scallop_face_detection/face_detection.py @@ -3,7 +3,7 @@ import os from PIL import Image -import face_detection +import face_detection as face_detection_lib import scallopy from scallop_gpu import get_device @@ -13,40 +13,46 @@ DUMP_IMAGE_PATH = ".tmp/scallop-face/" FIELDS = { - "bbox-x": { - "description": "The x-coordinate of the bounding box", - "type": scallopy.u32, - }, - "bbox-y": { - "description": "The y-coordinate of the bounding box", - "type": scallopy.u32, - }, - "bbox-w": { - "description": "The width of the bounding box", - "type": scallopy.u32, - }, - "bbox-h": { - "description": "The height of the bounding box", - "type": scallopy.u32, - }, - "cropped-image": { - "description": "The cropped image (bounded by bounding box)", - "type": scallopy.Tensor, - }, + "bbox-x": { + "description": "The x-coordinate of the bounding box", + "type": scallopy.u32, + }, + "bbox-y": { + "description": "The y-coordinate of the bounding box", + "type": scallopy.u32, + }, + "bbox-w": { + "description": "The width of the bounding box", + "type": scallopy.u32, + }, + "bbox-h": { + "description": "The height of the bounding box", + "type": scallopy.u32, + }, + "cropped-image": { + "description": "The cropped image (bounded by bounding box)", + "type": scallopy.Tensor, + }, } _FACE_MODEL = None + def get_face_model(): - global _FACE_MODEL + global _FACE_MODEL - if _FACE_MODEL is None: - try: - _FACE_MODEL = face_detection.build_detector("DSFDDetector", confidence_threshold=.5, nms_iou_threshold=.3, device=get_device()) - except: - return None + if _FACE_MODEL is None: + try: + _FACE_MODEL = face_detection_lib.build_detector( + "DSFDDetector", + confidence_threshold=0.5, + nms_iou_threshold=0.3, + device=get_device(), + ) + except: + return None - return _FACE_MODEL + return _FACE_MODEL @scallopy.foreign_attribute @@ -59,115 +65,147 @@ def face_detection( debug: bool = False, dump_image: bool = False, ): - # Needs to be a relation declaration, and can have only one relation - assert item.is_relation_decl(), f"{ERR_HEAD} has to be an attribute of a relation type declaration" - assert len(item.relation_decls()) == 1, f"{ERR_HEAD} cannot be an attribute on multiple relations]" - - # Get the argument names and argument types - relation_decl = item.relation_decl(0) - arg_names = [ab.name.name for ab in relation_decl.arg_bindings] - arg_types = [ab.ty for ab in relation_decl.arg_bindings] - - # Expected input arguments - expected_args = [(scallopy.Tensor, "image: Tensor")] - - # Expected output arguments - expected_args += [(scallopy.u32, "id: u32")] - - # Fields: make sure that all output fields are supported - assert type(output_fields) == list, f"{ERR_HEAD} The parameter `output_fields` need to be a list" - assert len(output_fields) > 0, f"{ERR_HEAD} The `output_fields` list must be non-empty" - for field in output_fields: - assert field in FIELDS, f"{ERR_HEAD} Unknown output field `{field}`; supported fields are {list(FIELDS.keys())}" - expected_args += [(FIELDS[f]["type"], f + ": " + FIELDS[f]["type"]) for f in output_fields] - - # Check arity of relation - assert len(arg_types) == len(expected_args), f"{ERR_HEAD} relation has to be of arity-{len(expected_args)}: {[s for (_, s) in expected_args]}" - - # Making sure that arg types are as expected - for arg_id in range(len(arg_types)): - (expected_ty, s), arg_name, arg_ty = expected_args[arg_id], arg_names[arg_id], arg_types[arg_id].name() - if arg_ty.lower() != expected_ty.lower(): - if arg_name is not None: - raise Exception(f"{ERR_HEAD} Type mismatch for `{arg_name}`. Expected `{s}`, but found arg of type `{arg_ty}`") - else: - raise Exception(f"{ERR_HEAD} Type mismatch for argument `{arg_id+1}`. Expected `{s}`, but found arg of type `{arg_ty}`") - - # For memoization of face detection output - STORAGE = {} - - # Generate foreign predicate - @scallopy.foreign_predicate( - name=relation_decl.name.name, - input_arg_types=arg_types[:1], - output_arg_types=arg_types[1:], - tag_type=float, - ) - def do_face_detect(img: scallopy.Tensor): - img_h, img_w, _ = img.shape - - # Deal with STORAGE; check if the response is memoized - if img in STORAGE: - faces = STORAGE[img] - else: - model = get_face_model() - if model is not None: - faces = model.detect(np.array(img)) - STORAGE[img] = faces - else: - faces = [] - - # Count - yielded_count = 0 - - # Iterate through all the faces - for (i, box) in enumerate(faces): - if debug: print(box) - - # Basic information - x1, y1, x2, y2, prob = *map(int, box[:-1]), box[-1] - w, h = int((enlarge_face_factor - 1) * (x2 - x1) / 2), int((enlarge_face_factor - 1) * (y2 - y1) / 2) - x1, y1, x2, y2 = max(0, x1 - w), max(0, y1 - h), min(img_w, x2 + w), min(img_h, y2 + h) - - # Check yielded count; stop if reaching limit - if limit is not None and yielded_count > limit: break - else: yielded_count += 1 - - # Get all the information for the fields - result_tuple = [i] - for field in output_fields: - if field == "cropped-image": - cropped_img = img[y1:y2, x1:x2, :] - result_tuple.append(cropped_img) - if dump_image: - save_temporary_image(id(img), i, field, cropped_img) - elif field == "bbox-x": - result_tuple.append(x1) - elif field == "bbox-y": - result_tuple.append(y1) - elif field == "bbox-w": - result_tuple.append(x2 - x1) - elif field == "bbox-h": - result_tuple.append(y2 - y1) - - # Debug - if debug: - print(result_tuple) - - # Generate the face - yield (prob, tuple(result_tuple)) - - # Remove the item and register a foreign predicate - return do_face_detect + # Needs to be a relation declaration, and can have only one relation + assert ( + item.is_relation_decl() + ), f"{ERR_HEAD} has to be an attribute of a relation type declaration" + assert ( + len(item.relation_decls()) == 1 + ), f"{ERR_HEAD} cannot be an attribute on multiple relations]" + + # Get the argument names and argument types + relation_decl = item.relation_decl(0) + arg_names = [ab.name.name for ab in relation_decl.arg_bindings] + arg_types = [ab.ty for ab in relation_decl.arg_bindings] + + # Expected input arguments + expected_args = [(scallopy.Tensor, "image: Tensor")] + + # Expected output arguments + expected_args += [(scallopy.u32, "id: u32")] + + # Fields: make sure that all output fields are supported + assert ( + type(output_fields) == list + ), f"{ERR_HEAD} The parameter `output_fields` need to be a list" + assert ( + len(output_fields) > 0 + ), f"{ERR_HEAD} The `output_fields` list must be non-empty" + for field in output_fields: + assert ( + field in FIELDS + ), f"{ERR_HEAD} Unknown output field `{field}`; supported fields are {list(FIELDS.keys())}" + expected_args += [ + (FIELDS[f]["type"], f + ": " + FIELDS[f]["type"]) for f in output_fields + ] + + # Check arity of relation + assert len(arg_types) == len( + expected_args + ), f"{ERR_HEAD} relation has to be of arity-{len(expected_args)}: {[s for (_, s) in expected_args]}" + + # Making sure that arg types are as expected + for arg_id in range(len(arg_types)): + (expected_ty, s), arg_name, arg_ty = ( + expected_args[arg_id], + arg_names[arg_id], + arg_types[arg_id].name(), + ) + if arg_ty.lower() != expected_ty.lower(): + if arg_name is not None: + raise Exception( + f"{ERR_HEAD} Type mismatch for `{arg_name}`. Expected `{s}`, but found arg of type `{arg_ty}`" + ) + else: + raise Exception( + f"{ERR_HEAD} Type mismatch for argument `{arg_id+1}`. Expected `{s}`, but found arg of type `{arg_ty}`" + ) + + # For memoization of face detection output + STORAGE = {} + + # Generate foreign predicate + @scallopy.foreign_predicate( + name=relation_decl.name.name, + input_arg_types=arg_types[:1], + output_arg_types=arg_types[1:], + tag_type=float, + ) + def do_face_detect(img: scallopy.Tensor): + img_h, img_w, _ = img.shape + + # Deal with STORAGE; check if the response is memoized + if img in STORAGE: + faces = STORAGE[img] + else: + model = get_face_model() + if model is not None: + faces = model.detect(np.array(img)) + STORAGE[img] = faces + else: + faces = [] + + # Count + yielded_count = 0 + + # Iterate through all the faces + for i, box in enumerate(faces): + if debug: + print(box) + + # Basic information + x1, y1, x2, y2, prob = *map(int, box[:-1]), box[-1] + w, h = int((enlarge_face_factor - 1) * (x2 - x1) / 2), int( + (enlarge_face_factor - 1) * (y2 - y1) / 2 + ) + x1, y1, x2, y2 = ( + max(0, x1 - w), + max(0, y1 - h), + min(img_w, x2 + w), + min(img_h, y2 + h), + ) + + # Check yielded count; stop if reaching limit + if limit is not None and yielded_count > limit: + break + else: + yielded_count += 1 + + # Get all the information for the fields + result_tuple = [i] + for field in output_fields: + if field == "cropped-image": + cropped_img = img[y1:y2, x1:x2, :] + result_tuple.append(cropped_img) + if dump_image: + save_temporary_image(id(img), i, field, cropped_img) + elif field == "bbox-x": + result_tuple.append(x1) + elif field == "bbox-y": + result_tuple.append(y1) + elif field == "bbox-w": + result_tuple.append(x2 - x1) + elif field == "bbox-h": + result_tuple.append(y2 - y1) + + # Debug + if debug: + print(result_tuple) + + # Generate the face + yield (prob, tuple(result_tuple)) + + # Remove the item and register a foreign predicate + return do_face_detect def save_temporary_image(img_id, i, kind, img_tensor): - # First get the directory - directory = os.path.join(DUMP_IMAGE_PATH, f"{img_id}") - if not os.path.exists(directory): - os.makedirs(directory) - - # Dump the image - file_name = os.path.join(directory, f"{kind}-{i}.jpg") - img = Image.fromarray(img_tensor.numpy()) - img.save(open(file_name, "w")) + # First get the directory + directory = os.path.join(DUMP_IMAGE_PATH, f"{img_id}") + if not os.path.exists(directory): + os.makedirs(directory) + + # Dump the image + file_name = os.path.join(directory, f"{kind}-{i}.jpg") + img = Image.fromarray(img_tensor.numpy()) + img.save(open(file_name, "w")) diff --git a/etc/scallopy-plugins/gpt/readme.md b/etc/scallopy-plugins/gpt/readme.md index e69de29b..1b1035d5 100644 --- a/etc/scallopy-plugins/gpt/readme.md +++ b/etc/scallopy-plugins/gpt/readme.md @@ -0,0 +1,37 @@ +# Scallop GPT + +## Installation + +To install from source, please use the following command: + +``` bash +$ make -C etc/scallopy-plugins install-gpt +``` + +For developers, you can do + +``` bash +$ make -C etc/scallopy-plugins develop-gpt +``` + +As such, you can edit in-source and have the changes reflected in your execution. + +## Usage + +The Scallop GPT plugin provides the following constructs: + +- (Foreign Attribute) `@gpt` +- (Foreign Attribute) `@gpt_extract_info` +- (Foreign Attribute) `@gpt_encoder` +- (Foreign Predicate) `gpt(bound input: String, output: String)` +- (Foreign Function) `$gpt(String) -> String` + +### Foreign Attribute `@gpt` + +### Foreign Attribute `@gpt_extract_info` + +### Foreign Attribute `@gpt_encoder` + +### Foreign Predicate `gpt` + +### Foreign Function `$gpt` diff --git a/etc/scallopy-plugins/gpt/src/scallop_gpt/__init__.py b/etc/scallopy-plugins/gpt/src/scallop_gpt/__init__.py index 7bd59e5a..2b966876 100644 --- a/etc/scallopy-plugins/gpt/src/scallop_gpt/__init__.py +++ b/etc/scallopy-plugins/gpt/src/scallop_gpt/__init__.py @@ -13,6 +13,7 @@ def load_into_context(ctx: scallopy.ScallopContext): ctx.register_foreign_attribute(fa_gpt.gpt) ctx.register_foreign_function(ff_gpt.gpt) ctx.register_foreign_predicate(fp_gpt.gpt) + fa_extract_info.reset_memo() if scallopy.torch_tensor_enabled(): ctx.register_foreign_attribute(fa_encoder.gpt_encoder) diff --git a/etc/scallopy-plugins/gpt/src/scallop_gpt/config.py b/etc/scallopy-plugins/gpt/src/scallop_gpt/config.py index 2bcc7a5a..be0ac91e 100644 --- a/etc/scallopy-plugins/gpt/src/scallop_gpt/config.py +++ b/etc/scallopy-plugins/gpt/src/scallop_gpt/config.py @@ -50,14 +50,14 @@ def configure(args): openai.api_key = api_key # Set request limit - NUM_ALLOWED_REQUESTS = args.num_allowed_openai_request + NUM_ALLOWED_REQUESTS = args["num_allowed_openai_request"] NUM_PERFORMED_REQUESTS = 0 # Set model - MODEL = args.openai_gpt_model + MODEL = args["openai_gpt_model"] # Set temperature - TEMPERATURE = args.openai_gpt_temperature + TEMPERATURE = args["openai_gpt_temperature"] def raise_unconfigured(): diff --git a/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_encoder.py b/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_encoder.py index dc1b30ad..71316db6 100644 --- a/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_encoder.py +++ b/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_encoder.py @@ -1,7 +1,11 @@ from typing import * -import openai +from openai import OpenAI + +client = OpenAI() import torch +import pickle +import os import scallopy from scallop_gpu import get_device @@ -11,9 +15,8 @@ FA_NAME = "gpt_encoder" ERR_HEAD = f"[@{FA_NAME}]" - @scallopy.foreign_attribute -def gpt_encoder(item, *, debug: bool = False, model: str = "text-embedding-ada-002"): +def gpt_encoder(item, *, debug: bool = False, model: str = "text-embedding-ada-002", storage_path: str = None): # Check if the annotation is on function type decl assert item.is_function_decl(), f"{ERR_HEAD} has to be an attribute of a function type declaration" @@ -22,13 +25,19 @@ def gpt_encoder(item, *, debug: bool = False, model: str = "text-embedding-ada-0 assert len(arg_types) == 1 and arg_types[0].is_string(), f"{ERR_HEAD} expects only one `String` argument" assert item.function_decl_ret_type().is_tensor(), f"{ERR_HEAD} expects that the return type is `Tensor`" - STORAGE = {} + if not storage_path is None and os.path.exists(storage_path): + STORAGE = pickle.load(open(storage_path, "rb")) + else: + STORAGE = {} # Generate foreign function @scallopy.foreign_function(name=item.function_decl_name()) def encode_text(text: str) -> scallopy.Tensor: + # print("fa encoder encode text start") + # Check memoization if text in STORAGE: + # print("no need to query") pass else: # Make sure that we can do request @@ -38,13 +47,15 @@ def encode_text(text: str) -> scallopy.Tensor: print(f"{ERR_HEAD} Querying `{model}` for text `{text}`") # Memoize the response - response = openai.Embedding.create(input=[text], model=model) - embedding = response['data'][0]['embedding'] + response = client.embeddings.create(input=[text], model=model) + embedding = response.data[0].embedding if debug: print(f"{ERR_HEAD} Obtaining response: {response}") STORAGE[text] = embedding + if not storage_path is None: + pickle.dump(STORAGE, open(storage_path, 'wb')) # Return device = get_device() diff --git a/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_extract_info.py b/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_extract_info.py index 4067bc83..489faf26 100644 --- a/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_extract_info.py +++ b/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_extract_info.py @@ -222,3 +222,7 @@ def parse_response_json(response, expected_keys): raise Exception(f"{ERR_HEAD} Unexpected output json type {type(response_json)}") else: raise Exception(f"{ERR_HEAD} Unknown GPT response role: {role}") + +def reset_memo(): + global _RESPONSE_STORAGE + _RESPONSE_STORAGE = {} \ No newline at end of file diff --git a/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_gpt.py b/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_gpt.py index 7894ce0b..32f8c2c2 100644 --- a/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_gpt.py +++ b/etc/scallopy-plugins/gpt/src/scallop_gpt/fa_gpt.py @@ -110,14 +110,15 @@ def fill_prompt( for example in examples: few_shot_prompt = prompt - few_shot_json = [] + + few_shot_json = {} for (arg_pattern, fill) in zip(arg_patterns[:num_bounded], example[:num_bounded]): few_shot_prompt = few_shot_prompt.replace(arg_pattern, str(fill)) for (arg_pattern, fill) in zip(arg_patterns[num_bounded:], example[num_bounded:]): - new_fill = str(fill).replace('"', '\\"') - few_shot_json.append(f"\"{arg_pattern[2:-2]}\": \"{new_fill}\"") + key = arg_pattern[2:-2] + few_shot_json[key] = str(fill) - few_shot_prompts.append(few_shot_prompt + '\n{' + ','.join(few_shot_json) + '}') + few_shot_prompts.append(few_shot_prompt + '\n' + json.dumps(few_shot_json)) for (arg_pattern, fill) in zip(arg_patterns[:num_bounded], args): filled_prompt = filled_prompt.replace(arg_pattern, str(fill)) diff --git a/etc/scallopy-plugins/gpt/src/scallop_gpt/fp_gpt.py b/etc/scallopy-plugins/gpt/src/scallop_gpt/fp_gpt.py index c45340ce..1c6615e1 100644 --- a/etc/scallopy-plugins/gpt/src/scallop_gpt/fp_gpt.py +++ b/etc/scallopy-plugins/gpt/src/scallop_gpt/fp_gpt.py @@ -8,7 +8,7 @@ STORAGE = {} @scallopy.foreign_predicate -def gpt(s: str) -> scallopy.Generator[None, str]: +def gpt(s: str) -> scallopy.Facts[None, str]: # Check if the storage already contains the response if s in STORAGE: response = STORAGE[s] diff --git a/etc/scallopy-plugins/gpu/src/scallop_gpu/config.py b/etc/scallopy-plugins/gpu/src/scallop_gpu/config.py index 7acaf615..f0406ab9 100644 --- a/etc/scallopy-plugins/gpu/src/scallop_gpu/config.py +++ b/etc/scallopy-plugins/gpu/src/scallop_gpu/config.py @@ -11,7 +11,7 @@ def setup_arg_parser(parser: ArgumentParser): def configure(args): - configure_device(args.cuda, args.gpu) + configure_device(args["cuda"], args["gpu"]) def configure_device(use_cuda: bool = False, gpu: Optional[int] = None): diff --git a/etc/scallopy-plugins/makefile b/etc/scallopy-plugins/makefile index 5c35e86d..ad4a6ebe 100644 --- a/etc/scallopy-plugins/makefile +++ b/etc/scallopy-plugins/makefile @@ -1,6 +1,7 @@ DIRECTORY = $(patsubst %/, %, $(sort $(dir $(wildcard */.)))) DEVELOP_TARGETS = $(patsubst %,develop-%,$(DIRECTORY)) INSTALL_TARGETS = $(patsubst %,install-%,$(DIRECTORY)) +BUILD_TARGETS = $(patsubst %,build-%,$(DIRECTORY)) develop: $(DEVELOP_TARGETS) @@ -9,6 +10,13 @@ develop-%: install: $(INSTALL_TARGETS) -install-%: - cd $*; python -m build +install-%: build-% cd $*; find dist -name "*.whl" -print | xargs pip install --force-reinstall + +build: $(BUILD_TARGETS) + +build-%: + cd $*; python -m build + +clean: + cd $*; rm -rf dist diff --git a/etc/scallopy-plugins/opencv/pyproject.toml b/etc/scallopy-plugins/opencv/pyproject.toml index 5bf97c69..b5162fb3 100644 --- a/etc/scallopy-plugins/opencv/pyproject.toml +++ b/etc/scallopy-plugins/opencv/pyproject.toml @@ -3,6 +3,7 @@ name = "scallop-opencv" version = "0.0.1" dependencies = [ "opencv-python", + "imgurpython", "torch", ] diff --git a/etc/scallopy-plugins/opencv/src/scallop_opencv/__init__.py b/etc/scallopy-plugins/opencv/src/scallop_opencv/__init__.py index 0c1e346e..eae8f949 100644 --- a/etc/scallopy-plugins/opencv/src/scallop_opencv/__init__.py +++ b/etc/scallopy-plugins/opencv/src/scallop_opencv/__init__.py @@ -5,15 +5,18 @@ from .bg_blur import bg_blur from .color_pop import color_pop from .crop_image import crop_image -from .load_image import load_image +from .load_image import load_image, load_image_url from .save_image import save_image from .tag_image import tag_image +from .upload_image import upload_imgur def load_into_context(ctx: scallopy.Context): - ctx.register_foreign_function(bg_blur) - ctx.register_foreign_function(color_pop) - ctx.register_foreign_function(crop_image) - ctx.register_foreign_function(load_image) - ctx.register_foreign_function(save_image) - ctx.register_foreign_function(tag_image) + ctx.register_foreign_function(bg_blur) + ctx.register_foreign_function(color_pop) + ctx.register_foreign_function(crop_image) + ctx.register_foreign_function(load_image) + ctx.register_foreign_function(load_image_url) + ctx.register_foreign_function(save_image) + ctx.register_foreign_function(tag_image) + ctx.register_foreign_function(upload_imgur) diff --git a/etc/scallopy-plugins/opencv/src/scallop_opencv/config.py b/etc/scallopy-plugins/opencv/src/scallop_opencv/config.py index b2d9bfde..ddce36ed 100644 --- a/etc/scallopy-plugins/opencv/src/scallop_opencv/config.py +++ b/etc/scallopy-plugins/opencv/src/scallop_opencv/config.py @@ -11,4 +11,4 @@ def setup_arg_parser(parser: ArgumentParser): def configure(args): global SAVE_IMAGE_PATH - SAVE_IMAGE_PATH = args.save_image_path + SAVE_IMAGE_PATH = args["save_image_path"] diff --git a/etc/scallop-cli/res/testing/fonts/NotoSansMono-Bold.ttf b/etc/scallopy-plugins/opencv/src/scallop_opencv/fonts/NotoSansMono-Bold.ttf similarity index 100% rename from etc/scallop-cli/res/testing/fonts/NotoSansMono-Bold.ttf rename to etc/scallopy-plugins/opencv/src/scallop_opencv/fonts/NotoSansMono-Bold.ttf diff --git a/etc/scallopy-plugins/opencv/src/scallop_opencv/load_image.py b/etc/scallopy-plugins/opencv/src/scallop_opencv/load_image.py index 1231f35b..d331d1fd 100644 --- a/etc/scallopy-plugins/opencv/src/scallop_opencv/load_image.py +++ b/etc/scallopy-plugins/opencv/src/scallop_opencv/load_image.py @@ -1,18 +1,31 @@ import scallopy -# For memoization -STORAGE = {} @scallopy.foreign_function def load_image(image_dir: str) -> scallopy.Tensor: - # Import - import torch - import numpy - from PIL import Image + # Import + import torch + import numpy + from PIL import Image - # Load image - image = Image.open(image_dir).convert('RGB') - image_tensor = torch.tensor(numpy.asarray(image)) + # Load image + image = Image.open(image_dir).convert("RGB") + image_tensor = torch.tensor(numpy.asarray(image)) - # Return - return image_tensor + # Return + return image_tensor + + +@scallopy.foreign_function +def load_image_url(image_url: str) -> scallopy.Tensor: + import torch + import numpy + import requests + from io import BytesIO + from PIL import Image + + response = requests.get(image_url) + image = Image.open(BytesIO(response.content)).convert("RGB") + image_tensor = torch.tensor(numpy.asarray(image)) + + return image_tensor diff --git a/etc/scallopy-plugins/opencv/src/scallop_opencv/save_image.py b/etc/scallopy-plugins/opencv/src/scallop_opencv/save_image.py index 924d322c..661077b3 100644 --- a/etc/scallopy-plugins/opencv/src/scallop_opencv/save_image.py +++ b/etc/scallopy-plugins/opencv/src/scallop_opencv/save_image.py @@ -3,26 +3,27 @@ from .config import SAVE_IMAGE_PATH + @scallopy.foreign_function -def save_image(img_tensor: scallopy.Tensor, img_name: str = None) -> bool: - from PIL import Image +def save_image(img_tensor: scallopy.Tensor, img_name: str = None) -> str: + from PIL import Image - # Convert image - img = Image.fromarray(img_tensor.numpy()) + # Convert image + img = Image.fromarray(img_tensor.numpy()) - # Default name - if img_name is None: - img_name = id(img) + # Default name + if img_name is None: + img_name = id(img) - # First get the directory - directory = SAVE_IMAGE_PATH - if not os.path.exists(directory): - os.makedirs(directory) - file_name = os.path.join(directory, f"{img_name}.jpg") + # First get the directory + directory = SAVE_IMAGE_PATH + if not os.path.exists(directory): + os.makedirs(directory) + file_name = os.path.join(directory, f"{img_name}.jpg") - # Save the image - try: - img.save(open(file_name, "w")) - return True - except: - return False + # Save the image + try: + img.save(open(file_name, "w")) + return file_name + except: + return "" diff --git a/etc/scallopy-plugins/opencv/src/scallop_opencv/tag_image.py b/etc/scallopy-plugins/opencv/src/scallop_opencv/tag_image.py index 7d296d8b..a2062524 100644 --- a/etc/scallopy-plugins/opencv/src/scallop_opencv/tag_image.py +++ b/etc/scallopy-plugins/opencv/src/scallop_opencv/tag_image.py @@ -2,65 +2,75 @@ import os """Code adapted from https://github.com/allenai/visprog.git""" + + @scallopy.foreign_function def tag_image( - img_tensor: scallopy.Tensor, - bbox_x: scallopy.u32, - bbox_y: scallopy.u32, - bbox_w: scallopy.u32, - bbox_h: scallopy.u32, - label: str = None, - color: str = None, - width: int = None, - font_size: int = None, - enlarge_factor: float = None, + img_tensor: scallopy.Tensor, + bbox_x: scallopy.u32, + bbox_y: scallopy.u32, + bbox_w: scallopy.u32, + bbox_h: scallopy.u32, + label: str = None, + color: str = None, + width: int = None, + font_size: int = None, + enlarge_factor: float = None, ) -> scallopy.Tensor: - from PIL import Image, ImageDraw, ImageFont - import torch - import numpy + from PIL import Image, ImageDraw, ImageFont + import torch + import numpy - # Process image - img = Image.fromarray(img_tensor.numpy()) - img_w, img_h = img.size + # Process image + img = Image.fromarray(img_tensor.numpy()) + img_w, img_h = img.size - # Default color - if color is None: - color = "green" + # Default color + if color is None: + color = "green" - # Default width - if width is None: - width = 4 + # Default width + if width is None: + width = 4 - # Default font dize - if font_size is None: - font_size = 16 + # Default font dize + if font_size is None: + font_size = 16 - # Default enlarge factor - if enlarge_factor is None: - enlarge_factor = 1 + # Default enlarge factor + if enlarge_factor is None: + enlarge_factor = 1 - # Enlarge bounding box - w, h = int(enlarge_factor * bbox_w), int(enlarge_factor * bbox_h) - x, y = bbox_x - int((w - bbox_w) / 2), bbox_y - int((h - bbox_h) / 2) + # Enlarge bounding box + w, h = int(enlarge_factor * bbox_w), int(enlarge_factor * bbox_h) + x, y = bbox_x - int((w - bbox_w) / 2), bbox_y - int((h - bbox_h) / 2) - # Clip bounding box - x1, y1, x2, y2 = min(x, img_w - 1), min(y, img_h - 1), min(x + w, img_w), min(y + h, img_h) + # Clip bounding box + x1, y1, x2, y2 = ( + min(x, img_w - 1), + min(y, img_h - 1), + min(x + w, img_w), + min(y + h, img_h), + ) - # Draw the box - tagged_img = img.copy() - draw = ImageDraw.Draw(tagged_img) - draw.rectangle((x1, y1, x2, y2), outline=color, width=width) + # Draw the box + tagged_img = img.copy() + draw = ImageDraw.Draw(tagged_img) + draw.rectangle((x1, y1, x2, y2), outline=color, width=width) - # Draw the label - if label is not None: - font_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../res/testing/fonts/NotoSansMono-Bold.ttf") - font = ImageFont.truetype(font_path, font_size) - fw, fh = font.getsize(label) - if x1 + fw > img_w: - x1 -= fw - (x2 - x1) - if y2 + fh > img_h: - y2 -= fh - draw.rectangle((x1, y2, x1 + fw, y2 + fh), fill=color) - draw.text((x1, y2), label, fill='white', font=font) + # Draw the label + if label is not None: + font_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "fonts/NotoSansMono-Bold.ttf", + ) + font = ImageFont.truetype(font_path, font_size) + _, _, fw, fh = font.getbbox(label) + if x1 + fw > img_w: + x1 -= fw - (x2 - x1) + if y2 + fh > img_h: + y2 -= fh + draw.rectangle((x1, y2, x1 + fw, y2 + fh), fill=color) + draw.text((x1, y2), label, fill="white", font=font) - return torch.tensor(numpy.asarray(tagged_img)) + return torch.tensor(numpy.asarray(tagged_img)) diff --git a/etc/scallopy-plugins/opencv/src/scallop_opencv/upload_image.py b/etc/scallopy-plugins/opencv/src/scallop_opencv/upload_image.py new file mode 100644 index 00000000..6da78288 --- /dev/null +++ b/etc/scallopy-plugins/opencv/src/scallop_opencv/upload_image.py @@ -0,0 +1,24 @@ +import scallopy +import os + + +@scallopy.foreign_function +def upload_imgur(img_tensor: scallopy.Tensor) -> str: + from .save_image import save_image + + img_path = save_image(img_tensor) + + if len(img_path) == 0: + return "" + + client_id = os.environ.get("IMGUR_API_ID") + client_secret = os.environ.get("IMGUR_API_SECRET") + if client_id is None or client_secret is None: + return "" + + from imgurpython import ImgurClient + + client = ImgurClient(client_id, client_secret) + response = client.upload_from_path(img_path) + + return response["link"] diff --git a/etc/scallopy-plugins/sam/src/scallop_sam/config.py b/etc/scallopy-plugins/sam/src/scallop_sam/config.py index 91aa2831..98bba0b0 100644 --- a/etc/scallopy-plugins/sam/src/scallop_sam/config.py +++ b/etc/scallopy-plugins/sam/src/scallop_sam/config.py @@ -18,8 +18,8 @@ def setup_arg_parser(parser: ArgumentParser): def configure(args): global _CHECKPOINT try: - if args.sam_checkpoint is not None: - _CHECKPOINT = args.sam_checkpoint + if args["sam_checkpoint"] is not None: + _CHECKPOINT = args["sam_checkpoint"] else: _CHECKPOINT = os.getenv("SAM_CHECKPOINT") if _CHECKPOINT is None: diff --git a/etc/scallopy-plugins/transformers/src/scallop_transformers/owl_vit.py b/etc/scallopy-plugins/transformers/src/scallop_transformers/owl_vit.py index ad54230e..7458c4c8 100644 --- a/etc/scallopy-plugins/transformers/src/scallop_transformers/owl_vit.py +++ b/etc/scallopy-plugins/transformers/src/scallop_transformers/owl_vit.py @@ -143,7 +143,7 @@ def owl_vit( output_arg_types=arg_types[num_bounded:], tag_type=float, ) - def owl_vit_search(*args) -> scallopy.Generator[float, Tuple[str]]: + def owl_vit_search(*args) -> scallopy.Facts[float, Tuple[str]]: # If object_queries is an arg, assume it's a semicolon-separated string if object_queries is None and input_obj_count: assert len(args) == 3 diff --git a/etc/scallopy/Cargo.toml b/etc/scallopy/Cargo.toml index 04f6f6a9..d32a2d9a 100644 --- a/etc/scallopy/Cargo.toml +++ b/etc/scallopy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scallopy" -version = "0.2.1" +version = "0.2.2" edition = "2018" [lib] diff --git a/etc/scallopy/examples/foreign_predicate.py b/etc/scallopy/examples/foreign_predicate.py index 02610576..1813e543 100644 --- a/etc/scallopy/examples/foreign_predicate.py +++ b/etc/scallopy/examples/foreign_predicate.py @@ -1,11 +1,11 @@ from typing import * import scallopy -from scallopy import foreign_predicate, Generator +from scallopy import foreign_predicate, Facts @foreign_predicate -def string_semantic_eq(s1: str, s2: str) -> Generator[float, Tuple]: +def string_semantic_eq(s1: str, s2: str) -> Facts[float, Tuple]: if s1 == "mom" and s2 == "mother": yield (0.99, ()) elif s1 == "mother" and s2 == "mother": diff --git a/etc/scallopy/scallopy/__init__.py b/etc/scallopy/scallopy/__init__.py index 45ae46dd..52d6b619 100644 --- a/etc/scallopy/scallopy/__init__.py +++ b/etc/scallopy/scallopy/__init__.py @@ -2,12 +2,12 @@ from .context import ScallopContext from .provenance import ScallopProvenance from .function import GenericTypeParameter, foreign_function, ForeignFunction -from .predicate import Generator, foreign_predicate, ForeignPredicate +from .predicate import Facts, foreign_predicate, ForeignPredicate from .value_types import * from .tag_types import * from .input_mapping import InputMapping -from .scallopy import torch_tensor_enabled from .syntax import * +from .scallopy import torch_tensor_enabled from . import input_output as io diff --git a/etc/scallopy/scallopy/context.py b/etc/scallopy/scallopy/context.py index 8d4de89b..54be6d49 100644 --- a/etc/scallopy/scallopy/context.py +++ b/etc/scallopy/scallopy/context.py @@ -105,7 +105,12 @@ def __init__( self._test_k = test_k self._history_actions: List[HistoryAction] = [] self._monitors = monitors - self._internal = InternalScallopContext(provenance=provenance, custom_provenance=custom_provenance, k=k) + self._internal = InternalScallopContext( + provenance=provenance, + custom_provenance=custom_provenance, + k=k, + wmc_with_disjunctions=wmc_with_disjunctions, + ) # Load stdlib self._internal.enable_tensor_registry() # Always enable tensor registry for now @@ -164,6 +169,7 @@ def clone( provenance: Optional[str] = None, k: Optional[int] = None, wmc_with_disjunctions: Optional[bool] = None, + monitors: Optional[List[str]] = None, ) -> ScallopContext: """ Clone the current context. This is useful for incremental execution: @@ -181,12 +187,11 @@ def clone( In this example, `ctx2` and `ctx3` will be executed independently, but both could inherit the computation already done on `ctx`. """ - if provenance is None: - # No provenance specified; just return a new context - return ScallopContext(fork_from=self) - else: - # Create a new context - new_ctx = ScallopContext(fork_from=self) + + # Create a new context + new_ctx = ScallopContext(fork_from=self) + + if provenance is not None: # Clone internal context; this process may fail if the provenance is not compatible new_k = k if k is not None else self._k @@ -197,8 +202,13 @@ def clone( new_ctx.provenance = provenance new_ctx._k = k - # Return - return new_ctx + if monitors is not None: + # Update parameters related to provenance + new_ctx._monitors = monitors + new_ctx._internal.load_monitors(monitors) + + # Return + return new_ctx def set_debug_front(self, debug_front: bool = True): """ @@ -432,6 +442,8 @@ def _type_to_scallop_type_str(ty): return "f32" elif type(ty) == str: return ty + elif isinstance(ty, TypeVar): + return ty.__name__ else: raise Exception(f"Unknown type `{ty}`") @@ -442,6 +454,9 @@ def _type_to_scallop_type_str(ty): elif type(relation_types) == type or type(relation_types) == str: is_singleton_tuple = True relation_types_tuple = (relation_types,) + elif isinstance(relation_types, TypeVar): + is_singleton_tuple = True + relation_types_tuple = (relation_types.__name__,) else: raise Exception(f"Unknown relation types `{relation_types}`") diff --git a/etc/scallopy/scallopy/forward.py b/etc/scallopy/scallopy/forward.py index bcd9aa8f..4c00c27e 100644 --- a/etc/scallopy/scallopy/forward.py +++ b/etc/scallopy/scallopy/forward.py @@ -30,6 +30,7 @@ def __init__( k: int = 3, train_k: Optional[int] = None, test_k: Optional[int] = None, + wmc_with_disjunctions: Optional[bool] = None, early_discard: Optional[bool] = None, iter_limit: Optional[int] = None, retain_graph: bool = False, @@ -43,13 +44,17 @@ def __init__( super(ScallopForwardFunction, self).__init__() # Setup the context - self.ctx = ScallopContext( - provenance=provenance, - custom_provenance=custom_provenance, - k=k, - train_k=train_k, - test_k=test_k, - monitors=monitors) + ctx_params = { + "provenance": provenance, + "custom_provenance": custom_provenance, + "k": k, + "train_k": train_k, + "test_k": test_k, + "wmc_with_disjunctions": wmc_with_disjunctions, + "monitors": monitors + } + ctx_params_non_null = {k: v for (k, v) in ctx_params.items() if v is not None} + self.ctx = ScallopContext(**ctx_params_non_null) # Import the file if specified if file is not None: @@ -223,6 +228,8 @@ def _process_one_output_mapping(self, output_mapping): return (False, [_mapping_tuple(t) for t in output_mapping]) elif type(output_mapping) == tuple: return (True, [_mapping_tuple(output_mapping)]) + elif type(output_mapping) == range: + return self._process_one_output_mapping(list(output_mapping)) elif output_mapping is None: return None else: diff --git a/etc/scallopy/scallopy/function.py b/etc/scallopy/scallopy/function.py index 309f26ee..30122ae4 100644 --- a/etc/scallopy/scallopy/function.py +++ b/etc/scallopy/scallopy/function.py @@ -15,18 +15,16 @@ class GenericTypeParameter: def __init__(self, type_family = "Any"): self.id = GenericTypeParameter.COUNTER - if type_family == "Any" or type_family == any or type_family == None: + if type_family == any or type_family == None: self.type_family = "Any" - elif type_family == "Number": - self.type_family = "Number" - elif type_family == "Integer" or type_family == int: + elif type_family == int: self.type_family = "Integer" - elif type_family == "SignedInteger": - self.type_family = "SignedInteger" - elif type_family == "UnsignedInteger": - self.type_family = "UnsignedInteger" - elif type_family == "Float" or type_family == float: + elif type_family == float: self.type_family = "Float" + elif isinstance(type_family, TypeVar): + self.type_family = GenericTypeParameter.sanitize_type_family(type_family.__name__) + elif isinstance(type_family, str): + self.type_family = GenericTypeParameter.sanitize_type_family(type_family) else: raise Exception(f"Unknown type family {type_family}") GenericTypeParameter.COUNTER += 1 @@ -34,6 +32,23 @@ def __init__(self, type_family = "Any"): def __repr__(self): return f"T{self.id}({self.type_family})" + @staticmethod + def sanitize_type_family(tf: str) -> str: + if tf == "Any": + return "Any" + elif tf == "Number": + return "Number" + elif tf == "Integer": + return "Integer" + elif tf == "SignedInteger": + return "SignedInteger" + elif tf == "UnsignedInteger": + return "UnsignedInteger" + elif tf == "Float": + return "Float" + else: + raise Exception(f"Unknown type family {tf}") + # Scallop Data Type class Type: @@ -45,41 +60,26 @@ def __init__(self, value): elif isinstance(value, syntax.AstTypeNode): self.kind = "base" self.type = value.name() - elif value == "Float" or value == float: + elif isinstance(value, TypeVar): + (self.kind, self.type) = Type.sanitize_type_str(value.__name__) + elif value == float: self.kind = "family" self.type_family = "Float" - elif value == "Integer" or value == int: + elif value == int: self.kind = "family" self.type_family = "Integer" - elif value == "UnsignedInteger": - self.kind = "family" - self.type_family = "UnsignedInteger" - elif value == "SignedInteger": - self.kind = "family" - self.type_family = "SignedInteger" - elif value == "Number": - self.kind = "family" - self.type_family = "Number" - elif value == "Any" or value == any or value == None: + elif value == any or value == None: self.kind = "family" self.type_family = "Any" - elif value == "char": - self.kind = "base" - self.type = "char" - elif value == "bool" or value == bool: + elif value == bool: self.kind = "base" self.type = "bool" - elif value == "String" or value == str: + elif value == str: self.kind = "base" self.type = "String" - elif value == "Tensor" or value == torch_importer.Tensor: + elif value == torch_importer.Tensor: self.kind = "base" self.type = "Tensor" - elif value == "i8" or value == "i16" or value == "i32" or value == "i64" or value == "i128" or value == "isize" or \ - value == "u8" or value == "u16" or value == "u32" or value == "u64" or value == "u128" or value == "usize" or \ - value == "f32" or value == "f64": - self.kind = "base" - self.type = value else: raise Exception(f"Unknown scallop function type annotation {value}") @@ -93,6 +93,30 @@ def __repr__(self): else: raise Exception(f"Unknown parameter kind {self.kind}") + @staticmethod + def sanitize_type_str(value: str) -> Tuple[str, str]: + if value == "Float": + return ("family", "Float") + elif value == "Integer": + return ("family", "Integer") + elif value == "UnsignedInteger": + return ("family", "UnsignedInteger") + elif value == "SignedInteger": + return ("family", "SignedInteger") + elif value == "Number": + return ("family", "Number") + elif value == "Any": + return ("family", "Any") + elif value == "String": + return ("base", "String") + elif value == "Tensor": + return ("base", "Tensor") + elif value == "i8" or value == "i16" or value == "i32" or value == "i64" or value == "i128" or value == "isize" or \ + value == "u8" or value == "u16" or value == "u32" or value == "u64" or value == "u128" or value == "usize" or \ + value == "f32" or value == "f64" or value == "bool" or value == "char" or value == "String" or value == "Tensor" or \ + value == "Symbol" or value == "Entity": + return ("base", value) + def is_base(self): return self.kind == "base" diff --git a/etc/scallopy/scallopy/predicate.py b/etc/scallopy/scallopy/predicate.py index bcfc373d..cd9b39b4 100644 --- a/etc/scallopy/scallopy/predicate.py +++ b/etc/scallopy/scallopy/predicate.py @@ -1,4 +1,5 @@ -from typing import * +import typing +from typing import TypeVar, Generic, Union, Tuple, Callable, List, Optional, Any, ForwardRef, ClassVar import inspect from . import torch_importer @@ -31,9 +32,13 @@ class Type: def __init__(self, value): if isinstance(value, ForwardRef): - value = value.__forward_arg__ - if isinstance(value, syntax.AstTypeNode): + self.type = Type.sanitize_type_str(value.__forward_arg__) + elif isinstance(value, syntax.AstTypeNode): self.type = value.name() + elif isinstance(value, TypeVar): + self.type = Type.sanitize_type_str(value.__name__) + elif isinstance(value, str): + self.type = Type.sanitize_type_str(value) elif value == float: self.type = "f32" elif value == int: @@ -42,26 +47,40 @@ def __init__(self, value): self.type = "bool" elif value == str: self.type = "String" - elif value == "Tensor" or value == torch_importer.Tensor: + elif value == torch_importer.Tensor: self.type = "Tensor" - elif value == "i8" or value == "i16" or value == "i32" or value == "i64" or value == "i128" or value == "isize" or \ + else: + raise Exception(f"Unknown scallop predicate type annotation `{value}`") + + def __repr__(self): + return self.type + + @staticmethod + def sanitize_type_str(value): + if value == "i8" or value == "i16" or value == "i32" or value == "i64" or value == "i128" or value == "isize" or \ value == "u8" or value == "u16" or value == "u32" or value == "u64" or value == "u128" or value == "usize" or \ value == "f32" or value == "f64" or \ - value == "bool" or value == "char" or value == "String" or \ + value == "bool" or value == "char" or value == "String" or value == "Symbol" or value == "Tensor" or \ value == "DateTime" or value == "Duration" or value == "Entity": - self.type = value + return value elif value in ALIASES: - self.type = ALIASES[value] + return ALIASES[value] else: raise Exception(f"Unknown scallop predicate type annotation `{value}`") - def __repr__(self): - return self.type +TagType = TypeVar("TagType") -class Generator(Generic[TypeVar("TagType"), TypeVar("TupleType")]): - pass +TupleType = TypeVar("TupleType") +Facts = ClassVar[typing.Generator[ + Union[ + Tuple[TagType, TupleType], + TupleType, + ], + None, + None, +]] class ForeignPredicate: """ @@ -148,7 +167,7 @@ def foreign_predicate( ``` python @scallopy.foreign_function - def string_chars(s: str) -> scallopy.Generator[Tuple[int, char]]: + def string_chars(s: str) -> scallopy.Facts[Tuple[int, char]]: for (i, c) in enumerate(s): yield (i, c) ``` @@ -189,12 +208,16 @@ def string_chars(s: str) -> scallopy.Generator[Tuple[int, char]]: if output_arg_types is None: if signature.return_annotation is None: raise Exception(f"Return type annotation not provided") - elif signature.return_annotation.__dict__["__origin__"] != Generator: - raise Exception(f"Return type must be Generator") + elif not str(signature.return_annotation).startswith("typing.ClassVar[typing.Generator[typing.Union[typing.Tuple["): + raise Exception(f"Return type must be Facts") else: - args = signature.return_annotation.__dict__["__args__"] + args = signature.return_annotation \ + .__dict__["__args__"][0] \ + .__dict__["__args__"][0] \ + .__dict__["__args__"][0] \ + .__dict__["__args__"] if len(args) != 2: - raise Exception(f"Generator must have 2 type arguments") + raise Exception(f"Facts must have 2 type arguments") # Produce return tag type return_tag_type = tag_types.get_tag_type(args[0]) diff --git a/etc/scallopy/scallopy/scallopy.pyi b/etc/scallopy/scallopy/scallopy.pyi index b20edcf4..4ff7f4f8 100644 --- a/etc/scallopy/scallopy/scallopy.pyi +++ b/etc/scallopy/scallopy/scallopy.pyi @@ -48,6 +48,8 @@ class InternalScallopContext: def add_monitors(self, monitors: List[str]): ... + def load_monitors(self, monitors: List[str]): ... + def run(self, iter_limit: Optional[int]) -> None: ... def run_with_debug_tag(self, iter_limit: Optional[int]) -> None: ... diff --git a/etc/scallopy/scallopy/stdlib.py b/etc/scallopy/scallopy/stdlib.py index 6993fe79..922a2dc9 100644 --- a/etc/scallopy/scallopy/stdlib.py +++ b/etc/scallopy/scallopy/stdlib.py @@ -1,17 +1,17 @@ from typing import Tuple from . import value_types -from .predicate import foreign_predicate, Generator +from .predicate import foreign_predicate, Facts @foreign_predicate(name="soft_eq", type_params=[value_types.Tensor]) -def soft_eq_tensor(x: value_types.Tensor, y: value_types.Tensor) -> Generator[value_types.Tensor, Tuple]: +def soft_eq_tensor(x: value_types.Tensor, y: value_types.Tensor) -> Facts[value_types.Tensor, Tuple]: import torch cs = torch.cosine_similarity(x, y, dim=0) prob = cs + 1.0 / 2.0 yield (prob, ()) @foreign_predicate(name="soft_neq", type_params=[value_types.Tensor]) -def soft_neq_tensor(x: value_types.Tensor, y: value_types.Tensor) -> Generator[value_types.Tensor, Tuple]: +def soft_neq_tensor(x: value_types.Tensor, y: value_types.Tensor) -> Facts[value_types.Tensor, Tuple]: import torch cs = torch.cosine_similarity(x, y, dim=0) prob = 1.0 - (cs + 1.0 / 2.0) diff --git a/etc/scallopy/scallopy/syntax.py b/etc/scallopy/scallopy/syntax.py index e9a0260b..6073dbae 100644 --- a/etc/scallopy/scallopy/syntax.py +++ b/etc/scallopy/scallopy/syntax.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, List, Any +from typing import Optional, Union, Dict, List, Any from . import value_types @@ -118,7 +118,7 @@ def to_python_type(self): value_types.Entity: str, }[self._type_name] - def parse_value(self, text: str) -> float | int | bool | str: + def parse_value(self, text: str) -> Union[float, int, bool, str]: ty = self.to_python_type() if ty == bool: if text == "true" or text == "True": return True diff --git a/etc/scallopy/scallopy/value_types.py b/etc/scallopy/scallopy/value_types.py index e7a7bcd8..61727191 100644 --- a/etc/scallopy/scallopy/value_types.py +++ b/etc/scallopy/scallopy/value_types.py @@ -1,31 +1,33 @@ +from typing import TypeVar + # Base types -i8 = "i8" -i16 = "i16" -i32 = "i32" -i64 = "i64" -i128 = "i128" -isize = "isize" -u8 = "u8" -u16 = "u16" -u32 = "u32" -u64 = "u64" -u128 = "u128" -usize = "usize" -f32 = "f32" -f64 = "f64" -bool = "bool" -char = "char" -String = "String" -Symbol = "Symbol" -DateTime = "DateTime" -Duration = "Duration" -Entity = "Entity" -Tensor = "Tensor" +i8 = TypeVar("i8") +i16 = TypeVar("i16") +i32 = TypeVar("i32") +i64 = TypeVar("i64") +i128 = TypeVar("i128") +isize = TypeVar("isize") +u8 = TypeVar("u8") +u16 = TypeVar("u16") +u32 = TypeVar("u32") +u64 = TypeVar("u64") +u128 = TypeVar("u128") +usize = TypeVar("usize") +f32 = TypeVar("f32") +f64 = TypeVar("f64") +bool = TypeVar("bool") +char = TypeVar("char") +String = TypeVar("String") +Symbol = TypeVar("Symbol") +DateTime = TypeVar("DateTime") +Duration = TypeVar("Duration") +Entity = TypeVar("Entity") +Tensor = TypeVar("Tensor") # Type families -Any = "Any" -Number = "Number" -Integer = "Integer" -SignedInteger = "SignedInteger" -UnsignedInteger = "UnsignedInteger" -Float = "Float" +Any = TypeVar("Any") +Number = TypeVar("Number") +Integer = TypeVar("Integer") +SignedInteger = TypeVar("SignedInteger") +UnsignedInteger = TypeVar("UnsignedInteger") +Float = TypeVar("Float") diff --git a/etc/scallopy/src/context.rs b/etc/scallopy/src/context.rs index 24912a91..d6c015bb 100644 --- a/etc/scallopy/src/context.rs +++ b/etc/scallopy/src/context.rs @@ -201,7 +201,12 @@ impl Context { } /// Create a new scallop context with a different provenance as the current context - fn clone_with_new_provenance(&self, provenance: &str, k: usize, wmc_with_disjunctions: bool) -> Result { + fn clone_with_new_provenance( + &self, + provenance: &str, + k: usize, + wmc_with_disjunctions: bool, + ) -> Result { // Check provenance type match provenance { "unit" => Ok(Self { @@ -243,7 +248,10 @@ impl Context { ctx: ContextEnum::TopBottomKClauses(match_context_except_custom!( &self.ctx, c, - c.clone_with_new_provenance(top_bottom_k_clauses::TopBottomKClausesProvenance::new(k, wmc_with_disjunctions),) + c.clone_with_new_provenance(top_bottom_k_clauses::TopBottomKClausesProvenance::new( + k, + wmc_with_disjunctions + ),) )?), }), "diffminmaxprob" => Ok(Self { @@ -292,14 +300,20 @@ impl Context { ctx: ContextEnum::DiffTopKProofs(match_context_except_custom!( &self.ctx, c, - c.clone_with_new_provenance(diff_top_k_proofs::DiffTopKProofsProvenance::new(k, wmc_with_disjunctions),) + c.clone_with_new_provenance(diff_top_k_proofs::DiffTopKProofsProvenance::new( + k, + wmc_with_disjunctions + ),) )?), }), "difftopbottomkclauses" => Ok(Self { ctx: ContextEnum::DiffTopBottomKClauses(match_context_except_custom!( &self.ctx, c, - c.clone_with_new_provenance(diff_top_bottom_k_clauses::DiffTopBottomKClausesProvenance::new(k, wmc_with_disjunctions),) + c.clone_with_new_provenance(diff_top_bottom_k_clauses::DiffTopBottomKClausesProvenance::new( + k, + wmc_with_disjunctions + ),) )?), }), "custom" => Err(BindingError::CustomProvenanceUnsupported), @@ -348,6 +362,11 @@ impl Context { match_context!(&mut self.ctx, c, c.add_monitors(&monitors)) } + /// Load monitors to the system (discarding all existing ones) + fn load_monitors(&mut self, monitors: Vec<&str>) { + match_context!(&mut self.ctx, c, c.load_monitors(&monitors)) + } + /// Compile the surface program stored in the scallopy context into the ram program. /// /// This function is usually used before creating a forward function. diff --git a/etc/scallopy/tests/basics.py b/etc/scallopy/tests/basics.py index 186cf113..a7b62354 100644 --- a/etc/scallopy/tests/basics.py +++ b/etc/scallopy/tests/basics.py @@ -30,13 +30,24 @@ def test_fibonacci_demand(self): def test_digit_sum(self): ctx = scallopy.ScallopContext() - ctx.add_relation("digit", int) - ctx.add_facts("digit", [(i,) for i in range(10)]) - ctx.add_facts("digit", [(i,) for i in range(10)]) - ctx.add_rule("sum_2(a + b) = digit(a), digit(b)") + ctx.add_relation("digit1", int) + ctx.add_relation("digit2", int) + ctx.add_facts("digit1", [(i,) for i in range(10)]) + ctx.add_facts("digit2", [(i,) for i in range(10)]) + ctx.add_rule("sum_2(a + b) = digit1(a), digit2(b)") ctx.run() self.assertEqual(list(ctx.relation("sum_2")), [(i,) for i in range(19)]) + def test_softmax(self): + ctx = scallopy.ScallopContext(provenance="minmaxprob") + ctx.add_relation("digit", int) + ctx.add_facts("digit", [(0.5, (i,)) for i in range(10)]) + ctx.add_rule("softmax_digit(n) = n := softmax(d: digit(d))") + ctx.run() + for (i, (prob, (p_i,))) in enumerate(list(ctx.relation("softmax_digit"))): + self.assertAlmostEqual(prob, 0.1) + self.assertEqual(i, p_i) + def test_rule_weight(self): ctx = scallopy.ScallopContext(provenance="minmaxprob") ctx.add_relation("speaks", (int, str)) diff --git a/etc/scallopy/tests/foreign_attribute.py b/etc/scallopy/tests/foreign_attribute.py index 8bf91176..75535cc1 100644 --- a/etc/scallopy/tests/foreign_attribute.py +++ b/etc/scallopy/tests/foreign_attribute.py @@ -39,7 +39,7 @@ def string_join(item, separator=","): # Generate the foreign predicate @scallopy.foreign_predicate(name=relation_decl.name.name, input_arg_types=arg_types[:-1], output_arg_types=arg_types[-1:]) - def f(*args) -> scallopy.Generator[None, str]: + def f(*args) -> scallopy.Facts[None, str]: yield (separator.join(args),) # Return the foreign predicate to indicate that we need to remove the item and add a foreign predicate diff --git a/etc/scallopy/tests/foreign_predicate.py b/etc/scallopy/tests/foreign_predicate.py index da15d92e..9011ae5f 100644 --- a/etc/scallopy/tests/foreign_predicate.py +++ b/etc/scallopy/tests/foreign_predicate.py @@ -6,7 +6,7 @@ class TestForeignPredicate(unittest.TestCase): def test_foreign_predicate_range(self): @scallopy.foreign_predicate - def my_range(a: int, b: int) -> scallopy.Generator[None, Tuple[int]]: + def my_range(a: int, b: int) -> scallopy.Facts[None, Tuple[int]]: for i in range(a, b): yield (i,) @@ -18,7 +18,7 @@ def my_range(a: int, b: int) -> scallopy.Generator[None, Tuple[int]]: def test_fp_entity(self): @scallopy.foreign_predicate - def my_dummy_semantic_parser(s: str) -> scallopy.Generator[None, Tuple[scallopy.Entity]]: + def my_dummy_semantic_parser(s: str) -> scallopy.Facts[None, Tuple[scallopy.Entity]]: if s == "If I have 3 apples and 2 pears, how many fruits do I have?": yield ("Add(Const(3), Const(2))",) @@ -44,7 +44,7 @@ def my_dummy_semantic_parser(s: str) -> scallopy.Generator[None, Tuple[scallopy. def test_fp_suppress_warning(self): @scallopy.foreign_predicate(suppress_warning=True) - def dummy(s: str) -> scallopy.Generator[None, str]: + def dummy(s: str) -> scallopy.Facts[None, str]: raise Exception("always false") ctx = scallopy.ScallopContext() diff --git a/etc/scallopy/tests/tensors.py b/etc/scallopy/tests/tensors.py index d8cccd11..2ba6aed5 100644 --- a/etc/scallopy/tests/tensors.py +++ b/etc/scallopy/tests/tensors.py @@ -50,6 +50,7 @@ def test_tensor_3(self): @unittest.skipIf(not scallopy.torch_tensor_enabled(), "not supported in this scallopy version") def test_tensor_backprop_4(self): + torch.manual_seed(1357) x = torch.randn(10, requires_grad=True) y = torch.randn(10) opt = torch.optim.Adam(params=[x], lr=0.01) diff --git a/etc/sclc/Cargo.toml b/etc/sclc/Cargo.toml index ec4a8213..ed1af25d 100644 --- a/etc/sclc/Cargo.toml +++ b/etc/sclc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sclc-core" -version = "0.2.1" +version = "0.2.2" authors = ["Ziyang Li "] edition = "2018" diff --git a/etc/sclc/src/exec.rs b/etc/sclc/src/exec.rs index 7c348b83..c944b418 100644 --- a/etc/sclc/src/exec.rs +++ b/etc/sclc/src/exec.rs @@ -160,9 +160,13 @@ fn main_body(opt: &Options) -> TokenStream { "bool" => quote! { run(boolean::BooleanProvenance::default()); }, "minmaxprob" => quote! { run(min_max_prob::MinMaxProbProvenance::default()); }, "addmultprob" => quote! { run(add_mult_prob::AddMultProbProvenance::default()); }, - "topkproofs" => quote! { run(top_k_proofs::TopKProofsProvenance::::new(#top_k, #wmc_with_disjunctions)); }, + "topkproofs" => { + quote! { run(top_k_proofs::TopKProofsProvenance::::new(#top_k, #wmc_with_disjunctions)); } + } "samplekproofs" => quote! { run(sample_k_proofs::SampleKProofsContext::new(#top_k)); }, - "topbottomkclauses" => quote! { run(top_bottom_k_clauses::TopBottomKClausesContext::::new(#top_k, #wmc_with_disjunctions)); }, + "topbottomkclauses" => { + quote! { run(top_bottom_k_clauses::TopBottomKClausesContext::::new(#top_k, #wmc_with_disjunctions)); } + } p => panic!("Unknown provenance `{}`. Aborting", p), } } else { diff --git a/etc/scli/Cargo.toml b/etc/scli/Cargo.toml index 11767e25..2862a33d 100644 --- a/etc/scli/Cargo.toml +++ b/etc/scli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scli" -version = "0.2.1" +version = "0.2.2" authors = ["Ziyang Li "] edition = "2018" diff --git a/etc/scli/src/main.rs b/etc/scli/src/main.rs index 1580b6d7..b166e48f 100644 --- a/etc/scli/src/main.rs +++ b/etc/scli/src/main.rs @@ -167,7 +167,10 @@ fn main() -> Result<(), String> { interpret(ctx, &opt.input, integrate_opt, predicate_set, monitor_options) } "topbottomkclauses" => { - let ctx = provenance::top_bottom_k_clauses::TopBottomKClausesProvenance::::new(opt.top_k, opt.wmc_with_disjunctions); + let ctx = provenance::top_bottom_k_clauses::TopBottomKClausesProvenance::::new( + opt.top_k, + opt.wmc_with_disjunctions, + ); interpret(ctx, &opt.input, integrate_opt, predicate_set, monitor_options) } _ => Err(format!("Unknown provenance semiring `{}`", opt.provenance)), diff --git a/etc/sclrepl/Cargo.toml b/etc/sclrepl/Cargo.toml index 857d6c18..72f2defb 100644 --- a/etc/sclrepl/Cargo.toml +++ b/etc/sclrepl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sclrepl" -version = "0.2.1" +version = "0.2.2" authors = ["Ziyang Li "] edition = "2018" diff --git a/etc/vscode-scl/CHANGELOG.md b/etc/vscode-scl/CHANGELOG.md index 3e76401b..5af6be15 100644 --- a/etc/vscode-scl/CHANGELOG.md +++ b/etc/vscode-scl/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.1.3, Jan 29, 2024 + +- Add `extern` syntax highlight + ## 0.1.1, Sep 23, 2023 - Improved aggregation syntax to allow for arbitrary aggregates diff --git a/etc/vscode-scl/examples/syntax_test_2.scl b/etc/vscode-scl/examples/syntax_test_2.scl index 1ad28233..82b44d92 100644 --- a/etc/vscode-scl/examples/syntax_test_2.scl +++ b/etc/vscode-scl/examples/syntax_test_2.scl @@ -118,7 +118,8 @@ rel expr_to_string(e, $format("({} {} {})", op1_str, op_str, op2_str)) = expr_to_string(op1, op1_str) and expr_to_string(op2, op2_str) and op_to_string(op, op_str) // Rules with aggregation inside -rel num_students(n) = n := count(a: student(p)) +rel num_students(n) = n := count(a: student(a)) +rel num_students = count(a: student(a)) rel num_paths(n) = n := count!(a, b: path(a, b)) rel my_rule(x) = _ := count(a, b: path(a, b) where c: color(c)) rel my_rule_2() = forall(a, b: path(a, b) and path(a, b) implies edge(a, b)) @@ -137,6 +138,10 @@ rel sample_rule(x) = x := argmin!(a, c, d, b: rel sample_rule(x) = _ := min![x](s: score(x, s)) rel sample_rule(x) = _ := sum[p](s: score(x, s)) rel sample_rule(x) = _ := cross_entropy[i](y_pred, y: pred(i, y_pred) and ground_truth(i, y)) +rel joined_string(x) = _ := string_join<3, d"asdfasdf", true, all>(s: some_string(s)) +rel best_student = argmax(s: score(p, s)) +rel best_student = argmax[p](s: score(p, s)) +rel top_ranked = top<3>(s: score(p, s)) // Nested aggregation rel nested_agg(x) = x := count(x: m := max(x: relation(x))) diff --git a/etc/vscode-scl/package.json b/etc/vscode-scl/package.json index 605f30cc..b85bfd69 100644 --- a/etc/vscode-scl/package.json +++ b/etc/vscode-scl/package.json @@ -2,7 +2,7 @@ "name": "scallop", "displayName": "Scallop Language Syntax Highlight", "description": "Scallop Language Support", - "version": "0.1.1", + "version": "0.1.3", "repository": { "type": "git", "url": "https://github.com/liby99/scallop-v2" diff --git a/etc/vscode-scl/syntaxes/scallop.tmLanguage.json b/etc/vscode-scl/syntaxes/scallop.tmLanguage.json index f59dd5a8..ceea1f5f 100644 --- a/etc/vscode-scl/syntaxes/scallop.tmLanguage.json +++ b/etc/vscode-scl/syntaxes/scallop.tmLanguage.json @@ -410,7 +410,8 @@ { "include": "#basic_aggregation" }, { "include": "#sample_aggregation" }, { "include": "#arg_aggr_aggregation" }, - { "include": "#forall_exists_aggregation" } + { "include": "#forall_exists_aggregation" }, + { "include": "#reduce_rule_sugar" } ] }, "basic_aggregation": { @@ -433,8 +434,8 @@ ] }, "sample_aggregation": { - "begin": "(:=)\\s*([a-zA-Z][a-zA-Z0-9_]*)(!?)(<)(\\d+)(>)\\(", - "end": ":", + "begin": "(:=)\\s*([a-zA-Z][a-zA-Z0-9_]*)(!?)(<)", + "end": "(>)", "beginCaptures": { "1": { "name": "keyword.operator.scallop" @@ -447,17 +448,17 @@ }, "4": { "name": "keyword.operator.scallop" - }, - "5": { - "name": "constant.numeric.scallop" - }, - "6": { + } + }, + "endCaptures": { + "1": { "name": "keyword.operator.scallop" } }, "patterns": [ { "include": "#comment" }, - { "include": "#variable" } + { "include": "#variable" }, + { "include": "#constants" } ] }, "arg_aggr_aggregation": { @@ -503,6 +504,26 @@ { "include": "#variable" } ] }, + "reduce_rule_sugar": { + "match": "(rel)\\s+([a-zA-Z][a-zA-Z0-9_]*)\\s+(=|:-)\\s+([a-zA-Z][a-zA-Z0-9_]*)(!?)", + "captures": { + "1": { + "name": "keyword.control.scallop" + }, + "2": { + "name": "entity.name.function.scallop" + }, + "3": { + "name": "keyword.operator.scallop" + }, + "4": { + "name": "keyword.other.scallop" + }, + "5": { + "name": "keyword.operator.scallop" + } + } + }, "case_is": { "begin": "(case)\\s+([a-zA-Z][a-zA-Z0-9_]*)\\s+(is)\\s+([a-zA-Z][a-zA-Z0-9_]*)\\s*\\(", "end": "\\)", @@ -630,7 +651,7 @@ "name": "variable.parameter.scallop" }, "keyword": { - "match": "\\b(import|type|const|rel|query)\\b", + "match": "\\b(import|extern|type|const|rel|query)\\b", "captures": { "1": { "name": "keyword.control.scallop"