From ff5fab879f245bb6bee800b6b3f864571f9e9d57 Mon Sep 17 00:00:00 2001 From: Ryan Spangler Date: Tue, 3 Dec 2024 16:27:36 -0800 Subject: [PATCH 1/8] checking for identical schemas while merging --- bigraph_schema/type_system.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index 0b6d32f..935d8cd 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -875,7 +875,15 @@ def default_union(schema, core): def union_keys(schema, state): - return set(schema.keys()).union(state.keys()) + keys = {} + for key in schema: + keys[key] = True + for key in state: + keys[key] = True + + return keys + + # return set(schema.keys()).union(state.keys()) def generate_any(core, schema, state, top_schema=None, top_state=None, path=None): @@ -1189,6 +1197,8 @@ def types(self): def merge_schemas(self, current, update): + if current == update: + return update if not isinstance(current, dict): return update if not isinstance(update, dict): @@ -1199,9 +1209,14 @@ def merge_schemas(self, current, update): for key in union_keys(current, update): if key in current: if key in update: + subcurrent = current[key] + subupdate = update[key] + if subcurrent == current or subupdate == update: + continue + merged[key] = self.merge_schemas( - current[key], - update[key]) + subcurrent, + subupdate) else: merged[key] = current[key] else: @@ -3757,7 +3772,7 @@ def generate_map(core, schema, state, top_schema=None, top_state=None, path=None schema, state) - all_keys = set(schema.keys()).union(state.keys()) + all_keys = union_keys(schema, state) # set(schema.keys()).union(state.keys()) for key in all_keys: if is_schema_key(key): @@ -3808,7 +3823,7 @@ def generate_tree(core, schema, state, top_schema=None, top_state=None, path=Non generate_schema = {} generate_state = {} - all_keys = set(schema.keys()).union(state.keys()) + all_keys = union_keys(schema, state) # set(schema.keys()).union(state.keys()) non_schema_keys = [ key for key in all_keys From 95001690cd1d0b473f30ab83aba072e8b02a72d4 Mon Sep 17 00:00:00 2001 From: Eran Date: Wed, 4 Dec 2024 18:16:29 -0500 Subject: [PATCH 2/8] default_tuple --- bigraph_schema/type_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index 935d8cd..45afd2d 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -861,7 +861,7 @@ def default_tuple(schema, core): parts = [] for parameter in schema['_type_parameters']: subschema = schema[f'_{parameter}'] - part = core.default(subschema, core) + part = core.default(subschema) parts.append(part) return tuple(parts) From 9776ca18643410ff74e5f53fcabb4cbfb7598342 Mon Sep 17 00:00:00 2001 From: Eran Date: Fri, 6 Dec 2024 20:46:31 -0500 Subject: [PATCH 3/8] get started with organization of type system --- bigraph_schema/type_system.py | 6410 ++++++++++++++++----------------- 1 file changed, 3191 insertions(+), 3219 deletions(-) diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index 45afd2d..04ca628 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -172,6 +172,9 @@ def remove_path(tree, path): def resolve_path(path): + """ + Given a path that includes '..' steps, resolve the path to a canonical form + """ resolve = [] for step in path: @@ -186,12 +189,10 @@ def resolve_path(path): return tuple(resolve) -def apply_schema(schema, current, update, core): - outcome = core.resolve_schemas(current, update) - return outcome - - def visit_method(schema, state, method, values, core): + """ + Visit a method for a schema and state and apply it, returning the result + """ schema = core.access(schema) method_key = f'_{method}' @@ -224,6 +225,234 @@ def generate_quote(core, schema, state, top_schema=None, top_state=None, path=No return schema, state, top_schema, top_state + +def type_parameters_for(schema): + parameters = [] + for key in schema['_type_parameters']: + subschema = schema.get(f'_{key}', 'any') + parameters.append(subschema) + + return parameters + +# Apply functions +# --------------- + +def apply_schema(schema, current, update, core): + """ + Apply an update to a schema, returning the new schema + """ + outcome = core.resolve_schemas(current, update) + return outcome + +def apply_tree(schema, current, update, core): + leaf_type = core.find_parameter( + schema, + 'leaf') + + if current is None: + current = core.default(leaf_type) + + if isinstance(current, dict) and isinstance(update, dict): + for key, branch in update.items(): + if key == '_add': + current.update(branch) + elif key == '_remove': + for removed_path in branch: + if isinstance(removed_path, str): + removed_path = [removed_path] + current = remove_path(current, removed_path) + elif isinstance(branch, dict): + subschema = schema + if key in schema: + subschema = schema[key] + + current[key] = core.apply( + subschema, + current.get(key), + branch) + + elif core.check(leaf_type, branch): + current[key] = core.apply( + leaf_type, + current.get(key), + branch) + + else: + raise Exception(f'state does not seem to be of leaf type:\n state: {state}\n leaf type: {leaf_type}') + + return current + + elif core.check(leaf_type, current): + return core.apply( + leaf_type, + current, + update) + + else: + raise Exception(f'trying to apply an update to a tree but the values are not trees or leaves of that tree\ncurrent:\n {pf(current)}\nupdate:\n {pf(update)}\nschema:\n {pf(schema)}') + +def apply_any(schema, current, update, core): + if isinstance(current, dict): + return apply_tree( + current, + update, + 'tree[any]', + core) + else: + return update + +def apply_tuple(schema, current, update, core): + parameters = core.parameters_for(schema) + result = [] + + for parameter, current_value, update_value in zip(parameters, current, update): + element = core.apply( + parameter, + current_value, + update_value) + + result.append(element) + + return tuple(result) + +def apply_union(schema, current, update, core): + current_type = find_union_type( + core, + schema, + current) + + update_type = find_union_type( + core, + schema, + update) + + if current_type is None: + raise Exception(f'trying to apply update to union value but cannot find type of value in the union\n value: {current}\n update: {update}\n union: {list(bindings.values())}') + elif update_type is None: + raise Exception(f'trying to apply update to union value but cannot find type of update in the union\n value: {current}\n update: {update}\n union: {list(bindings.values())}') + + # TODO: throw an exception if current_type is incompatible with update_type + + return core.apply( + update_type, + current, + update) + +def apply_boolean(schema, current: bool, update: bool, core=None) -> bool: + """Performs a bit flip if `current` does not match `update`, returning update. Returns current if they match.""" + if current != update: + return update + else: + return current + +def apply_list(schema, current, update, core): + element_type = core.find_parameter( + schema, + 'element') + + if current is None: + current = [] + + if core.check(element_type, update): + result = current + [update] + return result + + elif isinstance(update, list): + result = current + update + # for current_element, update_element in zip(current, update): + # applied = core.apply( + # element_type, + # current_element, + # update_element) + + # result.append(applied) + + return result + else: + raise Exception(f'trying to apply an update to an existing list, but the update is not a list or of element type:\n update: {update}\n element type: {pf(element_type)}') + +def apply_map(schema, current, update, core=None): + if not isinstance(current, dict): + raise Exception(f'trying to apply an update to a value that is not a map:\n value: {current}\n update: {update}') + if not isinstance(update, dict): + raise Exception(f'trying to apply an update that is not a map:\n value: {current}\n update: {update}') + + value_type = core.find_parameter( + schema, + 'value') + + result = current.copy() + + for key, update_value in update.items(): + if key == '_add': + for addition_key, addition in update_value.items(): + generated_schema, generated_state = core.generate( + value_type, + addition) + + result[addition_key] = generated_state + + elif key == '_remove': + for remove_key in update_value: + if remove_key in result: + del result[remove_key] + + elif key not in current: + # This supports adding without the '_add' key, if the key is not in the state + generated_schema, generated_state = core.generate( + value_type, + update_value) + + result[key] = generated_state + + # raise Exception(f'trying to update a key that does not exist:\n value: {current}\n update: {update}') + else: + result[key] = core.apply( + value_type, + result[key], + update_value) + + return result + +def apply_maybe(schema, current, update, core): + if current is None or update is None: + return update + else: + value_type = core.find_parameter( + schema, + 'value') + + return core.apply( + value_type, + current, + update) + +def apply_path(schema, current, update, core): + # paths replace previous paths + return update + +def apply_edge(schema, current, update, core): + result = current.copy() + result['inputs'] = core.apply( + 'wires', + current.get('inputs'), + update.get('inputs')) + + result['outputs'] = core.apply( + 'wires', + current.get('outputs'), + update.get('outputs')) + + return result + +# TODO: deal with all the different unit core +def apply_units(schema, current, update, core): + return current + update + + +# Sort functions +# -------------- + def sort_quote(core, schema, state): return schema, state @@ -255,6 +484,9 @@ def sort_any(core, schema, state): return merged_schema, merged_state +# Fold functions +# -------------- + def fold_any(schema, state, method, values, core): if isinstance(state, dict): result = {} @@ -327,34 +559,34 @@ def fold_union(schema, state, method, values, core): return result -def type_parameters_for(schema): - parameters = [] - for key in schema['_type_parameters']: - subschema = schema.get(f'_{key}', 'any') - parameters.append(subschema) - - return parameters +def resolve_any(schema, update, core): + schema = schema or {} + outcome = schema.copy() + for key, subschema in update.items(): + if key == '_type' and key in outcome: + if outcome[key] != subschema: + if core.inherits_from(outcome[key], subschema): + continue + elif core.inherits_from(subschema, outcome[key]): + outcome[key] = subschema + else: + raise Exception(f'cannot resolve types when updating\ncurrent type: {schema}\nupdate type: {update}') -def dataclass_union(schema, path, core): - parameters = type_parameters_for(schema) - subtypes = [] - for parameter in parameters: - dataclass = core.dataclass( - parameter, - path) - - if isinstance(dataclass, str): - subtypes.append(dataclass) - elif isinstance(dataclass, type): - subtypes.append(dataclass.__name__) + elif not key in outcome or type_parameter_key(update, key): + if subschema: + outcome[key] = subschema else: - subtypes.append(str(dataclass)) + outcome[key] = core.resolve_schemas( + outcome.get(key), + subschema) - parameter_block = ', '.join(subtypes) - return eval(f'Union[{parameter_block}]') + return outcome +# Divide functions +# ---------------- + def divide_any(schema, state, values, core): divisions = values.get('divisions', 2) @@ -380,313 +612,255 @@ def divide_any(schema, state, values, core): copy.deepcopy(state) for _ in range(divisions)] +def divide_tuple(schema, state, values, core): + divisions = values.get('divisions', 2) -def resolve_any(schema, update, core): - schema = schema or {} - outcome = schema.copy() - - for key, subschema in update.items(): - if key == '_type' and key in outcome: - if outcome[key] != subschema: - if core.inherits_from(outcome[key], subschema): - continue - elif core.inherits_from(subschema, outcome[key]): - outcome[key] = subschema - else: - raise Exception(f'cannot resolve types when updating\ncurrent type: {schema}\nupdate type: {update}') - - elif not key in outcome or type_parameter_key(update, key): - if subschema: - outcome[key] = subschema - else: - outcome[key] = core.resolve_schemas( - outcome.get(key), - subschema) + return [ + tuple([item[index] for item in state]) + for index in range(divisions)] - return outcome +def divide_float(schema, state, values, core): + divisions = values.get('divisions', 2) + portion = float(state) / divisions + return [ + portion + for _ in range(divisions)] -def dataclass_any(schema, path, core): - parts = path - if not parts: - parts = ['top'] - dataclass_name = '_'.join(parts) +# support function core for registrys? +def divide_integer(schema, value, values, core): + half = value // 2 + other_half = half + if value % 2 == 1: + other_half += 1 + return [half, other_half] - if isinstance(schema, dict): - type_name = schema.get('_type', 'any') - branches = {} - for key, subschema in schema.items(): - if not key.startswith('_'): - branch = core.dataclass( - subschema, - path + [key]) +def divide_longest(schema, dimensions, values, core): + # any way to declare the required keys for this function in the registry? + # find a way to ask a function what type its domain and codomain are - def default(subschema=subschema): - return core.default(subschema) + width = dimensions['width'] + height = dimensions['height'] - branches[key] = ( - key, - branch, - field(default_factory=default)) + if width > height: + a, b = divide_integer(width) + return [{'width': a, 'height': height}, {'width': b, 'height': height}] + else: + x, y = divide_integer(height) + return [{'width': width, 'height': x}, {'width': width, 'height': y}] - dataclass = make_dataclass( - dataclass_name, - branches.values(), - namespace={ - '__module__': 'bigraph_schema.data'}) +def divide_reaction(schema, state, reaction, core): + mother = reaction['mother'] + daughters = reaction['daughters'] - setattr( - bigraph_schema.data, - dataclass_name, - dataclass) + mother_schema, mother_state = core.slice( + schema, + state, + mother) - else: - schema = core.access(schema) - dataclass = core.dataclass(schema, path) + division = core.fold( + mother_schema, + mother_state, + 'divide', { + 'divisions': len(daughters), + 'daughter_configs': [daughter[1] for daughter in daughters]}) - return dataclass + after = { + daughter[0]: daughter_state + for daughter, daughter_state in zip(daughters, division)} + replace = { + 'before': { + mother: {}}, + 'after': after} -def dataclass_tuple(schema, path, core): - parameters = type_parameters_for(schema) - subtypes = [] + return replace_reaction( + schema, + state, + replace, + core) - for index, key in enumerate(schema['type_parameters']): - subschema = schema.get(key, 'any') - subtype = core.dataclass( - subschema, - path + [index]) - subtypes.append(subtype) +def divide_list(schema, state, values, core): + element_type = core.find_parameter( + schema, + 'element') - parameter_block = ', '.join(subtypes) - return eval(f'tuple[{parameter_block}]') + if core.check(element_type, state): + return core.fold( + element_type, + state, + 'divide', + values) + elif isinstance(state, list): + divisions = values.get('divisions', 2) + result = [[] for _ in range(divisions)] -def divide_tuple(schema, state, values, core): - divisions = values.get('divisions', 2) + for elements in state: + for index in range(divisions): + result[index].append( + elements[index]) - return [ - tuple([item[index] for item in state]) - for index in range(divisions)] + return result + else: + raise Exception( + f'trying to divide list but state does not resemble a list or an element.\n state: {pf(state)}\n schema: {pf(schema)}') -def apply_tree(schema, current, update, core): +def divide_tree(schema, state, values, core): leaf_type = core.find_parameter( schema, 'leaf') - if current is None: - current = core.default(leaf_type) - - if isinstance(current, dict) and isinstance(update, dict): - for key, branch in update.items(): - if key == '_add': - current.update(branch) - elif key == '_remove': - for removed_path in branch: - if isinstance(removed_path, str): - removed_path = [removed_path] - current = remove_path(current, removed_path) - elif isinstance(branch, dict): - subschema = schema - if key in schema: - subschema = schema[key] + if core.check(leaf_type, state): + return core.fold( + leaf_type, + state, + 'divide', + values) - current[key] = core.apply( - subschema, - current.get(key), - branch) + elif isinstance(state, dict): + divisions = values.get('divisions', 2) + division = [{} for _ in range(divisions)] - elif core.check(leaf_type, branch): - current[key] = core.apply( - leaf_type, - current.get(key), - branch) + for key, value in state.items(): + for index in range(divisions): + division[index][key] = value[index] - else: - raise Exception(f'state does not seem to be of leaf type:\n state: {state}\n leaf type: {leaf_type}') + return division - return current + else: + raise Exception( + f'trying to divide tree but state does not resemble a leaf or a tree.\n state: {pf(state)}\n schema: {pf(schema)}') - elif core.check(leaf_type, current): - return core.apply( - leaf_type, - current, - update) +def divide_map(schema, state, values, core): + if isinstance(state, dict): + divisions = values.get('divisions', 2) + division = [{} for _ in range(divisions)] + for key, value in state.items(): + for index in range(divisions): + division[index][key] = value[index] + return division else: - raise Exception(f'trying to apply an update to a tree but the values are not trees or leaves of that tree\ncurrent:\n {pf(current)}\nupdate:\n {pf(update)}\nschema:\n {pf(schema)}') + raise Exception( + f'trying to divide a map but state is not a dict.\n state: {pf(state)}\n schema: {pf(schema)}') +# Default functions +# ----------------- -def apply_any(schema, current, update, core): - if isinstance(current, dict): - return apply_tree( - current, - update, - 'tree[any]', - core) - else: - return update +def default_any(schema, core): + default = {} + for key, subschema in schema.items(): + if not is_schema_key(key): + default[key] = core.default( + subschema) -def slice_any(schema, state, path, core): - if not isinstance(path, (list, tuple)): - if path is None: - path = () - else: - path = [path] + return default - if len(path) == 0: - return schema, state +def default_tuple(schema, core): + parts = [] + for parameter in schema['_type_parameters']: + subschema = schema[f'_{parameter}'] + part = core.default(subschema) + parts.append(part) - elif len(path) > 0: - head = path[0] - tail = path[1:] - step = None + return tuple(parts) - if isinstance(state, dict): - if head not in state: - state[head] = core.default( - schema.get(head)) - step = state[head] - - elif hasattr(state, head): - step = getattr(state, head) - - if head in schema: - return core.slice( - schema[head], - step, - tail) - else: - return slice_any( - {}, - step, - tail, - core) - - -def check_any(schema, state, core): - if isinstance(schema, dict): - for key, subschema in schema.items(): - if not key.startswith('_'): - if isinstance(state, dict): - if key in state: - check = core.check_state( - subschema, - state[key]) - - if not check: - return False - else: - return False - else: - return False - - return True - else: - return True - - -def serialize_any(schema, state, core): - if isinstance(state, dict): - tree = {} - - for key in non_schema_keys(schema): - encoded = core.serialize( - schema.get(key, schema), - state.get(key)) - tree[key] = encoded - - return tree - - else: - return str(state) - - -def deserialize_any(schema, state, core): - if isinstance(state, dict): - tree = {} - - for key, value in state.items(): - if is_schema_key(key): - decoded = value - else: - decoded = core.deserialize( - schema.get(key, 'any'), - value) - - tree[key] = decoded +def default_union(schema, core): + final_parameter = schema['_type_parameters'][-1] + subschema = schema[f'_{final_parameter}'] - for key in non_schema_keys(schema): - if key not in tree: - # if key not in state: - # decoded = core.default( - # schema[key]) - # else: - if key in state: - decoded = core.deserialize( - schema[key], - state[key]) + return core.default(subschema) - tree[key] = decoded +def default_tree(schema, core): + leaf_schema = core.find_parameter( + schema, + 'leaf') - return tree + default = {} - else: - return state + non_schema_keys = [ + key + for key in schema + if not is_schema_key(key)] + if non_schema_keys: + base_schema = { + key: subschema + for key, subschema in schema.items() + if is_schema_key(key)} -def is_empty(value): - if isinstance(value, np.ndarray): - return False - elif value is None or value == {}: - return True - else: - return False + for key in non_schema_keys: + subschema = core.merge_schemas( + base_schema, + schema[key]) + subdefault = core.default( + subschema) -def bind_any(schema, state, key, subschema, substate, core): - result_schema = core.resolve_schemas( - schema, - {key: subschema}) + if subdefault: + default[key] = subdefault - if state is None: - state = {} + return default - state[key] = substate +def default_array(schema, core): + data_schema = core.find_parameter( + schema, + 'data') - return result_schema, state + dtype = read_datatype( + data_schema) + shape = read_shape( + schema['_shape']) -def apply_tuple(schema, current, update, core): - parameters = core.parameters_for(schema) - result = [] + return np.zeros( + shape, + dtype=dtype) - for parameter, current_value, update_value in zip(parameters, current, update): - element = core.apply( - parameter, - current_value, - update_value) - result.append(element) +# Slice functions +# --------------- - return tuple(result) +def slice_any(schema, state, path, core): + if not isinstance(path, (list, tuple)): + if path is None: + path = () + else: + path = [path] + if len(path) == 0: + return schema, state -def check_tuple(schema, state, core): - if not isinstance(state, (tuple, list)): - return False + elif len(path) > 0: + head = path[0] + tail = path[1:] + step = None - parameters = core.parameters_for(schema) - for parameter, element in zip(parameters, state): - if not core.check(parameter, element): - return False + if isinstance(state, dict): + if head not in state: + state[head] = core.default( + schema.get(head)) + step = state[head] - return True + elif hasattr(state, head): + step = getattr(state, head) + if head in schema: + return core.slice( + schema[head], + step, + tail) + else: + return slice_any( + {}, + step, + tail, + core) def slice_tuple(schema, state, path, core): if len(path) > 0: @@ -707,3684 +881,3569 @@ def slice_tuple(schema, state, path, core): else: return schema, state +def slice_union(schema, state, path, core): + union_type = find_union_type( + core, + schema, + state) -def serialize_tuple(schema, value, core): - parameters = core.parameters_for(schema) - result = [] + return core.slice( + union_type, + state, + path) - for parameter, element in zip(parameters, value): - encoded = core.serialize( - parameter, - element) +def slice_list(schema, state, path, core): + element_type = core.find_parameter( + schema, + 'element') - result.append(encoded) + if len(path) > 0: + head = path[0] + tail = path[1:] - return tuple(result) + if not isinstance(head, int) or head >= len(state): + raise Exception(f'bad index for list: {path} for {state}') + step = state[head] + return core.slice(element_type, step, tail) + else: + return schema, state -def deserialize_tuple(schema, state, core): - parameters = core.parameters_for(schema) - result = [] +def slice_tree(schema, state, path, core): + leaf_type = core.find_parameter( + schema, + 'leaf') - if isinstance(state, str): - if (state[0] == '(' and state[-1] == ')') or (state[0] == '[' and state[-1] == ']'): - state = state[1:-1].split(',') - else: - return None + if len(path) > 0: + head = path[0] + tail = path[1:] - for parameter, code in zip(parameters, state): - element = core.deserialize( - parameter, - code) + if not head in state: + state[head] = {} - result.append(element) + step = state[head] + if core.check(leaf_type, step): + return core.slice(leaf_type, step, tail) + else: + return core.slice(schema, step, tail) + else: + return schema, state - return tuple(result) +def slice_map(schema, state, path, core): + value_type = core.find_parameter( + schema, + 'value') + if len(path) > 0: + head = path[0] + tail = path[1:] -def bind_tuple(schema, state, key, subschema, substate, core): - new_schema = schema.copy() - new_schema[f'_{key}'] = subschema - open = list(state) - open[key] = substate + if not head in state: + state[head] = core.default( + value_type) - return new_schema, tuple(open) + step = state[head] + return core.slice( + value_type, + step, + tail) + else: + return schema, state +def slice_maybe(schema, state, path, core): + if state is None: + return schema, None -def find_union_type(core, schema, state): - parameters = core.parameters_for(schema) + else: + value_type = core.find_parameter( + schema, + 'value') - for possible in parameters: - if core.check(possible, state): - return core.access(possible) + return core.slice( + value_type, + state, + path) - return None +def slice_array(schema, state, path, core): + if len(path) > 0: + head = path[0] + tail = path[1:] + step = state[head] + + if isinstance(step, np.ndarray): + sliceschema = schema.copy() + sliceschema['_shape'] = step.shape + return core.slice( + sliceschema, + step, + tail) + else: + data_type = core.find_parameter( + schema, + 'data') + return core.slice( + data_type, + step, + tail) -def apply_union(schema, current, update, core): - current_type = find_union_type( - core, - schema, - current) + else: + return schema, state - update_type = find_union_type( - core, +def slice_string(schema, state, path, core): + raise Exception(f'cannot slice into an string: {path}\n{state}\n{schema}') + + +# Fold functions +# -------------- + +def fold_list(schema, state, method, values, core): + element_type = core.find_parameter( schema, - update) + 'element') - if current_type is None: - raise Exception(f'trying to apply update to union value but cannot find type of value in the union\n value: {current}\n update: {update}\n union: {list(bindings.values())}') - elif update_type is None: - raise Exception(f'trying to apply update to union value but cannot find type of update in the union\n value: {current}\n update: {update}\n union: {list(bindings.values())}') + if core.check(element_type, state): + result = core.fold( + element_type, + state, + method, + values) - # TODO: throw an exception if current_type is incompatible with update_type + elif isinstance(state, list): + subresult = [ + fold_list( + schema, + element, + method, + values, + core) + for element in state] - return core.apply( - update_type, - current, - update) + result = visit_method( + schema, + subresult, + method, + values, + core) + else: + raise Exception(f'state does not seem to be a list or an eelement:\n state: {state}\n schema: {schema}') -def check_union(schema, state, core): - found = find_union_type( - core, + return result + + +def fold_tree(schema, state, method, values, core): + leaf_type = core.find_parameter( schema, - state) + 'leaf') - return found is not None and len(found) > 0 + if core.check(leaf_type, state): + result = core.fold( + leaf_type, + state, + method, + values) + elif isinstance(state, dict): + subresult = {} -def slice_union(schema, state, path, core): - union_type = find_union_type( - core, + for key, branch in state.items(): + if key.startswith('_'): + subresult[key] = branch + else: + subresult[key] = fold_tree( + schema[key] if key in schema else schema, + branch, + method, + values, + core) + + result = visit_method( + schema, + subresult, + method, + values, + core) + + else: + raise Exception(f'state does not seem to be a tree or a leaf:\n state: {state}\n schema: {schema}') + + return result + + +def fold_map(schema, state, method, values, core): + value_type = core.find_parameter( schema, - state) + 'value') - return core.slice( - union_type, - state, - path) + subresult = {} + for key, value in state.items(): + subresult[key] = core.fold( + value_type, + value, + method, + values) -def bind_union(schema, state, key, subschema, substate, core): - union_type = find_union_type( - core, + result = visit_method( schema, - state) + subresult, + method, + values, + core) - return core.bind( - union_type, - state, - key, - subschema, - substate) + return result -def serialize_union(schema, value, core): - union_type = find_union_type( - core, +def fold_maybe(schema, state, method, values, core): + value_type = core.find_parameter( schema, - value) + 'value') - return core.serialize( - union_type, - value) + if state is None: + result = core.fold( + 'any', + state, + method, + values) + + else: + result = core.fold( + value_type, + state, + method, + values) + return result -def deserialize_union(schema, encoded, core): - if encoded == NONE_SYMBOL: - return None +def fold_enum(schema, state, method, values, core): + if not isinstance(state, (tuple, list)): + return visit_method( + schema, + state, + method, + values, + core) else: parameters = core.parameters_for(schema) - - for parameter in parameters: - value = core.deserialize( + result = [] + for parameter, element in zip(parameters, state): + fold = core.fold( parameter, - encoded) + element, + method, + values) + result.append(fold) - if value is not None: - return value + result = tuple(result) + return visit_method( + schema, + result, + method, + values, + core) -def default_any(schema, core): - default = {} - for key, subschema in schema.items(): - if not is_schema_key(key): - default[key] = core.default( - subschema) +# Bind functions +# -------------- - return default +def bind_any(schema, state, key, subschema, substate, core): + result_schema = core.resolve_schemas( + schema, + {key: subschema}) + if state is None: + state = {} -def default_tuple(schema, core): - parts = [] - for parameter in schema['_type_parameters']: - subschema = schema[f'_{parameter}'] - part = core.default(subschema) - parts.append(part) + state[key] = substate - return tuple(parts) + return result_schema, state +def bind_tuple(schema, state, key, subschema, substate, core): + new_schema = schema.copy() + new_schema[f'_{key}'] = subschema + open = list(state) + open[key] = substate -def default_union(schema, core): - final_parameter = schema['_type_parameters'][-1] - subschema = schema[f'_{final_parameter}'] + return new_schema, tuple(open) - return core.default(subschema) +def bind_union(schema, state, key, subschema, substate, core): + union_type = find_union_type( + core, + schema, + state) + return core.bind( + union_type, + state, + key, + subschema, + substate) -def union_keys(schema, state): - keys = {} - for key in schema: - keys[key] = True - for key in state: - keys[key] = True +def bind_enum(schema, state, key, subschema, substate, core): + new_schema = schema.copy() + new_schema[f'_{key}'] = subschema + open = list(state) + open[key] = substate - return keys + return new_schema, tuple(open) - # return set(schema.keys()).union(state.keys()) +# Check functions +# --------------- -def generate_any(core, schema, state, top_schema=None, top_state=None, path=None): - schema = schema or {} - if is_empty(state): - state = core.default(schema) - top_schema = top_schema or schema - top_state = top_state or state - path = path or [] +def check_any(schema, state, core): + if isinstance(schema, dict): + for key, subschema in schema.items(): + if not key.startswith('_'): + if isinstance(state, dict): + if key in state: + check = core.check_state( + subschema, + state[key]) - generated_schema = {} - generated_state = {} + if not check: + return False + else: + return False + else: + return False - if isinstance(state, dict): - visited = set([]) + return True + else: + return True - all_keys = union_keys( - schema, - state) +def check_tuple(schema, state, core): + if not isinstance(state, (tuple, list)): + return False - non_schema_keys = [ - key - for key in all_keys - if not is_schema_key(key)] + parameters = core.parameters_for(schema) + for parameter, element in zip(parameters, state): + if not core.check(parameter, element): + return False - for key in all_keys: - if is_schema_key(key): - generated_schema[key] = state.get( - key, - schema.get(key)) + return True - else: - subschema, substate, top_schema, top_state = core.generate_recur( - schema.get(key), - state.get(key), - top_schema=top_schema, - top_state=top_state, - path=path+[key]) +def check_union(schema, state, core): + found = find_union_type( + core, + schema, + state) - generated_schema[key] = core.resolve_schemas( - schema.get(key, {}), - subschema) + return found is not None and len(found) > 0 - generated_state[key] = substate +def check_number(schema, state, core=None): + return isinstance(state, numbers.Number) - if path: - top_schema, top_state = core.set_slice( - top_schema, - top_state, - path, - generated_schema, - generated_state) - else: - top_state = core.merge_recur( - top_schema, - top_state, - generated_state) +def check_boolean(schema, state, core=None): + return isinstance(state, bool) - else: - generated_schema, generated_state = schema, state +def check_integer(schema, state, core=None): + return isinstance(state, int) and not isinstance(state, bool) - return generated_schema, generated_state, top_schema, top_state +def check_float(schema, state, core=None): + return isinstance(state, float) +def check_string(schema, state, core=None): + return isinstance(state, str) -def is_method_key(key, parameters): - return key.startswith('_') and key not in type_schema_keys and key not in [ - f'_{parameter}' for parameter in parameters] +def check_list(schema, state, core): + element_type = core.find_parameter( + schema, + 'element') + if isinstance(state, list): + for element in state: + check = core.check( + element_type, + element) -registry_types = { - 'any': { - '_type': 'any', - '_default': default_any, - '_slice': slice_any, - '_apply': apply_any, - '_check': check_any, - '_sort': sort_any, - '_generate': generate_any, - '_serialize': serialize_any, - '_deserialize': deserialize_any, - '_dataclass': dataclass_any, - '_resolve': resolve_any, - '_fold': fold_any, - '_bind': bind_any, - '_divide': divide_any}, + if not check: + return False - 'quote': { - '_type': 'quote', - '_generate': generate_quote, - '_sort': sort_quote}, + return True + else: + return False - 'tuple': { - '_type': 'tuple', - '_default': default_tuple, - '_apply': apply_tuple, - '_check': check_tuple, - '_slice': slice_tuple, - '_serialize': serialize_tuple, - '_deserialize': deserialize_tuple, - '_dataclass': dataclass_tuple, - '_fold': fold_tuple, - '_divide': divide_tuple, - '_bind': bind_tuple, - '_description': 'tuple of an ordered set of typed values'}, +def check_tree(schema, state, core): + leaf_type = core.find_parameter( + schema, + 'leaf') - 'union': { - '_type': 'union', - '_default': default_union, - '_apply': apply_union, - '_check': check_union, - '_slice': slice_union, - '_serialize': serialize_union, - '_deserialize': deserialize_union, - '_dataclass': dataclass_union, - '_fold': fold_union, - '_description': 'union of a set of possible types'}} + if isinstance(state, dict): + for key, value in state.items(): + check = core.check({ + '_type': 'tree', + '_leaf': leaf_type}, + value) + if not check: + return core.check( + leaf_type, + value) -class TypeSystem(Registry): - """Handles type schemas and their operation""" + return True + else: + return core.check(leaf_type, state) - def __init__(self): - super().__init__() +def check_map(schema, state, core=None): + value_type = core.find_parameter( + schema, + 'value') - self.inherits = {} + if not isinstance(state, dict): + return False - self.default_registry = Registry(function_keys=[ - 'schema', - 'core']) + for key, substate in state.items(): + if not core.check(value_type, substate): + return False - self.check_registry = Registry(function_keys=[ - 'state', - 'schema', - 'core']) + return True - self.apply_registry = Registry(function_keys=[ - 'current', - 'update', - 'schema', - 'core']) +def check_ports(state, core, key): + return key in state and core.check( + 'wires', + state[key]) - self.serialize_registry = Registry(function_keys=[ - 'value', - 'schema', - 'core']) +def check_edge(schema, state, core): + return isinstance(state, dict) and check_ports(state, core, 'inputs') and check_ports(state, core, 'outputs') - self.deserialize_registry = Registry(function_keys=[ - 'encoded', - 'schema', - 'core']) +def check_maybe(schema, state, core): + if state is None: + return True + else: + value_type = core.find_parameter( + schema, + 'value') - self.fold_registry = Registry(function_keys=[ - 'method', - 'state', - 'schema', - 'core']) + return core.check(value_type, state) - self.react_registry = Registry() - self.method_registry = Registry() +def check_array(schema, state, core): + shape_type = core.find_parameter( + schema, + 'shape') - # register all the base methods and types - self.apply_registry.register( - 'set', - set_apply) + return isinstance(state, np.ndarray) and state.shape == array_shape(core, shape_type) # and state.dtype == bindings['data'] # TODO align numpy data types so we can validate the types of the arrays - self.register_types(registry_types) - self.register_types(base_type_library) +def dataclass_array(schema, path, core): + return np.ndarray - register_units(self, units) - register_base_reactions(self) +# Serialize functions +# ------------------- - def register_types(core, type_library): - for type_key, type_data in type_library.items(): - if not core.exists(type_key): - core.register( - type_key, - type_data) +def serialize_any(schema, state, core): + if isinstance(state, dict): + tree = {} - return core - + for key in non_schema_keys(schema): + encoded = core.serialize( + schema.get(key, schema), + state.get(key)) + tree[key] = encoded - def lookup(self, type_key, attribute): - return self.access(type_key).get(attribute) + return tree + else: + return str(state) - def lookup_registry(self, underscore_key): - """ - access the registry for the given key - """ +def serialize_union(schema, value, core): + union_type = find_union_type( + core, + schema, + value) - if underscore_key == '_type': - return self - root = underscore_key.strip('_') - registry_key = f'{root}_registry' - if hasattr(self, registry_key): - return getattr(self, registry_key) + return core.serialize( + union_type, + value) +def serialize_tuple(schema, value, core): + parameters = core.parameters_for(schema) + result = [] - def find_registry(self, underscore_key): - """ - access the registry for the given key - and create if it doesn't exist - """ + for parameter, element in zip(parameters, value): + encoded = core.serialize( + parameter, + element) - registry = self.lookup_registry(underscore_key) - if registry is None: - registry = Registry() - setattr( - self, - f'{underscore_key[1:]}_registry', - registry) + result.append(encoded) - return registry + return tuple(result) +def serialize_string(schema, value, core=None): + return value - def register(self, key, schema, alternate_keys=tuple(), force=False): - """ - register the schema under the given key in the registry - """ +def serialize_boolean(schema, value: bool, core) -> str: + return str(value) - if isinstance(schema, str): - schema = self.find(schema) - schema = copy.deepcopy(schema) +def serialize_list(schema, value, core=None): + element_type = core.find_parameter( + schema, + 'element') - if '_type' not in schema: - schema['_type'] = key + return [ + core.serialize( + element_type, + element) + for element in value] - if isinstance(schema, dict): - inherits = schema.get('_inherit', []) # list of immediate inherits - if isinstance(inherits, str): - inherits = [inherits] - schema['_inherit'] = inherits +def serialize_tree(schema, value, core): + if isinstance(value, dict): + encoded = {} + for key, subvalue in value.items(): + encoded[key] = serialize_tree( + schema, + subvalue, + core) - self.inherits[key] = [] - for inherit in inherits: - inherit_type = self.access(inherit) - new_schema = copy.deepcopy(inherit_type) + else: + leaf_type = core.find_parameter( + schema, + 'leaf') - # schema = self.resolve( - # new_schema, - # schema) + if core.check(leaf_type, value): + encoded = core.serialize( + leaf_type, + value) + else: + raise Exception(f'trying to serialize a tree but unfamiliar with this form of tree: {value} - current schema:\n {pf(schema)}') - schema = self.merge_schemas( - new_schema, - schema) + return encoded - # schema = type_merge( - # new_schema, - # schema) +def serialize_units(schema, value, core): + return str(value) - self.inherits[key].append( - inherit_type) +def serialize_maybe(schema, value, core): + if value is None: + return NONE_SYMBOL + else: + value_type = core.find_parameter( + schema, + 'value') - parameters = schema.get('_type_parameters', []) - for subkey, subschema in schema.items(): - if subkey == '_default' or subkey in TYPE_FUNCTION_KEYS or is_method_key(subkey, parameters): - if callable(subschema): - registry = self.find_registry(subkey) - function_name, module_key = registry.register_function(subschema) + return core.serialize( + value_type, + value) - schema[subkey] = function_name - else: - schema[subkey] = subschema +def serialize_map(schema, value, core=None): + value_type = core.find_parameter( + schema, + 'value') - elif subkey not in type_schema_keys: - if schema['_type'] in SYMBOL_TYPES: - schema[subkey] = subschema - else: - lookup = self.find(subschema) - if lookup is None: - import ipdb; ipdb.set_trace() - raise Exception( - f'trying to register a new type ({key}), ' - f'but it depends on a type ({subkey}) which is not in the registry') - else: - schema[subkey] = lookup - else: - raise Exception( - f'all type definitions must be dicts ' - f'with the following keys: {type_schema_keys}\nnot: {schema}') + return { + key: core.serialize( + value_type, + subvalue) if not is_schema_key(key) else subvalue + for key, subvalue in value.items()} - super().register( - key, - schema, - alternate_keys, - force) +def serialize_edge(schema, value, core): + return value - def resolve_parameters(self, type_parameters, schema): - """ - find the types associated with any type parameters in the schema - """ +def serialize_enum(schema, value, core): + return value - return { - type_parameter: self.access( - schema.get(f'_{type_parameter}')) - for type_parameter in type_parameters} +# Deserialize functions +# --------------------- - def register_reaction(self, reaction_key, reaction): - self.react_registry.register( - reaction_key, - reaction) +def deserialize_any(schema, state, core): + if isinstance(state, dict): + tree = {} + for key, value in state.items(): + if is_schema_key(key): + decoded = value + else: + decoded = core.deserialize( + schema.get(key, 'any'), + value) - def types(self): - return { - type_key: type_data - for type_key, type_data in self.registry.items()} + tree[key] = decoded + for key in non_schema_keys(schema): + if key not in tree: + # if key not in state: + # decoded = core.default( + # schema[key]) + # else: + if key in state: + decoded = core.deserialize( + schema[key], + state[key]) - def merge_schemas(self, current, update): - if current == update: - return update - if not isinstance(current, dict): - return update - if not isinstance(update, dict): - return update - - merged = {} + tree[key] = decoded - for key in union_keys(current, update): - if key in current: - if key in update: - subcurrent = current[key] - subupdate = update[key] - if subcurrent == current or subupdate == update: - continue - - merged[key] = self.merge_schemas( - subcurrent, - subupdate) - else: - merged[key] = current[key] - else: - merged[key] = update[key] - - return merged + return tree + else: + return state - def sort(self, schema, state): - schema = self.access(schema) +def deserialize_tuple(schema, state, core): + parameters = core.parameters_for(schema) + result = [] - sort_function = self.choose_method( - schema, - state, - 'sort') + if isinstance(state, str): + if (state[0] == '(' and state[-1] == ')') or (state[0] == '[' and state[-1] == ']'): + state = state[1:-1].split(',') + else: + return None - return sort_function( - self, - schema, - state) + for parameter, code in zip(parameters, state): + element = core.deserialize( + parameter, + code) + result.append(element) - def exists(self, type_key): - return type_key in self.registry + return tuple(result) +def deserialize_union(schema, encoded, core): + if encoded == NONE_SYMBOL: + return None + else: + parameters = core.parameters_for(schema) - def find(self, schema, strict=False): - """ - expand the schema to its full type information from the type registry - """ + for parameter in parameters: + value = core.deserialize( + parameter, + encoded) - found = None + if value is not None: + return value - if schema is None: - return self.access('any', strict=strict) +def deserialize_string(schema, encoded, core=None): + if isinstance(encoded, str): + return encoded - elif isinstance(schema, dict): - if '_description' in schema: - return schema +def deserialize_integer(schema, encoded, core=None): + value = None + try: + value = int(encoded) + except: + pass - elif '_union' in schema: - union_schema = { - '_type': 'union', - '_type_parameters': []} + return value - for index, element in enumerate(schema['_union']): - union_schema['_type_parameters'].append(str(index)) - union_schema[f'_{index}'] = element +def deserialize_float(schema, encoded, core=None): + value = None + try: + value = float(encoded) + except: + pass - return self.access( - union_schema, - strict=strict) + return value - elif '_type' in schema: - registry_type = self.retrieve( - schema['_type']) +def deserialize_list(schema, encoded, core=None): + if isinstance(encoded, list): + element_type = core.find_parameter( + schema, + 'element') - # found = self.resolve( - # registry_type, - # schema) + return [ + core.deserialize( + element_type, + element) + for element in encoded] - found = self.merge_schemas( - registry_type, - schema) +def deserialize_maybe(schema, encoded, core): + if encoded == NONE_SYMBOL or encoded is None: + return None + else: + value_type = core.find_parameter( + schema, + 'value') - # found = schema.copy() + return core.deserialize(value_type, encoded) - # for key, value in registry_type.items(): - # if key == '_type' or key not in found: - # found[key] = value +def deserialize_boolean(schema, encoded, core) -> bool: + if encoded == 'true': + return True + elif encoded == 'false': + return False + elif encoded == True or encoded == False: + return encoded +def deserialize_tree(schema, encoded, core): + if isinstance(encoded, dict): + tree = {} + for key, value in encoded.items(): + if key.startswith('_'): + tree[key] = value else: - found = { - key: self.access( - branch, - strict=strict) if key != '_default' else branch - for key, branch in schema.items()} + tree[key] = deserialize_tree(schema, value, core) - elif isinstance(schema, int): - return schema + return tree - elif isinstance(schema, tuple): - tuple_schema = { - '_type': 'tuple', - '_type_parameters': []} + else: + leaf_type = core.find_parameter( + schema, + 'leaf') - for index, element in enumerate(schema): - tuple_schema['_type_parameters'].append(str(index)) - tuple_schema[f'_{index}'] = element + if leaf_type: + return core.deserialize( + leaf_type, + encoded) + else: + return encoded - return self.access( - tuple_schema, - strict=strict) +def deserialize_units(schema, encoded, core): + if isinstance(encoded, Quantity): + return encoded + else: + return units(encoded) - elif isinstance(schema, list): - if isinstance(schema[0], int): - return schema +def deserialize_map(schema, encoded, core=None): + if isinstance(encoded, dict): + value_type = core.find_parameter( + schema, + 'value') - bindings = [] - if len(schema) > 1: - schema, bindings = schema - else: - schema = schema[0] - found = self.access( - schema, - strict=strict) + return { + key: core.deserialize( + value_type, + subvalue) if not is_schema_key(key) else subvalue + for key, subvalue in encoded.items()} - if len(bindings) > 0: - found = found.copy() +def deserialize_enum(schema, state, core): + return value - if '_type_parameters' not in found: - found['_type_parameters'] = [] - for index, binding in enumerate(bindings): - found['_type_parameters'].append(str(index)) - found[f'_{index}'] = binding - else: - for parameter, binding in zip(found['_type_parameters'], bindings): - binding_type = self.access( - binding, - strict=strict) or binding +def deserialize_array(schema, encoded, core): + if isinstance(encoded, np.ndarray): + return encoded - found[f'_{parameter}'] = binding_type + elif isinstance(encoded, dict): + if 'value' in encoded: + return encoded['value'] + else: + found = core.retrieve( + encoded.get( + 'data', + schema['_data'])) - elif isinstance(schema, str): - found = self.registry.get(schema) + dtype = read_datatype( + found) - if found is None and schema not in ('', '{}'): - try: - parse = parse_expression(schema) - if parse != schema: - found = self.access( - parse, - strict=strict) - elif not strict: - found = {'_type': schema} + shape = read_shape( + schema['_shape']) - except Exception: - print(f'type did not parse: {schema}') - traceback.print_exc() - - return found + if 'list' in encoded: + return np.array( + encoded['list'], + dtype=dtype).reshape( + shape) + else: + return np.zeros( + tuple(shape), + dtype=dtype) +# Dataclass functions +# ------------------- - def access(self, schema, strict=False): - if isinstance(schema, str): - return self.access_str(schema, strict=strict) +def dataclass_union(schema, path, core): + parameters = type_parameters_for(schema) + subtypes = [] + for parameter in parameters: + dataclass = core.dataclass( + parameter, + path) + + if isinstance(dataclass, str): + subtypes.append(dataclass) + elif isinstance(dataclass, type): + subtypes.append(dataclass.__name__) else: - return self.find(schema, strict=strict) + subtypes.append(str(dataclass)) + parameter_block = ', '.join(subtypes) + return eval(f'Union[{parameter_block}]') - @functools.lru_cache(maxsize=None) - def access_str(self, schema, strict=False): - return self.find( - schema, - strict) - - - def retrieve(self, schema): - """ - like access(schema) but raises an exception if nothing is found - """ - found = self.find( - schema, - strict=True) +def dataclass_any(schema, path, core): + parts = path + if not parts: + parts = ['top'] + dataclass_name = '_'.join(parts) - if found is None: - raise Exception(f'schema not found for type: {schema}') - return found + if isinstance(schema, dict): + type_name = schema.get('_type', 'any') + branches = {} + for key, subschema in schema.items(): + if not key.startswith('_'): + branch = core.dataclass( + subschema, + path + [key]) - def find_parameter(self, schema, parameter): - schema_key = f'_{parameter}' - if schema_key not in schema: - schema = self.access(schema) - if schema_key not in schema: - return 'any' - # raise Exception(f'parameter {parameter} not found in schema:\n {schema}') + def default(subschema=subschema): + return core.default(subschema) - parameter_type = self.access( - schema[schema_key]) + branches[key] = ( + key, + branch, + field(default_factory=default)) - return parameter_type + dataclass = make_dataclass( + dataclass_name, + branches.values(), + namespace={ + '__module__': 'bigraph_schema.data'}) + setattr( + bigraph_schema.data, + dataclass_name, + dataclass) - def parameters_for(self, initial_schema): - ''' - find any type parameters for this schema if they are present - ''' + else: + schema = core.access(schema) + dataclass = core.dataclass(schema, path) - if '_type_parameters' in initial_schema: - schema = initial_schema - else: - schema = self.access(initial_schema) + return dataclass - if '_type_parameters' not in schema: - return [] - else: - result = [] - for parameter in schema['_type_parameters']: - parameter_key = f'_{parameter}' - if parameter_key not in schema: - raise Exception( - f'looking for type parameter {parameter_key} but it is not in the schema:\n {pf(schema)}') - else: - parameter_type = schema[parameter_key] - result.append( - parameter_type) - return result +def dataclass_tuple(schema, path, core): + parameters = type_parameters_for(schema) + subtypes = [] + for index, key in enumerate(schema['type_parameters']): + subschema = schema.get(key, 'any') + subtype = core.dataclass( + subschema, + path + [index]) - def validate_schema(self, schema, enforce_connections=False): - # TODO: - # check() always returns true or false, - # validate() returns information about what doesn't match + subtypes.append(subtype) - # add ports and wires - # validate ports are wired to a matching type, - # either the same type or a subtype (more specific type) - # declared ports are equivalent to the presence of a process - # where the ports need to be looked up + parameter_block = ', '.join(subtypes) + return eval(f'tuple[{parameter_block}]') - report = {} +def dataclass_float(schema, path, core): + return float - if schema is None: - report = 'schema cannot be None' +def dataclass_integer(schema, path, core): + return int - elif isinstance(schema, str): - typ = self.access( - schema, - strict=True) +def dataclass_list(schema, path, core): + element_type = core.find_parameter( + schema, + 'element') - if typ is None: - report = f'type: {schema} is not in the registry' + dataclass = core.dataclass( + element_type, + path + ['element']) - elif isinstance(schema, dict): - report = {} + return list[dataclass] - schema_keys = set([]) - branches = set([]) +def dataclass_tree(schema, path, core): + leaf_type = core.find_parameter( + schema, + 'leaf') - for key, value in schema.items(): - if key == '_type': - typ = self.access( - value, - strict=True) - if typ is None: - report[key] = f'type: {value} is not in the registry' + leaf_dataclass = core.dataclass( + leaf_type, + path + ['leaf']) - elif key in type_schema_keys: - schema_keys.add(key) - registry = self.lookup_registry(key) - if registry is None or key == '_default': - # deserialize and serialize back and check it is equal - pass - elif isinstance(value, str): - element = registry.access( - value, - strict=True) + dataclass_name = '_'.join(path) + # block = f"{dataclass_name} = NewType('{dataclass_name}', Union[{leaf_dataclass}, Mapping[str, '{dataclass_name}']])" + block = f"NewType('{dataclass_name}', Union[{leaf_dataclass}, Mapping[str, '{dataclass_name}']])" - if element is None: - report[key] = f'no entry in the {key} registry for: {value}' - elif not inspect.isfunction(value): - report[key] = f'unknown value for key {key}: {value}' - else: - branches.add(key) - branch_report = self.validate_schema(value) - if len(branch_report) > 0: - report[key] = branch_report + dataclass = eval(block) + setattr(data, dataclass_name, dataclass) - return report + return dataclass +def dataclass_map(schema, path, core): + value_type = core.find_parameter( + schema, + 'value') - # TODO: if its an edge, ensure ports match wires - # TODO: make this work again, return information about what is wrong - # with the schema - def validate_state(self, original_schema, state): - schema = self.access(original_schema) - validation = {} + dataclass = core.dataclass( + value_type, + path + ['value']) - if '_serialize' in schema: - if '_deserialize' not in schema: - validation = { - '_deserialize': f'serialize found in type without deserialize: {schema}' - } - else: - serialize = self.serialize_registry.access( - schema['_serialize']) - deserialize = self.deserialize_registry.access( - schema['_deserialize']) - serial = serialize(state) - pass_through = deserialize(serial) + return Mapping[str, dataclass] - if state != pass_through: - validation = f'state and pass_through are not the same: {serial}' - else: - for key, subschema in schema.items(): - if key not in type_schema_keys: - if key not in state: - validation[key] = f'key present in schema but not in state: {key}\nschema: {schema}\nstate: {state}\n' - else: - subvalidation = self.validate_state( - subschema, - state[key]) - if not (subvalidation is None or len(subvalidation) == 0): - validation[key] = subvalidation +def dataclass_maybe(schema, path, core): + value_type = core.find_parameter( + schema, + 'value') - return validation + dataclass = core.dataclass( + value_type, + path + ['value']) + return Optional[dataclass] - def representation(self, schema, level=None): - ''' - produce a string representation of the schema - * intended to be the inverse of parse_expression() - ''' +def dataclass_edge(schema, path, core): + inputs = schema.get('_inputs', {}) + inputs_dataclass = core.dataclass( + inputs, + path + ['inputs']) + outputs = schema.get('_outputs', {}) + outputs_dataclass = core.dataclass( + outputs, + path + ['outputs']) - if isinstance(schema, str): - return schema + return Callable[[inputs_dataclass], outputs_dataclass] - elif isinstance(schema, tuple): - inner = [ - self.representation(element) - for element in schema] +def dataclass_boolean(schema, path, core): + return bool - pipes = '|'.join(inner) - return f'({pipes})' - - elif isinstance(schema, dict): - if '_type' in schema: - type = schema['_type'] +def dataclass_string(schema, path, core): + return str - inner = [] - block = '' - if '_type_parameters' in schema: - for parameter_key in schema['_type_parameters']: - schema_key = f'_{parameter_key}' - if schema_key in schema: - parameter = self.representation( - schema[schema_key]) - inner.append(parameter) - else: - inner.append('()') - commas = ','.join(inner) - block = f'[{commas}]' +# Generate functions +# ------------------ - if type == 'tuple': - pipes = '|'.join(inner) - return f'({pipes})' - else: - return f"{type}{block}" - - else: - inner = {} - for key in non_schema_keys(schema): - subschema = self.representation( - schema[key]) +def generate_map(core, schema, state, top_schema=None, top_state=None, path=None): + schema = schema or {} + state = state or core.default(schema) + top_schema = top_schema or schema + top_state = top_state or state + path = path or [] - inner[key] = subschema + value_type = core.find_parameter( + schema, + 'value') - colons = [ - f'{key}:{value}' - for key, value in inner.items()] + # generated_schema = {} + # generated_state = {} - pipes = '|'.join(colons) - return f'({pipes})' - else: - return str(schema) + # TODO: can we assume this was already sorted at the top level? + generated_schema, generated_state = core.sort( + schema, + state) - def default(self, schema): - ''' - produce the default value for the provided schema - ''' + all_keys = union_keys(schema, state) # set(schema.keys()).union(state.keys()) - default = None - found = self.retrieve(schema) + for key in all_keys: + if is_schema_key(key): + generated_schema[key] = state.get( + key, + schema.get(key)) - if '_default' in found: - default_value = found['_default'] - if isinstance(default_value, str): - default_method = self.default_registry.access(default_value) - if default_method and callable(default_method): - default = default_method(found, self) - else: - default = self.deserialize( - found, - default_value) + else: + subschema = schema.get(key, value_type) + substate = state.get(key) - elif not '_deserialize' in found: - raise Exception( - f'asking for default but no deserialize in {found}') + subschema = core.merge_schemas( + value_type, + subschema) - else: - default = self.deserialize(found, found['_default']) + subschema, generated_state[key], top_schema, top_state = core.generate_recur( + subschema, + substate, + top_schema=top_schema, + top_state=top_state, + path=path + [key]) - else: - default = {} - for key, subschema in found.items(): - if not is_schema_key(key): - default[key] = self.default(subschema) + return generated_schema, generated_state, top_schema, top_state - return default +def generate_tree(core, schema, state, top_schema=None, top_state=None, path=None): + schema = schema or {} + state = state or core.default(schema) + top_schema = top_schema or schema + top_state = top_state or state + path = path or [] - def choose_method(self, schema, state, method_name): - ''' - find in the provided state, or schema if not there, - a method for the given method_name - ''' + leaf_type = core.find_parameter( + schema, + 'leaf') - method_key = f'_{method_name}' - found = None + leaf_is_any = leaf_type == 'any' or (isinstance(leaf_type, dict) and leaf_type.get('_type') == 'any') - if isinstance(state, dict) and method_key in state: - found = state[method_key] + if not leaf_is_any and core.check(leaf_type, state): + generate_schema, generate_state, top_schema, top_state = core.generate_recur( + leaf_type, + state, + top_schema=top_schema, + top_state=top_state, + path=path) - elif isinstance(state, dict) and '_type' in state: - method_type = self.find( - state['_type']) + elif isinstance(state, dict): + generate_schema = {} + generate_state = {} - if method_type: - found = method_type.get(method_key) + all_keys = union_keys(schema, state) # set(schema.keys()).union(state.keys()) + non_schema_keys = [ + key + for key in all_keys + if not is_schema_key(key)] - if not found and isinstance(schema, dict) and method_key in schema: - found = schema[method_key] + if non_schema_keys: + base_schema = { + key: subschema + for key, subschema in schema.items() + if is_schema_key(key)} + else: + base_schema = schema - if found is None: - any_type = self.access('any') - found = any_type[method_key] + for key in all_keys: + if not is_schema_key(key): + subschema = schema.get(key) + substate = state.get(key) - registry = self.lookup_registry(method_key) - method_function = registry.access( - found) + if not substate or core.check(leaf_type, substate): + base_schema = leaf_type - return method_function + subschema = core.merge_schemas( + base_schema, + subschema) + subschema, generate_state[key], top_schema, top_state = core.generate_recur( + subschema, + substate, + top_schema=top_schema, + top_state=top_state, + path=path + [key]) - def slice(self, schema, state, path): - ''' - find the subschema and substate at a node in the place graph - given by the provided path - ''' + elif key in state: + generate_schema[key] = state[key] + elif key in schema: + generate_schema[key] = schema[key] + else: + raise Exception(' the impossible has occurred now is the time for celebration') + else: + generate_schema = schema + generate_state = state - if not isinstance(path, (list, tuple)): - path = [path] + return generate_schema, generate_state, top_schema, top_state - schema = self.access(schema) - if '..' in path: - path = resolve_path(path) - slice_function = self.choose_method( - schema, - state, - 'slice') +def generate_ports(core, schema, wires, top_schema=None, top_state=None, path=None): + schema = schema or {} + wires = wires or {} + top_schema = top_schema or schema + top_state = top_state or {} + path = path or [] - return slice_function( - schema, - state, - path, - self) + if isinstance(schema, str): + schema = {'_type': schema} + for port_key, subwires in wires.items(): + if port_key in schema: + port_schema = schema[port_key] + else: + port_schema, subwires = core.slice( + schema, + wires, + port_key) - def match_node(self, schema, state, pattern): - if isinstance(pattern, dict): - if not isinstance(state, dict): - return False + if isinstance(subwires, dict): + top_schema, top_state = generate_ports( + core, + port_schema, + subwires, + top_schema=top_schema, + top_state=top_state, + path=path) - if '_type' in pattern and not self.is_compatible(schema, pattern): - return False + else: + if isinstance(subwires, str): + subwires = [subwires] - for key, subpattern in pattern.items(): - if key.startswith('_'): - continue + default_state = core.default( + port_schema) - if key in schema: - subschema = schema[key] - else: - subschema = schema + top_schema, top_state = core.set_slice( + top_schema, + top_state, + path[:-1] + subwires, + port_schema, + default_state, + defer=True) - if key in state: - matches = self.match_node( - subschema, - state[key], - pattern[key]) - if not matches: - return False - else: - return False + return top_schema, top_state - return True - else: - return pattern == state +def generate_edge(core, schema, state, top_schema=None, top_state=None, path=None): + schema = schema or {} + state = state or {} + top_schema = top_schema or schema + top_state = top_state or state + path = path or [] + generated_schema, generated_state, top_schema, top_state = generate_any( + core, + schema, + state, + top_schema=top_schema, + top_state=top_state, + path=path) - def match_recur(self, schema, state, pattern, mode='first', path=()): - matches = [] + deserialized_state = core.deserialize( + generated_schema, + generated_state) - match = self.match_node( - schema, - state, - pattern) + merged_schema, merged_state = core.sort( + generated_schema, + deserialized_state) - if match: - if mode == 'first' or mode == 'immediate': - return [path] - else: - matches.append(path) - elif mode == 'immediate': - return [] + top_schema, top_state = core.set_slice( + top_schema, + top_state, + path, + merged_schema, + merged_state) - if isinstance(state, dict): - for key, substate in state.items(): - if key.startswith('_'): - continue + for port_key in ['inputs', 'outputs']: + port_schema = merged_schema.get( + f'_{port_key}', {}) + ports = merged_state.get( + port_key, {}) - if key in schema: - subschema = schema[key] - else: - subschema = schema + top_schema, top_state = generate_ports( + core, + port_schema, + ports, + top_schema=top_schema, + top_state=top_state, + path=path) - submatches = self.match_recur( - subschema, - state[key], - pattern, - mode=mode, - path=path + (key,)) + return merged_schema, merged_state, top_schema, top_state - if mode == 'first' and len(submatches) > 0: - return submatches[0:1] - else: - matches.extend(submatches) - return matches - def match(self, original_schema, state, pattern, mode='first', path=()): - """ - find the path or paths to any instances of a - given pattern in the tree. +def is_empty(value): + if isinstance(value, np.ndarray): + return False + elif value is None or value == {}: + return True + else: + return False - "mode" can be a few things: - * immediate: only match top level - * first: only return the first match - * random: return a random match of all that matched - * all (or any other value): return every match in the tree - """ - schema = self.access(original_schema) - matches = self.match_recur( - schema, - state, - pattern, - mode=mode, - path=path) - if mode == 'random': - matches_count = len(matches) - if matches_count > 0: - choice = random.randint( - 0, - matches_count-1) - matches = [matches[choice]] +def find_union_type(core, schema, state): + parameters = core.parameters_for(schema) - return matches + for possible in parameters: + if core.check(possible, state): + return core.access(possible) + return None - def react(self, schema, state, reaction, mode='random'): - # TODO: explain all this - # TODO: after the reaction, fill in the state with missing values - # from the schema - # TODO: add schema to redex and reactum +def union_keys(schema, state): + keys = {} + for key in schema: + keys[key] = True + for key in state: + keys[key] = True - if 'redex' in reaction or 'reactum' in reaction or 'calls' in reaction: - redex = reaction.get('redex', {}) - reactum = reaction.get('reactum', {}) - calls = reaction.get('calls', {}) - else: - # single key with reaction name - reaction_key = list(reaction.keys())[0] - make_reaction = self.react_registry.access( - reaction_key) - react = make_reaction( - schema, - state, - reaction.get(reaction_key, {}), - self) + return keys - redex = react.get('redex', {}) - reactum = react.get('reactum', {}) - calls = react.get('calls', {}) + # return set(schema.keys()).union(state.keys()) - paths = self.match( - schema, - state, - redex, - mode=mode) - - # for path in paths: - # path_schema, path_state = self.slice( - # schema, - # state, - # path) - def merge_state(before): - remaining = remove_omitted( - redex, - reactum, - before) +def generate_any(core, schema, state, top_schema=None, top_state=None, path=None): + schema = schema or {} + if is_empty(state): + state = core.default(schema) + top_schema = top_schema or schema + top_state = top_state or state + path = path or [] - merged = deep_merge( - remaining, - reactum) + generated_schema = {} + generated_state = {} - return merged + if isinstance(state, dict): + visited = set([]) - for path in paths: - state = transform_path( - state, - path, - merge_state) + all_keys = union_keys( + schema, + state) - return state + non_schema_keys = [ + key + for key in all_keys + if not is_schema_key(key)] + for key in all_keys: + if is_schema_key(key): + generated_schema[key] = state.get( + key, + schema.get(key)) - # TODO: maybe all fields are optional? - def dataclass(self, schema, path=None): - path = path or [] + else: + subschema, substate, top_schema, top_state = core.generate_recur( + schema.get(key), + state.get(key), + top_schema=top_schema, + top_state=top_state, + path=path+[key]) - dataclass_function = self.choose_method( - schema, - {}, - 'dataclass') + generated_schema[key] = core.resolve_schemas( + schema.get(key, {}), + subschema) - return dataclass_function( - schema, - path, - self) - + generated_state[key] = substate - def resolve(self, schema, update): - if update is None: - return schema + if path: + top_schema, top_state = core.set_slice( + top_schema, + top_state, + path, + generated_schema, + generated_state) else: - resolve_function = self.choose_method( - schema, - update, - 'resolve') + top_state = core.merge_recur( + top_schema, + top_state, + generated_state) - return resolve_function( - schema, - update, - self) + else: + generated_schema, generated_state = schema, state + return generated_schema, generated_state, top_schema, top_state - def resolve_schemas(self, initial_current, initial_update): - current = self.access(initial_current) - update = self.access(initial_update) - if self.equivalent(current, update): - outcome = current +def is_method_key(key, parameters): + return key.startswith('_') and key not in type_schema_keys and key not in [ + f'_{parameter}' for parameter in parameters] - elif self.inherits_from(current, update): - outcome = current - elif self.inherits_from(update, current): - outcome = update +registry_types = { + 'any': { + '_type': 'any', + '_default': default_any, + '_slice': slice_any, + '_apply': apply_any, + '_check': check_any, + '_sort': sort_any, + '_generate': generate_any, + '_serialize': serialize_any, + '_deserialize': deserialize_any, + '_dataclass': dataclass_any, + '_resolve': resolve_any, + '_fold': fold_any, + '_bind': bind_any, + '_divide': divide_any}, - elif '_type' in current and '_type' in update and current['_type'] == update['_type']: - outcome = current.copy() + 'quote': { + '_type': 'quote', + '_generate': generate_quote, + '_sort': sort_quote}, - for key in update: - if key == '_type_parameters' and '_type_parameters' in current: - for parameter in update['_type_parameters']: - parameter_key = f'_{parameter}' - if parameter in current['_type_parameters']: - if parameter_key in current: - outcome[parameter_key] = self.resolve_schemas( - current[parameter_key], - update[parameter_key]) - elif parameter_key in update: - outcome[parameter_key] = update[parameter_key] - # else: - # outcome[parameter_key] = {} - else: - outcome[parameter_key] = update[parameter_key] - elif key not in outcome or type_parameter_key(current, key): - key_update = update[key] - if key_update: - outcome[key] = key_update - else: - outcome[key] = self.resolve_schemas( - outcome.get(key), - update[key]) + 'tuple': { + '_type': 'tuple', + '_default': default_tuple, + '_apply': apply_tuple, + '_check': check_tuple, + '_slice': slice_tuple, + '_serialize': serialize_tuple, + '_deserialize': deserialize_tuple, + '_dataclass': dataclass_tuple, + '_fold': fold_tuple, + '_divide': divide_tuple, + '_bind': bind_tuple, + '_description': 'tuple of an ordered set of typed values'}, - elif '_type' in update and '_type' not in current: - outcome = self.resolve(update, current) + 'union': { + '_type': 'union', + '_default': default_union, + '_apply': apply_union, + '_check': check_union, + '_slice': slice_union, + '_serialize': serialize_union, + '_deserialize': deserialize_union, + '_dataclass': dataclass_union, + '_fold': fold_union, + '_description': 'union of a set of possible types'}} - else: - outcome = self.resolve(current, update) - # elif '_type' in current: - # outcome = self.resolve(current, update) +class TypeSystem(Registry): + """Handles type schemas and their operation""" - # elif '_type' in update: - # outcome = self.resolve(update, current) + def __init__(self): + super().__init__() - # else: - # outcome = self.resolve(current, update) - # outcome = current.copy() + self.inherits = {} - # for key in update: - # if not key in outcome or is_schema_key(update, key): - # key_update = update[key] - # if key_update: - # outcome[key] = key_update - # else: - # outcome[key] = self.resolve_schemas( - # outcome.get(key), - # update[key]) + self.default_registry = Registry(function_keys=[ + 'schema', + 'core']) - return outcome + self.check_registry = Registry(function_keys=[ + 'state', + 'schema', + 'core']) + self.apply_registry = Registry(function_keys=[ + 'current', + 'update', + 'schema', + 'core']) - def check_state(self, schema, state): - schema = self.access(schema) + self.serialize_registry = Registry(function_keys=[ + 'value', + 'schema', + 'core']) - check_function = self.choose_method( - schema, - state, - 'check') + self.deserialize_registry = Registry(function_keys=[ + 'encoded', + 'schema', + 'core']) - return check_function( - schema, - state, - self) + self.fold_registry = Registry(function_keys=[ + 'method', + 'state', + 'schema', + 'core']) + self.react_registry = Registry() + self.method_registry = Registry() - def check(self, initial_schema, state): - schema = self.retrieve(initial_schema) - return self.check_state(schema, state) - + # register all the base methods and types + self.apply_registry.register( + 'set', + set_apply) - def fold_state(self, schema, state, method, values): - schema = self.access(schema) + self.register_types(registry_types) + self.register_types(base_type_library) - fold_function = self.choose_method( - schema, - state, - 'fold') + register_units(self, units) + register_base_reactions(self) - return fold_function( - schema, - state, - method, - values, - self) + def register_types(core, type_library): + for type_key, type_data in type_library.items(): + if not core.exists(type_key): + core.register( + type_key, + type_data) - def fold(self, initial_schema, state, method, values=None): - schema = self.retrieve(initial_schema) - return self.fold_state( - schema, - state, - method, - values) + return core + + def lookup(self, type_key, attribute): + return self.access(type_key).get(attribute) - def validate(self, schema, state): - # TODO: - # go through the state using the schema and - # return information about what doesn't match - return {} + def lookup_registry(self, underscore_key): + """ + access the registry for the given key + """ + if underscore_key == '_type': + return self + root = underscore_key.strip('_') + registry_key = f'{root}_registry' + if hasattr(self, registry_key): + return getattr(self, registry_key) - def apply_update(self, schema, state, update): - if isinstance(update, dict) and '_react' in update: - new_state = self.react( - schema, - state, - update['_react']) - state = self.deserialize(schema, new_state) + def find_registry(self, underscore_key): + """ + access the registry for the given key + and create if it doesn't exist + """ - elif isinstance(update, dict) and '_fold' in update: - fold = update['_fold'] + registry = self.lookup_registry(underscore_key) + if registry is None: + registry = Registry() + setattr( + self, + f'{underscore_key[1:]}_registry', + registry) - if isinstance(fold, dict): - method = fold['method'] - values = { - key: value - for key, value in fold.items() - if key != 'method'} + return registry - elif isinstance(fold, str): - method = fold - values = {} - else: - raise Exception(f'unknown fold: {pf(update)}') + def register(self, key, schema, alternate_keys=tuple(), force=False): + """ + register the schema under the given key in the registry + """ - state = self.fold( - schema, - state, - method, - values) + if isinstance(schema, str): + schema = self.find(schema) + schema = copy.deepcopy(schema) - elif '_apply' in schema and schema['_apply'] != 'any': - apply_function = self.apply_registry.access(schema['_apply']) - - state = apply_function( - schema, - state, - update, - self) + if '_type' not in schema: + schema['_type'] = key - elif isinstance(schema, str) or isinstance(schema, list): - schema = self.access(schema) - state = self.apply_update(schema, state, update) + if isinstance(schema, dict): + inherits = schema.get('_inherit', []) # list of immediate inherits + if isinstance(inherits, str): + inherits = [inherits] + schema['_inherit'] = inherits - elif isinstance(update, dict): - for key, branch in update.items(): - if key not in schema: - raise Exception( - f'trying to update a key that is not in the schema' - f'for state: {key}\n{state}\nwith schema:\n{schema}') - else: - subupdate = self.apply_update( - self.access(schema[key]), - state[key], - branch) + self.inherits[key] = [] + for inherit in inherits: + inherit_type = self.access(inherit) + new_schema = copy.deepcopy(inherit_type) - state[key] = subupdate - else: - raise Exception( - f'trying to apply update\n {update}\nto state\n {state}\n' - f'with schema\n {schema}\nbut the update is not a dict') + # schema = self.resolve( + # new_schema, + # schema) - return state + schema = self.merge_schemas( + new_schema, + schema) + # schema = type_merge( + # new_schema, + # schema) - def apply(self, original_schema, initial, update): - schema = self.access(original_schema) - state = copy.deepcopy(initial) - return self.apply_update(schema, state, update) + self.inherits[key].append( + inherit_type) + parameters = schema.get('_type_parameters', []) + for subkey, subschema in schema.items(): + if subkey == '_default' or subkey in TYPE_FUNCTION_KEYS or is_method_key(subkey, parameters): + if callable(subschema): + registry = self.find_registry(subkey) + function_name, module_key = registry.register_function(subschema) - def apply_slice(self, schema, state, path, update): - path = path or () - if len(path) == 0: - result = self.apply( - schema, - state, - update) + schema[subkey] = function_name + else: + schema[subkey] = subschema + elif subkey not in type_schema_keys: + if schema['_type'] in SYMBOL_TYPES: + schema[subkey] = subschema + else: + lookup = self.find(subschema) + if lookup is None: + import ipdb; ipdb.set_trace() + raise Exception( + f'trying to register a new type ({key}), ' + f'but it depends on a type ({subkey}) which is not in the registry') + else: + schema[subkey] = lookup else: - subschema, substate = self.slice( - schema, - state, - path[0]) + raise Exception( + f'all type definitions must be dicts ' + f'with the following keys: {type_schema_keys}\nnot: {schema}') - if len(path) == 1: - subresult = self.apply( - subschema, - substate, - update) + super().register( + key, + schema, + alternate_keys, + force) - result = self.bind( - schema, - state, - path[1:], - subschema, - subresult) - else: - subresult = self.apply_slice( - subschema, - substate, - path[1:], - update) + def resolve_parameters(self, type_parameters, schema): + """ + find the types associated with any type parameters in the schema + """ - result = state + return { + type_parameter: self.access( + schema.get(f'_{type_parameter}')) + for type_parameter in type_parameters} - return result + def register_reaction(self, reaction_key, reaction): + self.react_registry.register( + reaction_key, + reaction) - def set_update(self, schema, state, update): - if '_apply' in schema: - apply_function = self.apply_registry.access('set') - - state = apply_function( - schema, - state, - update, - self) - elif isinstance(schema, str) or isinstance(schema, list): - schema = self.access(schema) - state = self.set_update(schema, state, update) + def types(self): + return { + type_key: type_data + for type_key, type_data in self.registry.items()} - elif isinstance(update, dict): - for key, branch in update.items(): - if key not in schema: - raise Exception( - f'trying to update a key that is not in the schema' - f'for state: {key}\n{state}\nwith schema:\n{schema}') - else: - subupdate = self.set_update( - schema[key], - state[key], - branch) - state[key] = subupdate - else: - raise Exception( - f'trying to apply update\n {update}\nto state\n {state}\n' - f'with schema\n{schema}, but the update is not a dict') + def merge_schemas(self, current, update): + if current == update: + return update + if not isinstance(current, dict): + return update + if not isinstance(update, dict): + return update + + merged = {} - return state + for key in union_keys(current, update): + if key in current: + if key in update: + subcurrent = current[key] + subupdate = update[key] + if subcurrent == current or subupdate == update: + continue + merged[key] = self.merge_schemas( + subcurrent, + subupdate) + else: + merged[key] = current[key] + else: + merged[key] = update[key] - def set(self, original_schema, initial, update): - schema = self.access(original_schema) - state = copy.deepcopy(initial) + return merged - return self.set_update(schema, state, update) + def sort(self, schema, state): + schema = self.access(schema) - def merge_recur(self, schema, state, update): - if is_empty(schema): - merge_state = update + sort_function = self.choose_method( + schema, + state, + 'sort') - elif is_empty(update): - merge_state = state + return sort_function( + self, + schema, + state) - elif isinstance(update, dict): - if isinstance(state, dict): - for key, value in update.items(): - if is_schema_key(key): - state[key] = value - else: - if isinstance(schema, str): - schema = self.access(schema) - state[key] = self.merge_recur( - schema.get(key), - state.get(key), - value) - merge_state = state - else: - merge_state = update - else: - merge_state = update + def exists(self, type_key): + return type_key in self.registry - return merge_state + def find(self, schema, strict=False): + """ + expand the schema to its full type information from the type registry + """ - def merge(self, schema, state, path, update_schema, update_state, defer=False): - top_schema, top_state = self.set_slice( - schema, - state, - path, - update_schema, - update_state, - defer) + found = None - return self.generate(top_schema, top_state) + if schema is None: + return self.access('any', strict=strict) + elif isinstance(schema, dict): + if '_description' in schema: + return schema - def bind(self, schema, state, key, target_schema, target_state): - schema = self.retrieve(schema) + elif '_union' in schema: + union_schema = { + '_type': 'union', + '_type_parameters': []} - bind_function = self.choose_method( - schema, - state, - 'bind') + for index, element in enumerate(schema['_union']): + union_schema['_type_parameters'].append(str(index)) + union_schema[f'_{index}'] = element - return bind_function( - schema, - state, - key, - target_schema, - target_state, - self) + return self.access( + union_schema, + strict=strict) + elif '_type' in schema: + registry_type = self.retrieve( + schema['_type']) - def set_slice(self, schema, state, path, target_schema, target_state, defer=False): + # found = self.resolve( + # registry_type, + # schema) - ''' - Makes a local modification to the schema/state at the path, and - returns the top_schema and top_state - ''' + found = self.merge_schemas( + registry_type, + schema) - if len(path) == 0: - # deal with paths of length 0 - # this should never happen? - merged_schema = self.resolve_schemas( - schema, - target_schema) + # found = schema.copy() - merged_state = deep_merge( - state, - target_state) + # for key, value in registry_type.items(): + # if key == '_type' or key not in found: + # found[key] = value - return merged_schema, merged_state + else: + found = { + key: self.access( + branch, + strict=strict) if key != '_default' else branch + for key, branch in schema.items()} - elif len(path) == 1: - key = path[0] - destination_schema, destination_state = self.slice( - schema, - state, - key) + elif isinstance(schema, int): + return schema - final_schema = self.resolve_schemas( - destination_schema, - target_schema) + elif isinstance(schema, tuple): + tuple_schema = { + '_type': 'tuple', + '_type_parameters': []} - if not defer: - result_state = self.merge_recur( - final_schema, - destination_state, - target_state) + for index, element in enumerate(schema): + tuple_schema['_type_parameters'].append(str(index)) + tuple_schema[f'_{index}'] = element - else: - result_state = self.merge_recur( - final_schema, - target_state, - destination_state) + return self.access( + tuple_schema, + strict=strict) - return self.bind( + elif isinstance(schema, list): + if isinstance(schema[0], int): + return schema + + bindings = [] + if len(schema) > 1: + schema, bindings = schema + else: + schema = schema[0] + found = self.access( schema, - state, - key, - final_schema, - result_state) + strict=strict) - else: - path = resolve_path(path) + if len(bindings) > 0: + found = found.copy() - head = path[0] - tail = path[1:] - - down_schema, down_state = self.slice( - schema, - state, - head) + if '_type_parameters' not in found: + found['_type_parameters'] = [] + for index, binding in enumerate(bindings): + found['_type_parameters'].append(str(index)) + found[f'_{index}'] = binding + else: + for parameter, binding in zip(found['_type_parameters'], bindings): + binding_type = self.access( + binding, + strict=strict) or binding - result_schema, result_state = self.set_slice( - down_schema, - down_state, - tail, - target_schema, - target_state, - defer=defer) + found[f'_{parameter}'] = binding_type - return self.bind( - schema, - state, - head, - result_schema, - result_state) + elif isinstance(schema, str): + found = self.registry.get(schema) + if found is None and schema not in ('', '{}'): + try: + parse = parse_expression(schema) + if parse != schema: + found = self.access( + parse, + strict=strict) + elif not strict: + found = {'_type': schema} - def serialize(self, schema, state): - schema = self.retrieve(schema) + except Exception: + print(f'type did not parse: {schema}') + traceback.print_exc() + + return found - serialize_function = self.choose_method( - schema, - state, - 'serialize') - return serialize_function( + def access(self, schema, strict=False): + if isinstance(schema, str): + return self.access_str(schema, strict=strict) + else: + return self.find(schema, strict=strict) + + + @functools.lru_cache(maxsize=None) + def access_str(self, schema, strict=False): + return self.find( schema, - state, - self) + strict) - def deserialize(self, schema, state): - schema = self.retrieve(schema) + def retrieve(self, schema): + """ + like access(schema) but raises an exception if nothing is found + """ - deserialize_function = self.choose_method( + found = self.find( schema, - state, - 'deserialize') + strict=True) - return deserialize_function( - schema, - state, - self) + if found is None: + raise Exception(f'schema not found for type: {schema}') + return found - def fill_ports(self, interface, wires=None, state=None, top_schema=None, top_state=None, path=None): - # deal with wires - if wires is None: - wires = {} - if state is None: - state = {} - if top_schema is None: - top_schema = schema - if top_state is None: - top_state = state - if path is None: - path = [] + def find_parameter(self, schema, parameter): + schema_key = f'_{parameter}' + if schema_key not in schema: + schema = self.access(schema) + if schema_key not in schema: + return 'any' + # raise Exception(f'parameter {parameter} not found in schema:\n {schema}') - if isinstance(interface, str): - interface = {'_type': interface} + parameter_type = self.access( + schema[schema_key]) - for port_key, subwires in wires.items(): - if port_key in interface: - port_schema = interface[port_key] - else: - port_schema, subwires = self.slice( - interface, - wires, - port_key) + return parameter_type - if isinstance(subwires, dict): - if isinstance(state, dict): - state = self.fill_ports( - port_schema, - wires=subwires, - state=state, - top_schema=top_schema, - top_state=top_state, - path=path) - else: - if isinstance(subwires, str): - subwires = [subwires] + def parameters_for(self, initial_schema): + ''' + find any type parameters for this schema if they are present + ''' - subschema, substate = self.set_slice( - top_schema, - top_state, - path[:-1] + subwires, - port_schema, - self.default(port_schema), - defer=True) + if '_type_parameters' in initial_schema: + schema = initial_schema + else: + schema = self.access(initial_schema) - return state + if '_type_parameters' not in schema: + return [] + else: + result = [] + for parameter in schema['_type_parameters']: + parameter_key = f'_{parameter}' + if parameter_key not in schema: + raise Exception( + f'looking for type parameter {parameter_key} but it is not in the schema:\n {pf(schema)}') + else: + parameter_type = schema[parameter_key] + result.append( + parameter_type) + return result - def fill_state(self, schema, state=None, top_schema=None, top_state=None, path=None, type_key=None, context=None): - # if a port is disconnected, build a store - # for it under the '_open' key in the current - # node (?) - # inform the user that they have disconnected - # ports somehow + def validate_schema(self, schema, enforce_connections=False): + # TODO: + # check() always returns true or false, + # validate() returns information about what doesn't match - if schema is None: - return None - if state is None: - state = self.default(schema) - if top_schema is None: - top_schema = schema - if top_state is None: - top_state = state - if path is None: - path = [] + # add ports and wires + # validate ports are wired to a matching type, + # either the same type or a subtype (more specific type) + # declared ports are equivalent to the presence of a process + # where the ports need to be looked up - if '_inputs' in schema: - inputs = state.get('inputs', {}) - state = self.fill_ports( - schema['_inputs'], - wires=inputs, - state=state, - top_schema=top_schema, - top_state=top_state, - path=path) + report = {} - if '_outputs' in schema: - outputs = state.get('outputs', {}) - state = self.fill_ports( - schema['_outputs'], - wires=outputs, - state=state, - top_schema=top_schema, - top_state=top_state, - path=path) + if schema is None: + report = 'schema cannot be None' - if isinstance(schema, str): - schema = self.access(schema) + elif isinstance(schema, str): + typ = self.access( + schema, + strict=True) - branches = non_schema_keys(schema) + if typ is None: + report = f'type: {schema} is not in the registry' - if isinstance(state, dict): - for branch in branches: - subpath = path + [branch] - state[branch] = self.fill_state( - schema[branch], - state=state.get(branch), - top_schema=top_schema, - top_state=top_state, - path=subpath) - - state_keys = non_schema_keys(state) - for key in set(state_keys) - set(branches): - subschema, substate = self.slice( - schema, - state, - key) - - subpath = path + [key] - state[key] = self.fill_state( - subschema, - substate, - top_schema=top_schema, - top_state=top_state, - path=subpath) - - return state - - - def fill(self, original_schema, state=None): - schema = self.access(original_schema) - - return self.fill_state( - schema, - state=state) - - - def ports_schema(self, schema, instance, edge_path, ports_key='inputs'): - found = self.access(schema) - - edge_schema, edge_state = self.slice( - schema, - instance, - edge_path) - - ports_schema = edge_state.get(f'_{ports_key}', - edge_schema.get(f'_{ports_key}')) - ports = edge_state.get(ports_key) - - return ports_schema, ports - - - def view(self, schema, wires, path, top_schema=None, top_state=None): - result = {} - - if isinstance(wires, str): - wires = [wires] - - if isinstance(wires, (list, tuple)): - _, result = self.slice( - top_schema, - top_state, - list(path) + list(wires)) - - elif isinstance(wires, dict): - result = {} - for port_key, port_path in wires.items(): - subschema, _ = self.slice( - schema, - {}, - port_key) - - inner_view = self.view( - subschema, - port_path, - path, - top_schema=top_schema, - top_state=top_state) - - if inner_view is not None: - result[port_key] = inner_view - else: - raise Exception(f'trying to project state with these ports:\n{schema}\nbut not sure what these wires are:\n{wires}') - - return result - - - def view_edge(self, schema, state, edge_path=None, ports_key='inputs'): - """ - project the current state into a form the edge expects, based on its ports. - """ - - if schema is None: - return None - if state is None: - state = self.default(schema) - if edge_path is None: - edge_path = [] - - ports_schema, ports = self.ports_schema( - schema, - state, - edge_path=edge_path, - ports_key=ports_key) - - if not ports_schema: - return None - if ports is None: - return None - - return self.view( - ports_schema, - ports, - edge_path[:-1], - top_schema=schema, - top_state=state) - - - def project(self, ports, wires, path, states): - result = {} - - if isinstance(wires, str): - wires = [wires] - - if isinstance(wires, (list, tuple)): - destination = resolve_path(list(path) + list(wires)) - result = set_path( - result, - destination, - states) - - elif isinstance(wires, dict): - if isinstance(states, list): - result = [ - self.project(ports, wires, path, state) - for state in states] - else: - branches = [] - for key in wires.keys(): - subports, substates = self.slice(ports, states, key) - projection = self.project( - subports, - wires[key], - path, - substates) - - if projection is not None: - branches.append(projection) - - branches = [ - branch - for branch in branches - if branch is not None] - - result = {} - for branch in branches: - deep_merge(result, branch) - else: - raise Exception( - f'inverting state\n {states}\naccording to ports schema\n {ports}\nbut wires are not recognized\n {wires}') - - return result - - - def project_edge(self, schema, instance, edge_path, states, ports_key='outputs'): - """ - Given states from the perspective of an edge (through its ports), produce states aligned to the tree - the wires point to. - (inverse of view) - """ - - if schema is None: - return None - if instance is None: - instance = self.default(schema) - - ports_schema, ports = self.ports_schema( - schema, - instance, - edge_path, - ports_key) - - if ports_schema is None: - return None - if ports is None: - return None - - return self.project( - ports_schema, - ports, - edge_path[:-1], - states) - - - def equivalent(self, icurrent, iquestion): - if icurrent == iquestion: - return True - - current = self.access(icurrent) - question = self.access(iquestion) + elif isinstance(schema, dict): + report = {} - if current is None: - return question is None + schema_keys = set([]) + branches = set([]) - if current == {}: - return question == {} + for key, value in schema.items(): + if key == '_type': + typ = self.access( + value, + strict=True) + if typ is None: + report[key] = f'type: {value} is not in the registry' - elif '_type' in current: - if '_type' in question: - if current['_type'] == question['_type']: - if '_type_parameters' in current: - if '_type_parameters' in question: - for parameter in current['_type_parameters']: - parameter_key = f'_{parameter}' - if parameter_key in question: - if parameter_key not in current: - return False + elif key in type_schema_keys: + schema_keys.add(key) + registry = self.lookup_registry(key) + if registry is None or key == '_default': + # deserialize and serialize back and check it is equal + pass + elif isinstance(value, str): + element = registry.access( + value, + strict=True) - if not self.equivalent(current[parameter_key], question[parameter_key]): - return False - else: - return False + if element is None: + report[key] = f'no entry in the {key} registry for: {value}' + elif not inspect.isfunction(value): + report[key] = f'unknown value for key {key}: {value}' else: - return False - else: - return False - else: - if '_type' in question: - return False - - for key, value in current.items(): - if not is_schema_key(key): # key not in type_schema_keys: - if key not in question or not self.equivalent(current.get(key), question[key]): - return False - - for key in set(question.keys()) - set(current.keys()): - if not is_schema_key(key): # key not in type_schema_keys: - if key not in question or not self.equivalent(current.get(key), question[key]): - return False - - return True - - - def inherits_from(self, descendant, ancestor): - descendant = self.access(descendant) - ancestor = self.access(ancestor) + branches.add(key) + branch_report = self.validate_schema(value) + if len(branch_report) > 0: + report[key] = branch_report - if descendant == {}: - return ancestor == {} + return report - if descendant is None: - return True - if ancestor is None: - return False + # TODO: if its an edge, ensure ports match wires + # TODO: make this work again, return information about what is wrong + # with the schema + def validate_state(self, original_schema, state): + schema = self.access(original_schema) + validation = {} - if isinstance(ancestor, int): - if isinstance(descendant, int): - return ancestor == descendant + if '_serialize' in schema: + if '_deserialize' not in schema: + validation = { + '_deserialize': f'serialize found in type without deserialize: {schema}' + } else: - return False - - elif isinstance(ancestor, list): - if isinstance(descendant, list): - if len(ancestor) == len(descendant): - for a, d in zip(ancestor, descendant): - if not self.inherits_from(d, a): - return False - else: - return False - - elif '_type' in ancestor and ancestor['_type'] == 'any': - return True - - elif '_type' in descendant: - if '_inherit' in descendant: - for inherit in descendant['_inherit']: - - if self.equivalent(inherit, ancestor) or self.inherits_from(inherit, ancestor): - return True - - return False - - elif '_type_parameters' in descendant: - for type_parameter in descendant['_type_parameters']: - parameter_key = f'_{type_parameter}' - if parameter_key in ancestor and parameter_key in descendant: - if not self.inherits_from(descendant[parameter_key], ancestor[parameter_key]): - return False - - if '_type' not in ancestor or descendant['_type'] != ancestor['_type']: - return False + serialize = self.serialize_registry.access( + schema['_serialize']) + deserialize = self.deserialize_registry.access( + schema['_deserialize']) + serial = serialize(state) + pass_through = deserialize(serial) + if state != pass_through: + validation = f'state and pass_through are not the same: {serial}' else: - for key, value in ancestor.items(): + for key, subschema in schema.items(): if key not in type_schema_keys: - # if not key.startswith('_'): - if key in descendant: - if not self.inherits_from(descendant[key], value): - return False + if key not in state: + validation[key] = f'key present in schema but not in state: {key}\nschema: {schema}\nstate: {state}\n' else: - return False - - return True - - - # def infer_wires(self, ports, state, wires, top_schema=None, top_state=None, path=None, internal_path=None): - def infer_wires(self, ports, wires, top_schema=None, top_state=None, path=None, internal_path=None): - top_schema = top_schema or {} - top_state = top_state or state - path = path or () - internal_path = internal_path or () - - if isinstance(ports, str): - ports = self.access(ports) - - if isinstance(wires, (list, tuple)): - if len(wires) == 0: - destination_schema, destination_state = top_schema, top_state - - else: - destination_schema, destination_state = self.slice( - top_schema, - top_state, - path[:-1] + wires) - - merged_schema = apply_schema( - 'schema', - destination_schema, - ports, - self) - - merged_state = self.complete( - merged_schema, - destination_state) - - else: - for port_key, port_wires in wires.items(): - subschema, substate = self.slice( - ports, - {}, - port_key) - - if isinstance(port_wires, dict): - top_schema, top_state = self.infer_wires( - subschema, - # substate, - port_wires, - top_schema=top_schema, - top_state=top_state, - path=path, - internal_path=internal_path+(port_key,)) - - # port_wires must be a list - elif len(port_wires) == 0: - raise Exception(f'no wires at port "{port_key}" in ports {ports} with state {state}') - - else: - compound_path = resolve_path( - path[:-1] + tuple(port_wires)) + subvalidation = self.validate_state( + subschema, + state[key]) + if not (subvalidation is None or len(subvalidation) == 0): + validation[key] = subvalidation - compound_schema, compound_state = self.set_slice( - {}, {}, - compound_path, - subschema or 'any', - self.default(subschema)) + return validation - top_schema = self.resolve( - top_schema, - compound_schema) - top_state = self.merge_recur( - top_schema, - compound_state, - top_state) + def representation(self, schema, level=None): + ''' + produce a string representation of the schema + * intended to be the inverse of parse_expression() + ''' - return top_schema, top_state + if isinstance(schema, str): + return schema - def infer_edge(self, schema, state, top_schema=None, top_state=None, path=None): - ''' - given the schema and state for this edge, and its path relative to - the top_schema and top_state, make all the necessary completions to - both the schema and the state according to the input and output schemas - of this edge in '_inputs' and '_outputs', along the wires in its state - under 'inputs' and 'outputs'. + elif isinstance(schema, tuple): + inner = [ + self.representation(element) + for element in schema] - returns the top_schema and top_state, even if the edge is deeply embedded, - as the particular wires could have implications anywhere in the tree. - ''' + pipes = '|'.join(inner) + return f'({pipes})' + + elif isinstance(schema, dict): + if '_type' in schema: + type = schema['_type'] - schema = schema or {} - top_schema = top_schema or schema - top_state = top_state or state - path = path or () + inner = [] + block = '' + if '_type_parameters' in schema: + for parameter_key in schema['_type_parameters']: + schema_key = f'_{parameter_key}' + if schema_key in schema: + parameter = self.representation( + schema[schema_key]) + inner.append(parameter) + else: + inner.append('()') - if self.check('edge', state): - for port_key in ['inputs', 'outputs']: - ports = state.get(port_key) - schema_key = f'_{port_key}' - port_schema = schema.get(schema_key, {}) - state_schema = state.get(schema_key, {}) + commas = ','.join(inner) + block = f'[{commas}]' - schema[schema_key] = self.resolve( - port_schema, - self.access( - state_schema)) + if type == 'tuple': + pipes = '|'.join(inner) + return f'({pipes})' + else: + return f"{type}{block}" - if ports: - top_schema, top_state = self.infer_wires( - schema[schema_key], - # state, - ports, - top_schema=top_schema, - top_state=top_state, - path=path) + else: + inner = {} + for key in non_schema_keys(schema): + subschema = self.representation( + schema[key]) - return top_schema, top_state + inner[key] = subschema + colons = [ + f'{key}:{value}' + for key, value in inner.items()] - def infer_schema(self, schema, state, top_schema=None, top_state=None, path=None): - """ - Given a schema fragment and an existing state with _type keys, - return the full schema required to describe that state, - and whatever state was hydrated (edges) during this process + pipes = '|'.join(colons) + return f'({pipes})' + else: + return str(schema) - """ - # during recursive call, schema is kept at the top level and the - # path is used to access it (!) + def default(self, schema): + ''' + produce the default value for the provided schema + ''' - schema = schema or {} - top_schema = top_schema or schema - top_state = top_state or state - path = path or () + default = None + found = self.retrieve(schema) - if isinstance(state, dict): - state_schema = None - if '_type' in state: - state_type = { - key: value - for key, value in state.items() - if is_schema_key(key)} + if '_default' in found: + default_value = found['_default'] + if isinstance(default_value, str): + default_method = self.default_registry.access(default_value) + if default_method and callable(default_method): + default = default_method(found, self) + else: + default = self.deserialize( + found, + default_value) - schema = self.resolve( - schema, - state_type) + elif not '_deserialize' in found: + raise Exception( + f'asking for default but no deserialize in {found}') - if '_type' in schema: - hydrated_state = self.deserialize( - schema, - state) + else: + default = self.deserialize(found, found['_default']) - top_schema, top_state = self.set_slice( - top_schema, - top_state, - path, - schema, - hydrated_state) + else: + default = {} + for key, subschema in found.items(): + if not is_schema_key(key): + default[key] = self.default(subschema) - top_schema, top_state = self.infer_edge( - schema, - hydrated_state, - top_schema, - top_state, - path) + return default - else: - for key in state: - inner_path = path + (key,) - inner_schema, inner_state = self.slice( - schema, - state, - key) - top_schema, top_state = self.infer_schema( - inner_schema, - inner_state, - top_schema=top_schema, - top_state=top_state, - path=inner_path) + def choose_method(self, schema, state, method_name): + ''' + find in the provided state, or schema if not there, + a method for the given method_name + ''' - elif isinstance(state, str): - pass + method_key = f'_{method_name}' + found = None - else: - type_schema = TYPE_SCHEMAS.get( - type(state).__name__, - 'any') + if isinstance(state, dict) and method_key in state: + found = state[method_key] - top_schema, top_state = self.set_slice( - top_schema, - top_state, - path, - type_schema, - state) + elif isinstance(state, dict) and '_type' in state: + method_type = self.find( + state['_type']) - return top_schema, top_state - + if method_type: + found = method_type.get(method_key) - def hydrate(self, schema, state): - hydrated = self.deserialize(schema, state) - return self.fill(schema, hydrated) + if not found and isinstance(schema, dict) and method_key in schema: + found = schema[method_key] + if found is None: + any_type = self.access('any') + found = any_type[method_key] - def complete(self, initial_schema, initial_state): - full_schema = self.access( - initial_schema) + registry = self.lookup_registry(method_key) + method_function = registry.access( + found) - state = self.deserialize( - full_schema, - initial_state) + return method_function - # fill in the parts of the composition schema - # determined by the state - schema, state = self.infer_schema( - full_schema, - state) - final_state = self.fill(schema, state) + def slice(self, schema, state, path): + ''' + find the subschema and substate at a node in the place graph + given by the provided path + ''' - # TODO: add flag to types.access(copy=True) - return self.access(schema), final_state - + if not isinstance(path, (list, tuple)): + path = [path] - def generate_recur(self, schema, state, top_schema=None, top_state=None, path=None): - found = self.retrieve( - schema) + schema = self.access(schema) + if '..' in path: + path = resolve_path(path) - generate_function = self.choose_method( - found, + slice_function = self.choose_method( + schema, state, - 'generate') + 'slice') - return generate_function( - self, - found, + return slice_function( + schema, state, - top_schema=top_schema, - top_state=top_state, - path=path) + path, + self) - def generate(self, schema, state): - merged_schema, merged_state = self.sort( - schema, - state) + def match_node(self, schema, state, pattern): + if isinstance(pattern, dict): + if not isinstance(state, dict): + return False - _, _, top_schema, top_state = self.generate_recur( - merged_schema, - merged_state) + if '_type' in pattern and not self.is_compatible(schema, pattern): + return False - return top_schema, top_state + for key, subpattern in pattern.items(): + if key.startswith('_'): + continue + if key in schema: + subschema = schema[key] + else: + subschema = schema - def find_method(self, schema, method_key): - if not isinstance(schema, dict) or method_key not in schema: - schema = self.access(schema) + if key in state: + matches = self.match_node( + subschema, + state[key], + pattern[key]) + if not matches: + return False + else: + return False - if method_key in schema: - registry = self.lookup_registry( - method_key) + return True - if registry is not None: - method_name = schema[method_key] - method = registry.access(method_name) + else: + return pattern == state - return method + def match_recur(self, schema, state, pattern, mode='first', path=()): + matches = [] + + match = self.match_node( + schema, + state, + pattern) + + if match: + if mode == 'first' or mode == 'immediate': + return [path] + else: + matches.append(path) + elif mode == 'immediate': + return [] + + if isinstance(state, dict): + for key, substate in state.items(): + if key.startswith('_'): + continue + + if key in schema: + subschema = schema[key] + else: + subschema = schema - def import_types(self, package, strict=False): - for type_key, type_data in package.items(): - if not (strict and self.exists(type_key)): - self.register( - type_key, - type_data) + submatches = self.match_recur( + subschema, + state[key], + pattern, + mode=mode, + path=path + (key,)) + if mode == 'first' and len(submatches) > 0: + return submatches[0:1] + else: + matches.extend(submatches) - def define(self, method_name, methods): - method_key = f'_{method_name}' - for type_key, method in methods.items(): - self.register( - type_key, - {method_key: method}) + return matches - def link_place(self, place, link): - pass + def match(self, original_schema, state, pattern, mode='first', path=()): + """ + find the path or paths to any instances of a + given pattern in the tree. + "mode" can be a few things: + * immediate: only match top level + * first: only return the first match + * random: return a random match of all that matched + * all (or any other value): return every match in the tree + """ - def compose(self, a, b): - pass + schema = self.access(original_schema) + matches = self.match_recur( + schema, + state, + pattern, + mode=mode, + path=path) - def query(self, schema, instance, redex): - subschema = {} - return subschema + if mode == 'random': + matches_count = len(matches) + if matches_count > 0: + choice = random.randint( + 0, + matches_count-1) + matches = [matches[choice]] + return matches -def check_number(schema, state, core=None): - return isinstance(state, numbers.Number) -def check_boolean(schema, state, core=None): - return isinstance(state, bool) + def react(self, schema, state, reaction, mode='random'): + # TODO: explain all this + # TODO: after the reaction, fill in the state with missing values + # from the schema -def check_integer(schema, state, core=None): - return isinstance(state, int) and not isinstance(state, bool) + # TODO: add schema to redex and reactum -def check_float(schema, state, core=None): - return isinstance(state, float) + if 'redex' in reaction or 'reactum' in reaction or 'calls' in reaction: + redex = reaction.get('redex', {}) + reactum = reaction.get('reactum', {}) + calls = reaction.get('calls', {}) + else: + # single key with reaction name + reaction_key = list(reaction.keys())[0] + make_reaction = self.react_registry.access( + reaction_key) + react = make_reaction( + schema, + state, + reaction.get(reaction_key, {}), + self) -def check_string(schema, state, core=None): - return isinstance(state, str) + redex = react.get('redex', {}) + reactum = react.get('reactum', {}) + calls = react.get('calls', {}) + paths = self.match( + schema, + state, + redex, + mode=mode) + + # for path in paths: + # path_schema, path_state = self.slice( + # schema, + # state, + # path) -class Edge: - def __init__(self): - pass + def merge_state(before): + remaining = remove_omitted( + redex, + reactum, + before) + merged = deep_merge( + remaining, + reactum) - def inputs(self): - return {} + return merged + for path in paths: + state = transform_path( + state, + path, + merge_state) - def outputs(self): - return {} + return state - def interface(self): - """Returns the schema for this type""" - return { - 'inputs': self.inputs(), - 'outputs': self.outputs()} + # TODO: maybe all fields are optional? + def dataclass(self, schema, path=None): + path = path or [] + dataclass_function = self.choose_method( + schema, + {}, + 'dataclass') -################# -# Apply methods # -################# + return dataclass_function( + schema, + path, + self) + -def apply_boolean(schema, current: bool, update: bool, core=None) -> bool: - """Performs a bit flip if `current` does not match `update`, returning update. Returns current if they match.""" - if current != update: - return update - else: - return current + def resolve(self, schema, update): + if update is None: + return schema + else: + resolve_function = self.choose_method( + schema, + update, + 'resolve') + return resolve_function( + schema, + update, + self) -def serialize_boolean(schema, value: bool, core) -> str: - return str(value) + def resolve_schemas(self, initial_current, initial_update): + current = self.access(initial_current) + update = self.access(initial_update) -def deserialize_boolean(schema, encoded, core) -> bool: - if encoded == 'true': - return True - elif encoded == 'false': - return False - elif encoded == True or encoded == False: - return encoded + if self.equivalent(current, update): + outcome = current + elif self.inherits_from(current, update): + outcome = current -def accumulate(schema, current, update, core): - if current is None: - return update - if update is None: - return current - else: - return current + update + elif self.inherits_from(update, current): + outcome = update + elif '_type' in current and '_type' in update and current['_type'] == update['_type']: + outcome = current.copy() -def set_apply(schema, current, update, core): - if isinstance(current, dict) and isinstance(update, dict): - for key, value in update.items(): - # TODO: replace this with type specific functions (??) - if key in schema: - subschema = schema[key] - elif '_leaf' in schema: - if core.check(schema['_leaf'], value): - subschema = schema['_leaf'] + for key in update: + if key == '_type_parameters' and '_type_parameters' in current: + for parameter in update['_type_parameters']: + parameter_key = f'_{parameter}' + if parameter in current['_type_parameters']: + if parameter_key in current: + outcome[parameter_key] = self.resolve_schemas( + current[parameter_key], + update[parameter_key]) + elif parameter_key in update: + outcome[parameter_key] = update[parameter_key] + # else: + # outcome[parameter_key] = {} + else: + outcome[parameter_key] = update[parameter_key] + elif key not in outcome or type_parameter_key(current, key): + key_update = update[key] + if key_update: + outcome[key] = key_update else: - subschema = schema - elif '_value' in schema: - subschema = schema['_value'] - - current[key] = set_apply( - subschema, - current.get(key), - value, - core) + outcome[key] = self.resolve_schemas( + outcome.get(key), + update[key]) - return current + elif '_type' in update and '_type' not in current: + outcome = self.resolve(update, current) - else: - return update + else: + outcome = self.resolve(current, update) + # elif '_type' in current: + # outcome = self.resolve(current, update) -def concatenate(schema, current, update, core=None): - return current + update + # elif '_type' in update: + # outcome = self.resolve(update, current) + # else: + # outcome = self.resolve(current, update) + # outcome = current.copy() -def dataclass_float(schema, path, core): - return float + # for key in update: + # if not key in outcome or is_schema_key(update, key): + # key_update = update[key] + # if key_update: + # outcome[key] = key_update + # else: + # outcome[key] = self.resolve_schemas( + # outcome.get(key), + # update[key]) + return outcome -def dataclass_integer(schema, path, core): - return int + def check_state(self, schema, state): + schema = self.access(schema) -def divide_float(schema, state, values, core): - divisions = values.get('divisions', 2) - portion = float(state) / divisions - return [ - portion - for _ in range(divisions)] + check_function = self.choose_method( + schema, + state, + 'check') + return check_function( + schema, + state, + self) -# ################## -# # Divide methods # -# ################## -# # support dividing by ratios? -# # ---> divide_float({...}, [0.1, 0.3, 0.6]) -# def divide_float(schema, value, ratios, core=None): -# half = value / 2.0 -# return (half, half) + def check(self, initial_schema, state): + schema = self.retrieve(initial_schema) + return self.check_state(schema, state) + + def fold_state(self, schema, state, method, values): + schema = self.access(schema) -# support function core for registrys? -def divide_integer(schema, value, values, core): - half = value // 2 - other_half = half - if value % 2 == 1: - other_half += 1 - return [half, other_half] + fold_function = self.choose_method( + schema, + state, + 'fold') + return fold_function( + schema, + state, + method, + values, + self) -def divide_longest(schema, dimensions, values, core): - # any way to declare the required keys for this function in the registry? - # find a way to ask a function what type its domain and codomain are - width = dimensions['width'] - height = dimensions['height'] + def fold(self, initial_schema, state, method, values=None): + schema = self.retrieve(initial_schema) + return self.fold_state( + schema, + state, + method, + values) - if width > height: - a, b = divide_integer(width) - return [{'width': a, 'height': height}, {'width': b, 'height': height}] - else: - x, y = divide_integer(height) - return [{'width': width, 'height': x}, {'width': width, 'height': y}] + def validate(self, schema, state): + # TODO: + # go through the state using the schema and + # return information about what doesn't match -def replace(schema, current, update, core=None): - return update + return {} -def serialize_string(schema, value, core=None): - return value + def apply_update(self, schema, state, update): + if isinstance(update, dict) and '_react' in update: + new_state = self.react( + schema, + state, + update['_react']) + state = self.deserialize(schema, new_state) -def deserialize_string(schema, encoded, core=None): - if isinstance(encoded, str): - return encoded + elif isinstance(update, dict) and '_fold' in update: + fold = update['_fold'] + if isinstance(fold, dict): + method = fold['method'] + values = { + key: value + for key, value in fold.items() + if key != 'method'} -def to_string(schema, value, core=None): - return str(value) + elif isinstance(fold, str): + method = fold + values = {} + else: + raise Exception(f'unknown fold: {pf(update)}') -def deserialize_integer(schema, encoded, core=None): - value = None - try: - value = int(encoded) - except: - pass + state = self.fold( + schema, + state, + method, + values) - return value + elif '_apply' in schema and schema['_apply'] != 'any': + apply_function = self.apply_registry.access(schema['_apply']) + + state = apply_function( + schema, + state, + update, + self) + elif isinstance(schema, str) or isinstance(schema, list): + schema = self.access(schema) + state = self.apply_update(schema, state, update) -def deserialize_float(schema, encoded, core=None): - value = None - try: - value = float(encoded) - except: - pass + elif isinstance(update, dict): + for key, branch in update.items(): + if key not in schema: + raise Exception( + f'trying to update a key that is not in the schema' + f'for state: {key}\n{state}\nwith schema:\n{schema}') + else: + subupdate = self.apply_update( + self.access(schema[key]), + state[key], + branch) - return value + state[key] = subupdate + else: + raise Exception( + f'trying to apply update\n {update}\nto state\n {state}\n' + f'with schema\n {schema}\nbut the update is not a dict') + return state -def evaluate(schema, encoded, core=None): - return eval(encoded) + def apply(self, original_schema, initial, update): + schema = self.access(original_schema) + state = copy.deepcopy(initial) + return self.apply_update(schema, state, update) -def apply_list(schema, current, update, core): - element_type = core.find_parameter( - schema, - 'element') - if current is None: - current = [] + def apply_slice(self, schema, state, path, update): + path = path or () + if len(path) == 0: + result = self.apply( + schema, + state, + update) - if core.check(element_type, update): - result = current + [update] - return result + else: + subschema, substate = self.slice( + schema, + state, + path[0]) - elif isinstance(update, list): - result = current + update - # for current_element, update_element in zip(current, update): - # applied = core.apply( - # element_type, - # current_element, - # update_element) + if len(path) == 1: + subresult = self.apply( + subschema, + substate, + update) - # result.append(applied) + result = self.bind( + schema, + state, + path[1:], + subschema, + subresult) - return result - else: - raise Exception(f'trying to apply an update to an existing list, but the update is not a list or of element type:\n update: {update}\n element type: {pf(element_type)}') + else: + subresult = self.apply_slice( + subschema, + substate, + path[1:], + update) + result = state -def check_list(schema, state, core): - element_type = core.find_parameter( - schema, - 'element') + return result - if isinstance(state, list): - for element in state: - check = core.check( - element_type, - element) - if not check: - return False + def set_update(self, schema, state, update): + if '_apply' in schema: + apply_function = self.apply_registry.access('set') + + state = apply_function( + schema, + state, + update, + self) - return True - else: - return False + elif isinstance(schema, str) or isinstance(schema, list): + schema = self.access(schema) + state = self.set_update(schema, state, update) + elif isinstance(update, dict): + for key, branch in update.items(): + if key not in schema: + raise Exception( + f'trying to update a key that is not in the schema' + f'for state: {key}\n{state}\nwith schema:\n{schema}') + else: + subupdate = self.set_update( + schema[key], + state[key], + branch) -def dataclass_list(schema, path, core): - element_type = core.find_parameter( - schema, - 'element') + state[key] = subupdate + else: + raise Exception( + f'trying to apply update\n {update}\nto state\n {state}\n' + f'with schema\n{schema}, but the update is not a dict') - dataclass = core.dataclass( - element_type, - path + ['element']) + return state - return list[dataclass] + def set(self, original_schema, initial, update): + schema = self.access(original_schema) + state = copy.deepcopy(initial) -def slice_list(schema, state, path, core): - element_type = core.find_parameter( - schema, - 'element') + return self.set_update(schema, state, update) - if len(path) > 0: - head = path[0] - tail = path[1:] - if not isinstance(head, int) or head >= len(state): - raise Exception(f'bad index for list: {path} for {state}') + def merge_recur(self, schema, state, update): + if is_empty(schema): + merge_state = update - step = state[head] - return core.slice(element_type, step, tail) - else: - return schema, state + elif is_empty(update): + merge_state = state + elif isinstance(update, dict): + if isinstance(state, dict): + for key, value in update.items(): + if is_schema_key(key): + state[key] = value + else: + if isinstance(schema, str): + schema = self.access(schema) -def serialize_list(schema, value, core=None): - element_type = core.find_parameter( - schema, - 'element') + state[key] = self.merge_recur( + schema.get(key), + state.get(key), + value) + merge_state = state + else: + merge_state = update + else: + merge_state = update - return [ - core.serialize( - element_type, - element) - for element in value] + return merge_state -def deserialize_list(schema, encoded, core=None): - if isinstance(encoded, list): - element_type = core.find_parameter( + def merge(self, schema, state, path, update_schema, update_state, defer=False): + top_schema, top_state = self.set_slice( schema, - 'element') + state, + path, + update_schema, + update_state, + defer) - return [ - core.deserialize( - element_type, - element) - for element in encoded] + return self.generate(top_schema, top_state) -def check_tree(schema, state, core): - leaf_type = core.find_parameter( - schema, - 'leaf') + def bind(self, schema, state, key, target_schema, target_state): + schema = self.retrieve(schema) - if isinstance(state, dict): - for key, value in state.items(): - check = core.check({ - '_type': 'tree', - '_leaf': leaf_type}, - value) + bind_function = self.choose_method( + schema, + state, + 'bind') - if not check: - return core.check( - leaf_type, - value) + return bind_function( + schema, + state, + key, + target_schema, + target_state, + self) - return True - else: - return core.check(leaf_type, state) + def set_slice(self, schema, state, path, target_schema, target_state, defer=False): -def dataclass_tree(schema, path, core): - leaf_type = core.find_parameter( - schema, - 'leaf') + ''' + Makes a local modification to the schema/state at the path, and + returns the top_schema and top_state + ''' - leaf_dataclass = core.dataclass( - leaf_type, - path + ['leaf']) + if len(path) == 0: + # deal with paths of length 0 + # this should never happen? + merged_schema = self.resolve_schemas( + schema, + target_schema) - dataclass_name = '_'.join(path) - # block = f"{dataclass_name} = NewType('{dataclass_name}', Union[{leaf_dataclass}, Mapping[str, '{dataclass_name}']])" - block = f"NewType('{dataclass_name}', Union[{leaf_dataclass}, Mapping[str, '{dataclass_name}']])" + merged_state = deep_merge( + state, + target_state) - dataclass = eval(block) - setattr(data, dataclass_name, dataclass) + return merged_schema, merged_state - return dataclass + elif len(path) == 1: + key = path[0] + destination_schema, destination_state = self.slice( + schema, + state, + key) + final_schema = self.resolve_schemas( + destination_schema, + target_schema) -def slice_tree(schema, state, path, core): - leaf_type = core.find_parameter( - schema, - 'leaf') + if not defer: + result_state = self.merge_recur( + final_schema, + destination_state, + target_state) - if len(path) > 0: - head = path[0] - tail = path[1:] + else: + result_state = self.merge_recur( + final_schema, + target_state, + destination_state) - if not head in state: - state[head] = {} + return self.bind( + schema, + state, + key, + final_schema, + result_state) - step = state[head] - if core.check(leaf_type, step): - return core.slice(leaf_type, step, tail) else: - return core.slice(schema, step, tail) - else: - return schema, state - + path = resolve_path(path) -def serialize_tree(schema, value, core): - if isinstance(value, dict): - encoded = {} - for key, subvalue in value.items(): - encoded[key] = serialize_tree( + head = path[0] + tail = path[1:] + + down_schema, down_state = self.slice( schema, - subvalue, - core) - - else: - leaf_type = core.find_parameter( - schema, - 'leaf') - - if core.check(leaf_type, value): - encoded = core.serialize( - leaf_type, - value) - else: - raise Exception(f'trying to serialize a tree but unfamiliar with this form of tree: {value} - current schema:\n {pf(schema)}') + state, + head) - return encoded + result_schema, result_state = self.set_slice( + down_schema, + down_state, + tail, + target_schema, + target_state, + defer=defer) + return self.bind( + schema, + state, + head, + result_schema, + result_state) -def deserialize_tree(schema, encoded, core): - if isinstance(encoded, dict): - tree = {} - for key, value in encoded.items(): - if key.startswith('_'): - tree[key] = value - else: - tree[key] = deserialize_tree(schema, value, core) - return tree + def serialize(self, schema, state): + schema = self.retrieve(schema) - else: - leaf_type = core.find_parameter( + serialize_function = self.choose_method( schema, - 'leaf') - - if leaf_type: - return core.deserialize( - leaf_type, - encoded) - else: - return encoded + state, + 'serialize') + return serialize_function( + schema, + state, + self) -def apply_map(schema, current, update, core=None): - if not isinstance(current, dict): - raise Exception(f'trying to apply an update to a value that is not a map:\n value: {current}\n update: {update}') - if not isinstance(update, dict): - raise Exception(f'trying to apply an update that is not a map:\n value: {current}\n update: {update}') - value_type = core.find_parameter( - schema, - 'value') + def deserialize(self, schema, state): + schema = self.retrieve(schema) - result = current.copy() + deserialize_function = self.choose_method( + schema, + state, + 'deserialize') - for key, update_value in update.items(): - if key == '_add': - for addition_key, addition in update_value.items(): - generated_schema, generated_state = core.generate( - value_type, - addition) + return deserialize_function( + schema, + state, + self) - result[addition_key] = generated_state - elif key == '_remove': - for remove_key in update_value: - if remove_key in result: - del result[remove_key] + def fill_ports(self, interface, wires=None, state=None, top_schema=None, top_state=None, path=None): + # deal with wires + if wires is None: + wires = {} + if state is None: + state = {} + if top_schema is None: + top_schema = schema + if top_state is None: + top_state = state + if path is None: + path = [] - elif key not in current: - # This supports adding without the '_add' key, if the key is not in the state - generated_schema, generated_state = core.generate( - value_type, - update_value) + if isinstance(interface, str): + interface = {'_type': interface} - result[key] = generated_state + for port_key, subwires in wires.items(): + if port_key in interface: + port_schema = interface[port_key] + else: + port_schema, subwires = self.slice( + interface, + wires, + port_key) - # raise Exception(f'trying to update a key that does not exist:\n value: {current}\n update: {update}') - else: - result[key] = core.apply( - value_type, - result[key], - update_value) + if isinstance(subwires, dict): + if isinstance(state, dict): + state = self.fill_ports( + port_schema, + wires=subwires, + state=state, + top_schema=top_schema, + top_state=top_state, + path=path) - return result + else: + if isinstance(subwires, str): + subwires = [subwires] + subschema, substate = self.set_slice( + top_schema, + top_state, + path[:-1] + subwires, + port_schema, + self.default(port_schema), + defer=True) -def resolve_map(schema, update, core): - if isinstance(update, dict): - value_schema = update.get( - '_value', - schema.get('_value', {})) + return state - for key, subschema in update.items(): - if not is_schema_key(key): - value_schema = core.resolve_schemas( - value_schema, - subschema) - schema['_type'] = update.get( - '_type', - schema.get('_type', 'map')) - schema['_value'] = value_schema + def fill_state(self, schema, state=None, top_schema=None, top_state=None, path=None, type_key=None, context=None): + # if a port is disconnected, build a store + # for it under the '_open' key in the current + # node (?) - return schema + # inform the user that they have disconnected + # ports somehow + if schema is None: + return None + if state is None: + state = self.default(schema) + if top_schema is None: + top_schema = schema + if top_state is None: + top_state = state + if path is None: + path = [] -def resolve_array(schema, update, core): - if not '_shape' in schema: - schema = core.access(schema) - if not '_shape' in schema: - raise Exception(f'array must have a "_shape" key, not {schema}') + if '_inputs' in schema: + inputs = state.get('inputs', {}) + state = self.fill_ports( + schema['_inputs'], + wires=inputs, + state=state, + top_schema=top_schema, + top_state=top_state, + path=path) - data_schema = schema.get('_data', {}) + if '_outputs' in schema: + outputs = state.get('outputs', {}) + state = self.fill_ports( + schema['_outputs'], + wires=outputs, + state=state, + top_schema=top_schema, + top_state=top_state, + path=path) - if '_type' in update: - data_schema = core.resolve_schemas( - data_schema, - update.get('_data', {})) + if isinstance(schema, str): + schema = self.access(schema) - if update['_type'] == 'array': - if '_shape' in update: - if update['_shape'] != schema['_shape']: - raise Exception(f'arrays must be of the same shape, not \n {schema}\nand\n {update}') + branches = non_schema_keys(schema) - elif core.inherits_from(update, schema): - schema.update(update) + if isinstance(state, dict): + for branch in branches: + subpath = path + [branch] + state[branch] = self.fill_state( + schema[branch], + state=state.get(branch), + top_schema=top_schema, + top_state=top_state, + path=subpath) - elif not core.inherits_from(schema, update): - raise Exception(f'cannot resolve incompatible array schemas:\n {schema}\n {update}') + state_keys = non_schema_keys(state) + for key in set(state_keys) - set(branches): + subschema, substate = self.slice( + schema, + state, + key) - else: - for key, subschema in update.items(): - if isinstance(key, int): - key = (key,) + subpath = path + [key] + state[key] = self.fill_state( + subschema, + substate, + top_schema=top_schema, + top_state=top_state, + path=subpath) - if len(key) > len(schema['_shape']): - raise Exception(f'key is longer than array dimension: {key}\n{schema}\n{update}') - elif len(key) == len(schema['_shape']): - data_schema = core.resolve_schemas( - data_schema, - subschema) - else: - shape = tuple_from_type( - schema['_shape']) + return state - subshape = shape[len(key):] - inner_schema = schema.copy() - inner_schema['_shape'] = subshape - inner_schema = core.resolve_schemas( - inner_schema, - subschema) - data_schema = inner_schema['_data'] + def fill(self, original_schema, state=None): + schema = self.access(original_schema) - schema['_data'] = data_schema + return self.fill_state( + schema, + state=state) - return schema + def ports_schema(self, schema, instance, edge_path, ports_key='inputs'): + found = self.access(schema) -def tuple_from_type(tuple_type): - if isinstance(tuple_type, tuple): - return tuple_type + edge_schema, edge_state = self.slice( + schema, + instance, + edge_path) - elif isinstance(tuple_type, list): - return tuple(tuple_type) + ports_schema = edge_state.get(f'_{ports_key}', + edge_schema.get(f'_{ports_key}')) + ports = edge_state.get(ports_key) + + return ports_schema, ports - elif isinstance(tuple_type, dict): - tuple_list = [ - tuple_type[f'_{parameter}'] - for parameter in tuple_type['_type_parameters']] - return tuple(tuple_list) + def view(self, schema, wires, path, top_schema=None, top_state=None): + result = {} - else: - raise Exception(f'do not recognize this type as a tuple: {tuple_type}') + if isinstance(wires, str): + wires = [wires] + if isinstance(wires, (list, tuple)): + _, result = self.slice( + top_schema, + top_state, + list(path) + list(wires)) -# def resolve_tree(schema, update, core): -# import ipdb; ipdb.set_trace() + elif isinstance(wires, dict): + result = {} + for port_key, port_path in wires.items(): + subschema, _ = self.slice( + schema, + {}, + port_key) -# if isinstance(update, dict): -# leaf_schema = schema.get('_leaf', {}) + inner_view = self.view( + subschema, + port_path, + path, + top_schema=top_schema, + top_state=top_state) -# if '_type' in update: -# if update['_type'] == 'map': -# value_schema = update.get('_value', {}) -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# value_schema) + if inner_view is not None: + result[port_key] = inner_view + else: + raise Exception(f'trying to project state with these ports:\n{schema}\nbut not sure what these wires are:\n{wires}') -# elif update['_type'] == 'tree': -# for key, subschema in update.items(): -# if not key.startswith('_'): -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# subschema) -# else: -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# update) + return result -# schema['_leaf'] = leaf_schema -# else: -# for key, subupdate in -# return schema + def view_edge(self, schema, state, edge_path=None, ports_key='inputs'): + """ + project the current state into a form the edge expects, based on its ports. + """ + if schema is None: + return None + if state is None: + state = self.default(schema) + if edge_path is None: + edge_path = [] + ports_schema, ports = self.ports_schema( + schema, + state, + edge_path=edge_path, + ports_key=ports_key) -def dataclass_map(schema, path, core): - value_type = core.find_parameter( - schema, - 'value') + if not ports_schema: + return None + if ports is None: + return None - dataclass = core.dataclass( - value_type, - path + ['value']) - - return Mapping[str, dataclass] + return self.view( + ports_schema, + ports, + edge_path[:-1], + top_schema=schema, + top_state=state) -def check_map(schema, state, core=None): - value_type = core.find_parameter( - schema, - 'value') + def project(self, ports, wires, path, states): + result = {} - if not isinstance(state, dict): - return False + if isinstance(wires, str): + wires = [wires] - for key, substate in state.items(): - if not core.check(value_type, substate): - return False + if isinstance(wires, (list, tuple)): + destination = resolve_path(list(path) + list(wires)) + result = set_path( + result, + destination, + states) - return True + elif isinstance(wires, dict): + if isinstance(states, list): + result = [ + self.project(ports, wires, path, state) + for state in states] + else: + branches = [] + for key in wires.keys(): + subports, substates = self.slice(ports, states, key) + projection = self.project( + subports, + wires[key], + path, + substates) + + if projection is not None: + branches.append(projection) + branches = [ + branch + for branch in branches + if branch is not None] -def slice_map(schema, state, path, core): - value_type = core.find_parameter( - schema, - 'value') + result = {} + for branch in branches: + deep_merge(result, branch) + else: + raise Exception( + f'inverting state\n {states}\naccording to ports schema\n {ports}\nbut wires are not recognized\n {wires}') - if len(path) > 0: - head = path[0] - tail = path[1:] + return result - if not head in state: - state[head] = core.default( - value_type) - step = state[head] - return core.slice( - value_type, - step, - tail) - else: - return schema, state + def project_edge(self, schema, instance, edge_path, states, ports_key='outputs'): + """ + Given states from the perspective of an edge (through its ports), produce states aligned to the tree + the wires point to. + (inverse of view) + """ + if schema is None: + return None + if instance is None: + instance = self.default(schema) -def serialize_map(schema, value, core=None): - value_type = core.find_parameter( - schema, - 'value') + ports_schema, ports = self.ports_schema( + schema, + instance, + edge_path, + ports_key) - return { - key: core.serialize( - value_type, - subvalue) if not is_schema_key(key) else subvalue - for key, subvalue in value.items()} + if ports_schema is None: + return None + if ports is None: + return None + return self.project( + ports_schema, + ports, + edge_path[:-1], + states) -def deserialize_map(schema, encoded, core=None): - if isinstance(encoded, dict): - value_type = core.find_parameter( - schema, - 'value') - return { - key: core.deserialize( - value_type, - subvalue) if not is_schema_key(key) else subvalue - for key, subvalue in encoded.items()} + def equivalent(self, icurrent, iquestion): + if icurrent == iquestion: + return True + current = self.access(icurrent) + question = self.access(iquestion) -def apply_maybe(schema, current, update, core): - if current is None or update is None: - return update - else: - value_type = core.find_parameter( - schema, - 'value') + if current is None: + return question is None - return core.apply( - value_type, - current, - update) + if current == {}: + return question == {} + elif '_type' in current: + if '_type' in question: + if current['_type'] == question['_type']: + if '_type_parameters' in current: + if '_type_parameters' in question: + for parameter in current['_type_parameters']: + parameter_key = f'_{parameter}' + if parameter_key in question: + if parameter_key not in current: + return False -def dataclass_maybe(schema, path, core): - value_type = core.find_parameter( - schema, - 'value') + if not self.equivalent(current[parameter_key], question[parameter_key]): + return False + else: + return False + else: + return False + else: + return False + else: + if '_type' in question: + return False - dataclass = core.dataclass( - value_type, - path + ['value']) - - return Optional[dataclass] + for key, value in current.items(): + if not is_schema_key(key): # key not in type_schema_keys: + if key not in question or not self.equivalent(current.get(key), question[key]): + return False + for key in set(question.keys()) - set(current.keys()): + if not is_schema_key(key): # key not in type_schema_keys: + if key not in question or not self.equivalent(current.get(key), question[key]): + return False -def check_maybe(schema, state, core): - if state is None: return True - else: - value_type = core.find_parameter( - schema, - 'value') - - return core.check(value_type, state) -def slice_maybe(schema, state, path, core): - if state is None: - return schema, None + def inherits_from(self, descendant, ancestor): + descendant = self.access(descendant) + ancestor = self.access(ancestor) - else: - value_type = core.find_parameter( - schema, - 'value') + if descendant == {}: + return ancestor == {} - return core.slice( - value_type, - state, - path) + if descendant is None: + return True + if ancestor is None: + return False -def serialize_maybe(schema, value, core): - if value is None: - return NONE_SYMBOL - else: - value_type = core.find_parameter( - schema, - 'value') + if isinstance(ancestor, int): + if isinstance(descendant, int): + return ancestor == descendant + else: + return False - return core.serialize( - value_type, - value) + elif isinstance(ancestor, list): + if isinstance(descendant, list): + if len(ancestor) == len(descendant): + for a, d in zip(ancestor, descendant): + if not self.inherits_from(d, a): + return False + else: + return False + elif '_type' in ancestor and ancestor['_type'] == 'any': + return True -def deserialize_maybe(schema, encoded, core): - if encoded == NONE_SYMBOL or encoded is None: - return None - else: - value_type = core.find_parameter( - schema, - 'value') + elif '_type' in descendant: + if '_inherit' in descendant: + for inherit in descendant['_inherit']: - return core.deserialize(value_type, encoded) + if self.equivalent(inherit, ancestor) or self.inherits_from(inherit, ancestor): + return True + return False -# TODO: deal with all the different unit core -def apply_units(schema, current, update, core): - return current + update + elif '_type_parameters' in descendant: + for type_parameter in descendant['_type_parameters']: + parameter_key = f'_{type_parameter}' + if parameter_key in ancestor and parameter_key in descendant: + if not self.inherits_from(descendant[parameter_key], ancestor[parameter_key]): + return False + if '_type' not in ancestor or descendant['_type'] != ancestor['_type']: + return False -def check_units(schema, state, core): - # TODO: expand this to check the actual units for compatibility - return isinstance(state, Quantity) + else: + for key, value in ancestor.items(): + if key not in type_schema_keys: + # if not key.startswith('_'): + if key in descendant: + if not self.inherits_from(descendant[key], value): + return False + else: + return False + return True -def serialize_units(schema, value, core): - return str(value) + # def infer_wires(self, ports, state, wires, top_schema=None, top_state=None, path=None, internal_path=None): + def infer_wires(self, ports, wires, top_schema=None, top_state=None, path=None, internal_path=None): + top_schema = top_schema or {} + top_state = top_state or state + path = path or () + internal_path = internal_path or () -def deserialize_units(schema, encoded, core): - if isinstance(encoded, Quantity): - return encoded - else: - return units(encoded) + if isinstance(ports, str): + ports = self.access(ports) + if isinstance(wires, (list, tuple)): + if len(wires) == 0: + destination_schema, destination_state = top_schema, top_state -def apply_path(schema, current, update, core): - # paths replace previous paths - return update + else: + destination_schema, destination_state = self.slice( + top_schema, + top_state, + path[:-1] + wires) + merged_schema = apply_schema( + 'schema', + destination_schema, + ports, + self) -def generate_map(core, schema, state, top_schema=None, top_state=None, path=None): - schema = schema or {} - state = state or core.default(schema) - top_schema = top_schema or schema - top_state = top_state or state - path = path or [] + merged_state = self.complete( + merged_schema, + destination_state) - value_type = core.find_parameter( - schema, - 'value') + else: + for port_key, port_wires in wires.items(): + subschema, substate = self.slice( + ports, + {}, + port_key) - # generated_schema = {} - # generated_state = {} + if isinstance(port_wires, dict): + top_schema, top_state = self.infer_wires( + subschema, + # substate, + port_wires, + top_schema=top_schema, + top_state=top_state, + path=path, + internal_path=internal_path+(port_key,)) - # TODO: can we assume this was already sorted at the top level? + # port_wires must be a list + elif len(port_wires) == 0: + raise Exception(f'no wires at port "{port_key}" in ports {ports} with state {state}') - generated_schema, generated_state = core.sort( - schema, - state) + else: + compound_path = resolve_path( + path[:-1] + tuple(port_wires)) - all_keys = union_keys(schema, state) # set(schema.keys()).union(state.keys()) + compound_schema, compound_state = self.set_slice( + {}, {}, + compound_path, + subschema or 'any', + self.default(subschema)) - for key in all_keys: - if is_schema_key(key): - generated_schema[key] = state.get( - key, - schema.get(key)) + top_schema = self.resolve( + top_schema, + compound_schema) - else: - subschema = schema.get(key, value_type) - substate = state.get(key) + top_state = self.merge_recur( + top_schema, + compound_state, + top_state) - subschema = core.merge_schemas( - value_type, - subschema) + return top_schema, top_state - subschema, generated_state[key], top_schema, top_state = core.generate_recur( - subschema, - substate, - top_schema=top_schema, - top_state=top_state, - path=path+[key]) - return generated_schema, generated_state, top_schema, top_state + def infer_edge(self, schema, state, top_schema=None, top_state=None, path=None): + ''' + given the schema and state for this edge, and its path relative to + the top_schema and top_state, make all the necessary completions to + both the schema and the state according to the input and output schemas + of this edge in '_inputs' and '_outputs', along the wires in its state + under 'inputs' and 'outputs'. + returns the top_schema and top_state, even if the edge is deeply embedded, + as the particular wires could have implications anywhere in the tree. + ''' -def generate_tree(core, schema, state, top_schema=None, top_state=None, path=None): - schema = schema or {} - state = state or core.default(schema) - top_schema = top_schema or schema - top_state = top_state or state - path = path or [] + schema = schema or {} + top_schema = top_schema or schema + top_state = top_state or state + path = path or () - leaf_type = core.find_parameter( - schema, - 'leaf') + if self.check('edge', state): + for port_key in ['inputs', 'outputs']: + ports = state.get(port_key) + schema_key = f'_{port_key}' + port_schema = schema.get(schema_key, {}) + state_schema = state.get(schema_key, {}) - leaf_is_any = leaf_type == 'any' or (isinstance(leaf_type, dict) and leaf_type.get('_type') == 'any') + schema[schema_key] = self.resolve( + port_schema, + self.access( + state_schema)) - if not leaf_is_any and core.check(leaf_type, state): - generate_schema, generate_state, top_schema, top_state = core.generate_recur( - leaf_type, - state, - top_schema=top_schema, - top_state=top_state, - path=path) + if ports: + top_schema, top_state = self.infer_wires( + schema[schema_key], + # state, + ports, + top_schema=top_schema, + top_state=top_state, + path=path) - elif isinstance(state, dict): - generate_schema = {} - generate_state = {} + return top_schema, top_state - all_keys = union_keys(schema, state) # set(schema.keys()).union(state.keys()) - non_schema_keys = [ - key - for key in all_keys - if not is_schema_key(key)] - if non_schema_keys: - base_schema = { - key: subschema - for key, subschema in schema.items() - if is_schema_key(key)} - else: - base_schema = schema + def infer_schema(self, schema, state, top_schema=None, top_state=None, path=None): + """ + Given a schema fragment and an existing state with _type keys, + return the full schema required to describe that state, + and whatever state was hydrated (edges) during this process - for key in all_keys: - if not is_schema_key(key): - subschema = schema.get(key) - substate = state.get(key) + """ - if not substate or core.check(leaf_type, substate): - base_schema = leaf_type + # during recursive call, schema is kept at the top level and the + # path is used to access it (!) - subschema = core.merge_schemas( - base_schema, - subschema) + schema = schema or {} + top_schema = top_schema or schema + top_state = top_state or state + path = path or () - subschema, generate_state[key], top_schema, top_state = core.generate_recur( - subschema, - substate, - top_schema=top_schema, - top_state=top_state, - path=path+[key]) + if isinstance(state, dict): + state_schema = None + if '_type' in state: + state_type = { + key: value + for key, value in state.items() + if is_schema_key(key)} - elif key in state: - generate_schema[key] = state[key] - elif key in schema: - generate_schema[key] = schema[key] - else: - raise Exception(' the impossible has occurred now is the time for celebration') - else: - generate_schema = schema - generate_state = state + schema = self.resolve( + schema, + state_type) - return generate_schema, generate_state, top_schema, top_state + if '_type' in schema: + hydrated_state = self.deserialize( + schema, + state) + top_schema, top_state = self.set_slice( + top_schema, + top_state, + path, + schema, + hydrated_state) -def generate_ports(core, schema, wires, top_schema=None, top_state=None, path=None): - schema = schema or {} - wires = wires or {} - top_schema = top_schema or schema - top_state = top_state or {} - path = path or [] + top_schema, top_state = self.infer_edge( + schema, + hydrated_state, + top_schema, + top_state, + path) - if isinstance(schema, str): - schema = {'_type': schema} + else: + for key in state: + inner_path = path + (key,) + inner_schema, inner_state = self.slice( + schema, + state, + key) - for port_key, subwires in wires.items(): - if port_key in schema: - port_schema = schema[port_key] - else: - port_schema, subwires = core.slice( - schema, - wires, - port_key) + top_schema, top_state = self.infer_schema( + inner_schema, + inner_state, + top_schema=top_schema, + top_state=top_state, + path=inner_path) - if isinstance(subwires, dict): - top_schema, top_state = generate_ports( - core, - port_schema, - subwires, - top_schema=top_schema, - top_state=top_state, - path=path) + elif isinstance(state, str): + pass else: - if isinstance(subwires, str): - subwires = [subwires] - - default_state = core.default( - port_schema) + type_schema = TYPE_SCHEMAS.get( + type(state).__name__, + 'any') - top_schema, top_state = core.set_slice( + top_schema, top_state = self.set_slice( top_schema, top_state, - path[:-1] + subwires, - port_schema, - default_state, - defer=True) - - return top_schema, top_state - - -def generate_edge(core, schema, state, top_schema=None, top_state=None, path=None): - schema = schema or {} - state = state or {} - top_schema = top_schema or schema - top_state = top_state or state - path = path or [] - - generated_schema, generated_state, top_schema, top_state = generate_any( - core, - schema, - state, - top_schema=top_schema, - top_state=top_state, - path=path) + path, + type_schema, + state) - deserialized_state = core.deserialize( - generated_schema, - generated_state) + return top_schema, top_state + - merged_schema, merged_state = core.sort( - generated_schema, - deserialized_state) + def hydrate(self, schema, state): + hydrated = self.deserialize(schema, state) + return self.fill(schema, hydrated) - top_schema, top_state = core.set_slice( - top_schema, - top_state, - path, - merged_schema, - merged_state) - for port_key in ['inputs', 'outputs']: - port_schema = merged_schema.get( - f'_{port_key}', {}) - ports = merged_state.get( - port_key, {}) + def complete(self, initial_schema, initial_state): + full_schema = self.access( + initial_schema) - top_schema, top_state = generate_ports( - core, - port_schema, - ports, - top_schema=top_schema, - top_state=top_state, - path=path) + state = self.deserialize( + full_schema, + initial_state) - return merged_schema, merged_state, top_schema, top_state + # fill in the parts of the composition schema + # determined by the state + schema, state = self.infer_schema( + full_schema, + state) + final_state = self.fill(schema, state) -def apply_edge(schema, current, update, core): - result = current.copy() - result['inputs'] = core.apply( - 'wires', - current.get('inputs'), - update.get('inputs')) + # TODO: add flag to types.access(copy=True) + return self.access(schema), final_state + - result['outputs'] = core.apply( - 'wires', - current.get('outputs'), - update.get('outputs')) + def generate_recur(self, schema, state, top_schema=None, top_state=None, path=None): + found = self.retrieve( + schema) - return result + generate_function = self.choose_method( + found, + state, + 'generate') + return generate_function( + self, + found, + state, + top_schema=top_schema, + top_state=top_state, + path=path) -def dataclass_edge(schema, path, core): - inputs = schema.get('_inputs', {}) - inputs_dataclass = core.dataclass( - inputs, - path + ['inputs']) - outputs = schema.get('_outputs', {}) - outputs_dataclass = core.dataclass( - outputs, - path + ['outputs']) + def generate(self, schema, state): + merged_schema, merged_state = self.sort( + schema, + state) - return Callable[[inputs_dataclass], outputs_dataclass] + _, _, top_schema, top_state = self.generate_recur( + merged_schema, + merged_state) + return top_schema, top_state -def check_ports(state, core, key): - return key in state and core.check( - 'wires', - state[key]) + def find_method(self, schema, method_key): + if not isinstance(schema, dict) or method_key not in schema: + schema = self.access(schema) -def check_edge(schema, state, core): - return isinstance(state, dict) and check_ports(state, core, 'inputs') and check_ports(state, core, 'outputs') + if method_key in schema: + registry = self.lookup_registry( + method_key) + if registry is not None: + method_name = schema[method_key] + method = registry.access(method_name) -def serialize_edge(schema, value, core): - return value + return method -def deserialize_edge(schema, encoded, core): - return encoded + def import_types(self, package, strict=False): + for type_key, type_data in package.items(): + if not (strict and self.exists(type_key)): + self.register( + type_key, + type_data) -def array_shape(core, schema): - if '_type_parameters' not in schema: - schema = core.access(schema) - parameters = schema.get('_type_parameters', []) + def define(self, method_name, methods): + method_key = f'_{method_name}' + for type_key, method in methods.items(): + self.register( + type_key, + {method_key: method}) - return tuple([ - int(schema[f'_{parameter}']) - for parameter in schema['_type_parameters']]) + def link_place(self, place, link): + pass -def check_array(schema, state, core): - shape_type = core.find_parameter( - schema, - 'shape') - return isinstance(state, np.ndarray) and state.shape == array_shape(core, shape_type) # and state.dtype == bindings['data'] # TODO align numpy data types so we can validate the types of the arrays + def compose(self, a, b): + pass + def query(self, schema, instance, redex): + subschema = {} + return subschema -def dataclass_array(schema, path, core): - return np.ndarray +class Edge: + def __init__(self): + pass -def slice_array(schema, state, path, core): - if len(path) > 0: - head = path[0] - tail = path[1:] - step = state[head] - if isinstance(step, np.ndarray): - sliceschema = schema.copy() - sliceschema['_shape'] = step.shape - return core.slice( - sliceschema, - step, - tail) - else: - data_type = core.find_parameter( - schema, - 'data') + def inputs(self): + return {} - return core.slice( - data_type, - step, - tail) - else: - return schema, state + def outputs(self): + return {} -def apply_array(schema, current, update, core): - if isinstance(update, dict): - paths = hierarchy_depth(update) - for path, inner_update in paths.items(): - if len(path) > len(schema['_shape']): - raise Exception(f'index is too large for array update: {path}\n {schema}') - else: - index = tuple(path) - current[index] += inner_update + def interface(self): + """Returns the schema for this type""" + return { + 'inputs': self.inputs(), + 'outputs': self.outputs()} - return current +def accumulate(schema, current, update, core): + if current is None: + return update + if update is None: + return current else: return current + update -def serialize_array(schema, value, core): - """ Serialize numpy array to list """ +def set_apply(schema, current, update, core): + if isinstance(current, dict) and isinstance(update, dict): + for key, value in update.items(): + # TODO: replace this with type specific functions (??) + if key in schema: + subschema = schema[key] + elif '_leaf' in schema: + if core.check(schema['_leaf'], value): + subschema = schema['_leaf'] + else: + subschema = schema + elif '_value' in schema: + subschema = schema['_value'] - if isinstance(value, dict): - return value - elif isinstance(value, str): - import ipdb; ipdb.set_trace() - else: - array_data = 'string' - dtype = value.dtype.name - if dtype.startswith('int'): - array_data = 'integer' - elif dtype.startswith('float'): - array_data = 'float' + current[key] = set_apply( + subschema, + current.get(key), + value, + core) - return { - 'list': value.tolist(), - 'data': array_data, - 'shape': list(value.shape)} + return current + else: + return update -DTYPE_MAP = { - 'float': 'float64', - 'integer': 'int64', - 'string': 'str'} +def concatenate(schema, current, update, core=None): + return current + update -def lookup_dtype(data_name): - data_name = data_name or 'string' - dtype_name = DTYPE_MAP.get(data_name) - if dtype_name is None: - raise Exception(f'unknown data type for array: {data_name}') +def replace(schema, current, update, core=None): + return update - return np.dtype(dtype_name) +def to_string(schema, value, core=None): + return str(value) -def read_datatype(data_schema): - return lookup_dtype( - data_schema['_type']) +def evaluate(schema, encoded, core=None): + return eval(encoded) -def read_shape(shape): - return tuple([ - int(x) - for x in tuple_from_type( - shape)]) +def resolve_map(schema, update, core): + if isinstance(update, dict): + value_schema = update.get( + '_value', + schema.get('_value', {})) -def deserialize_array(schema, encoded, core): - if isinstance(encoded, np.ndarray): - return encoded + for key, subschema in update.items(): + if not is_schema_key(key): + value_schema = core.resolve_schemas( + value_schema, + subschema) - elif isinstance(encoded, dict): - if 'value' in encoded: - return encoded['value'] - else: - found = core.retrieve( - encoded.get( - 'data', - schema['_data'])) + schema['_type'] = update.get( + '_type', + schema.get('_type', 'map')) + schema['_value'] = value_schema - dtype = read_datatype( - found) + return schema - shape = read_shape( - schema['_shape']) - if 'list' in encoded: - return np.array( - encoded['list'], - dtype=dtype).reshape( - shape) - else: - return np.zeros( - tuple(shape), - dtype=dtype) +def resolve_array(schema, update, core): + if not '_shape' in schema: + schema = core.access(schema) + if not '_shape' in schema: + raise Exception(f'array must have a "_shape" key, not {schema}') + data_schema = schema.get('_data', {}) -def default_tree(schema, core): - leaf_schema = core.find_parameter( - schema, - 'leaf') + if '_type' in update: + data_schema = core.resolve_schemas( + data_schema, + update.get('_data', {})) - default = {} + if update['_type'] == 'array': + if '_shape' in update: + if update['_shape'] != schema['_shape']: + raise Exception(f'arrays must be of the same shape, not \n {schema}\nand\n {update}') - non_schema_keys = [ - key - for key in schema - if not is_schema_key(key)] + elif core.inherits_from(update, schema): + schema.update(update) - if non_schema_keys: - base_schema = { - key: subschema - for key, subschema in schema.items() - if is_schema_key(key)} + elif not core.inherits_from(schema, update): + raise Exception(f'cannot resolve incompatible array schemas:\n {schema}\n {update}') - for key in non_schema_keys: - subschema = core.merge_schemas( - base_schema, - schema[key]) + else: + for key, subschema in update.items(): + if isinstance(key, int): + key = (key,) - subdefault = core.default( - subschema) + if len(key) > len(schema['_shape']): + raise Exception(f'key is longer than array dimension: {key}\n{schema}\n{update}') + elif len(key) == len(schema['_shape']): + data_schema = core.resolve_schemas( + data_schema, + subschema) + else: + shape = tuple_from_type( + schema['_shape']) - if subdefault: - default[key] = subdefault + subshape = shape[len(key):] + inner_schema = schema.copy() + inner_schema['_shape'] = subshape + inner_schema = core.resolve_schemas( + inner_schema, + subschema) - return default + data_schema = inner_schema['_data'] + schema['_data'] = data_schema -def default_array(schema, core): - data_schema = core.find_parameter( - schema, - 'data') + return schema - dtype = read_datatype( - data_schema) - shape = read_shape( - schema['_shape']) +def tuple_from_type(tuple_type): + if isinstance(tuple_type, tuple): + return tuple_type - return np.zeros( - shape, - dtype=dtype) + elif isinstance(tuple_type, list): + return tuple(tuple_type) + + elif isinstance(tuple_type, dict): + tuple_list = [ + tuple_type[f'_{parameter}'] + for parameter in tuple_type['_type_parameters']] + return tuple(tuple_list) -def fold_list(schema, state, method, values, core): - element_type = core.find_parameter( - schema, - 'element') - - if core.check(element_type, state): - result = core.fold( - element_type, - state, - method, - values) + else: + raise Exception(f'do not recognize this type as a tuple: {tuple_type}') - elif isinstance(state, list): - subresult = [ - fold_list( - schema, - element, - method, - values, - core) - for element in state] - result = visit_method( - schema, - subresult, - method, - values, - core) +# def resolve_tree(schema, update, core): +# import ipdb; ipdb.set_trace() - else: - raise Exception(f'state does not seem to be a list or an eelement:\n state: {state}\n schema: {schema}') +# if isinstance(update, dict): +# leaf_schema = schema.get('_leaf', {}) - return result +# if '_type' in update: +# if update['_type'] == 'map': +# value_schema = update.get('_value', {}) +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# value_schema) +# elif update['_type'] == 'tree': +# for key, subschema in update.items(): +# if not key.startswith('_'): +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# subschema) +# else: +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# update) -def fold_tree(schema, state, method, values, core): - leaf_type = core.find_parameter( - schema, - 'leaf') - - if core.check(leaf_type, state): - result = core.fold( - leaf_type, - state, - method, - values) +# schema['_leaf'] = leaf_schema +# else: +# for key, subupdate in - elif isinstance(state, dict): - subresult = {} +# return schema - for key, branch in state.items(): - if key.startswith('_'): - subresult[key] = branch - else: - subresult[key] = fold_tree( - schema[key] if key in schema else schema, - branch, - method, - values, - core) - result = visit_method( - schema, - subresult, - method, - values, - core) - else: - raise Exception(f'state does not seem to be a tree or a leaf:\n state: {state}\n schema: {schema}') - return result -def fold_map(schema, state, method, values, core): - value_type = core.find_parameter( - schema, - 'value') - - subresult = {} +def check_units(schema, state, core): + # TODO: expand this to check the actual units for compatibility + return isinstance(state, Quantity) - for key, value in state.items(): - subresult[key] = core.fold( - value_type, - value, - method, - values) - - result = visit_method( - schema, - subresult, - method, - values, - core) - return result -def fold_maybe(schema, state, method, values, core): - value_type = core.find_parameter( - schema, - 'value') - if state is None: - result = core.fold( - 'any', - state, - method, - values) - else: - result = core.fold( - value_type, - state, - method, - values) +def deserialize_edge(schema, encoded, core): + return encoded - return result +def array_shape(core, schema): + if '_type_parameters' not in schema: + schema = core.access(schema) + parameters = schema.get('_type_parameters', []) -def divide_list(schema, state, values, core): - element_type = core.find_parameter( - schema, - 'element') + return tuple([ + int(schema[f'_{parameter}']) + for parameter in schema['_type_parameters']]) - if core.check(element_type, state): - return core.fold( - element_type, - state, - 'divide', - values) - elif isinstance(state, list): - divisions = values.get('divisions', 2) - result = [[] for _ in range(divisions)] +def apply_array(schema, current, update, core): + if isinstance(update, dict): + paths = hierarchy_depth(update) + for path, inner_update in paths.items(): + if len(path) > len(schema['_shape']): + raise Exception(f'index is too large for array update: {path}\n {schema}') + else: + index = tuple(path) + current[index] += inner_update - for elements in state: - for index in range(divisions): - result[index].append( - elements[index]) + return current + + else: + return current + update - return result +def serialize_array(schema, value, core): + """ Serialize numpy array to list """ + + if isinstance(value, dict): + return value + elif isinstance(value, str): + import ipdb; ipdb.set_trace() else: - raise Exception(f'trying to divide list but state does not resemble a list or an element.\n state: {pf(state)}\n schema: {pf(schema)}') + array_data = 'string' + dtype = value.dtype.name + if dtype.startswith('int'): + array_data = 'integer' + elif dtype.startswith('float'): + array_data = 'float' + return { + 'list': value.tolist(), + 'data': array_data, + 'shape': list(value.shape)} -def divide_tree(schema, state, values, core): - leaf_type = core.find_parameter( - schema, - 'leaf') - - if core.check(leaf_type, state): - return core.fold( - leaf_type, - state, - 'divide', - values) - elif isinstance(state, dict): - divisions = values.get('divisions', 2) - division = [{} for _ in range(divisions)] +DTYPE_MAP = { + 'float': 'float64', + 'integer': 'int64', + 'string': 'str'} - for key, value in state.items(): - for index in range(divisions): - division[index][key] = value[index] - return division +def lookup_dtype(data_name): + data_name = data_name or 'string' + dtype_name = DTYPE_MAP.get(data_name) + if dtype_name is None: + raise Exception(f'unknown data type for array: {data_name}') - else: - raise Exception(f'trying to divide tree but state does not resemble a leaf or a tree.\n state: {pf(state)}\n schema: {pf(schema)}') + return np.dtype(dtype_name) -def divide_map(schema, state, values, core): - if isinstance(state, dict): - divisions = values.get('divisions', 2) - division = [{} for _ in range(divisions)] - for key, value in state.items(): - for index in range(divisions): - division[index][key] = value[index] +def read_datatype(data_schema): + return lookup_dtype( + data_schema['_type']) + + +def read_shape(shape): + return tuple([ + int(x) + for x in tuple_from_type( + shape)]) - return division - else: - raise Exception(f'trying to divide a map but state is not a dict.\n state: {pf(state)}\n schema: {pf(schema)}') def register_units(core, units): @@ -4408,16 +4467,6 @@ def register_units(core, units): return core - - -def dataclass_boolean(schema, path, core): - return bool - - -def dataclass_string(schema, path, core): - return str - - def apply_enum(schema, current, update, core): parameters = core.parameters_for(schema) if update in parameters: @@ -4434,54 +4483,9 @@ def check_enum(schema, state, core): return state in parameters -def slice_string(schema, state, path, core): - raise Exception(f'cannot slice into an string: {path}\n{state}\n{schema}') - - -def serialize_enum(schema, value, core): - return value - - -def deserialize_enum(schema, state, core): - return value - - -def bind_enum(schema, state, key, subschema, substate, core): - new_schema = schema.copy() - new_schema[f'_{key}'] = subschema - open = list(state) - open[key] = substate - - return new_schema, tuple(open) - -def fold_enum(schema, state, method, values, core): - if not isinstance(state, (tuple, list)): - return visit_method( - schema, - state, - method, - values, - core) - else: - parameters = core.parameters_for(schema) - result = [] - for parameter, element in zip(parameters, state): - fold = core.fold( - parameter, - element, - method, - values) - result.append(fold) - result = tuple(result) - return visit_method( - schema, - result, - method, - values, - core) def dataclass_enum(schema, path, core): @@ -4786,38 +4790,6 @@ def replace_reaction(schema, state, reaction, core): 'reactum': reactum} -def divide_reaction(schema, state, reaction, core): - mother = reaction['mother'] - daughters = reaction['daughters'] - - mother_schema, mother_state = core.slice( - schema, - state, - mother) - - division = core.fold( - mother_schema, - mother_state, - 'divide', { - 'divisions': len(daughters), - 'daughter_configs': [daughter[1] for daughter in daughters]}) - - after = { - daughter[0]: daughter_state - for daughter, daughter_state in zip(daughters, division)} - - replace = { - 'before': { - mother: {}}, - 'after': after} - - return replace_reaction( - schema, - state, - replace, - core) - - def register_base_reactions(core): core.register_reaction('add', add_reaction) core.register_reaction('remove', remove_reaction) From 6ac9dc45cffd84bcf872bdca6f178c0eb8aa0fd8 Mon Sep 17 00:00:00 2001 From: Eran Date: Fri, 6 Dec 2024 20:55:58 -0500 Subject: [PATCH 4/8] continue with type system reorganization --- bigraph_schema/type_system.py | 595 ++++++++++++++++------------------ 1 file changed, 273 insertions(+), 322 deletions(-) diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index 04ca628..141350c 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -170,25 +170,6 @@ def remove_path(tree, path): del upon[path[-1]] return tree - -def resolve_path(path): - """ - Given a path that includes '..' steps, resolve the path to a canonical form - """ - resolve = [] - - for step in path: - if step == '..': - if len(resolve) == 0: - raise Exception(f'cannot go above the top in path: "{path}"') - else: - resolve = resolve[:-1] - else: - resolve.append(step) - - return tuple(resolve) - - def visit_method(schema, state, method, values, core): """ Visit a method for a schema and state and apply it, returning the result @@ -220,12 +201,6 @@ def visit_method(schema, state, method, values, core): return result - -def generate_quote(core, schema, state, top_schema=None, top_state=None, path=None): - return schema, state, top_schema, top_state - - - def type_parameters_for(schema): parameters = [] for key in schema['_type_parameters']: @@ -234,6 +209,7 @@ def type_parameters_for(schema): return parameters + # Apply functions # --------------- @@ -364,7 +340,6 @@ def apply_list(schema, current, update, core): # element_type, # current_element, # update_element) - # result.append(applied) return result @@ -449,6 +424,28 @@ def apply_edge(schema, current, update, core): def apply_units(schema, current, update, core): return current + update +def apply_enum(schema, current, update, core): + parameters = core.parameters_for(schema) + if update in parameters: + return update + else: + raise Exception(f'{update} is not in the enum, options are: {parameters}') + +def apply_array(schema, current, update, core): + if isinstance(update, dict): + paths = hierarchy_depth(update) + for path, inner_update in paths.items(): + if len(path) > len(schema['_shape']): + raise Exception(f'index is too large for array update: {path}\n {schema}') + else: + index = tuple(path) + current[index] += inner_update + + return current + + else: + return current + update + # Sort functions # -------------- @@ -583,6 +580,50 @@ def resolve_any(schema, update, core): return outcome +# def resolve_tree(schema, update, core): +# if isinstance(update, dict): +# leaf_schema = schema.get('_leaf', {}) + +# if '_type' in update: +# if update['_type'] == 'map': +# value_schema = update.get('_value', {}) +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# value_schema) + +# elif update['_type'] == 'tree': +# for key, subschema in update.items(): +# if not key.startswith('_'): +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# subschema) +# else: +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# update) + +# schema['_leaf'] = leaf_schema +# else: +# for key, subupdate in + +# return schema + +def resolve_path(path): + """ + Given a path that includes '..' steps, resolve the path to a canonical form + """ + resolve = [] + + for step in path: + if step == '..': + if len(resolve) == 0: + raise Exception(f'cannot go above the top in path: "{path}"') + else: + resolve = resolve[:-1] + else: + resolve.append(step) + + return tuple(resolve) # Divide functions # ---------------- @@ -748,6 +789,13 @@ def divide_map(schema, state, values, core): raise Exception( f'trying to divide a map but state is not a dict.\n state: {pf(state)}\n schema: {pf(schema)}') +def divide_enum(schema, state, values, core): + divisions = values.get('divisions', 2) + + return [ + tuple([item[index] for item in state]) + for index in range(divisions)] + # Default functions # ----------------- @@ -822,6 +870,19 @@ def default_array(schema, core): shape, dtype=dtype) +def default_enum(schema, core): + parameter = schema['_type_parameters'][0] + return schema[f'_{parameter}'] + + +def default_edge(schema, core): + edge = {} + for key in schema: + if not is_schema_key(key): + edge[key] = core.default( + schema[key]) + + return edge # Slice functions # --------------- @@ -1190,6 +1251,82 @@ def bind_enum(schema, state, key, subschema, substate, core): return new_schema, tuple(open) +# Resolve functions +# ---------------- + +def resolve_map(schema, update, core): + if isinstance(update, dict): + value_schema = update.get( + '_value', + schema.get('_value', {})) + + for key, subschema in update.items(): + if not is_schema_key(key): + value_schema = core.resolve_schemas( + value_schema, + subschema) + + schema['_type'] = update.get( + '_type', + schema.get('_type', 'map')) + schema['_value'] = value_schema + + return schema + + +def resolve_array(schema, update, core): + if not '_shape' in schema: + schema = core.access(schema) + if not '_shape' in schema: + raise Exception(f'array must have a "_shape" key, not {schema}') + + data_schema = schema.get('_data', {}) + + if '_type' in update: + data_schema = core.resolve_schemas( + data_schema, + update.get('_data', {})) + + if update['_type'] == 'array': + if '_shape' in update: + if update['_shape'] != schema['_shape']: + raise Exception(f'arrays must be of the same shape, not \n {schema}\nand\n {update}') + + elif core.inherits_from(update, schema): + schema.update(update) + + elif not core.inherits_from(schema, update): + raise Exception(f'cannot resolve incompatible array schemas:\n {schema}\n {update}') + + else: + for key, subschema in update.items(): + if isinstance(key, int): + key = (key,) + + if len(key) > len(schema['_shape']): + raise Exception(f'key is longer than array dimension: {key}\n{schema}\n{update}') + elif len(key) == len(schema['_shape']): + data_schema = core.resolve_schemas( + data_schema, + subschema) + else: + shape = tuple_from_type( + schema['_shape']) + + subshape = shape[len(key):] + inner_schema = schema.copy() + inner_schema['_shape'] = subshape + inner_schema = core.resolve_schemas( + inner_schema, + subschema) + + data_schema = inner_schema['_data'] + + schema['_data'] = data_schema + + return schema + + # Check functions # --------------- @@ -1326,8 +1463,16 @@ def check_array(schema, state, core): return isinstance(state, np.ndarray) and state.shape == array_shape(core, shape_type) # and state.dtype == bindings['data'] # TODO align numpy data types so we can validate the types of the arrays -def dataclass_array(schema, path, core): - return np.ndarray +def check_enum(schema, state, core): + if not isinstance(state, str): + return False + + parameters = core.parameters_for(schema) + return state in parameters + +def check_units(schema, state, core): + # TODO: expand this to check the actual units for compatibility + return isinstance(state, Quantity) # Serialize functions @@ -1444,6 +1589,28 @@ def serialize_edge(schema, value, core): def serialize_enum(schema, value, core): return value +def serialize_schema(schema, state, core): + return state + +def serialize_array(schema, value, core): + """ Serialize numpy array to list """ + + if isinstance(value, dict): + return value + elif isinstance(value, str): + import ipdb; ipdb.set_trace() + else: + array_data = 'string' + dtype = value.dtype.name + if dtype.startswith('int'): + array_data = 'integer' + elif dtype.startswith('float'): + array_data = 'float' + + return { + 'list': value.tolist(), + 'data': array_data, + 'shape': list(value.shape)} # Deserialize functions # --------------------- @@ -1638,6 +1805,12 @@ def deserialize_array(schema, encoded, core): tuple(shape), dtype=dtype) +def deserialize_edge(schema, encoded, core): + return encoded + +def deserialize_schema(schema, state, core): + return state + # Dataclass functions # ------------------- @@ -1793,10 +1966,30 @@ def dataclass_boolean(schema, path, core): def dataclass_string(schema, path, core): return str +def dataclass_enum(schema, path, core): + parameters = type_parameters_for(schema) + subtypes = [] + + for index, key in enumerate(schema['type_parameters']): + subschema = schema.get(key, 'any') + subtype = core.dataclass( + subschema, + path + [index]) + + subtypes.append(subtype) + + parameter_block = ', '.join(subtypes) + return eval(f'tuple[{parameter_block}]') + +def dataclass_array(schema, path, core): + return np.ndarray # Generate functions # ------------------ +def generate_quote(core, schema, state, top_schema=None, top_state=None, path=None): + return schema, state, top_schema, top_state + def generate_map(core, schema, state, top_schema=None, top_state=None, path=None): schema = schema or {} state = state or core.default(schema) @@ -1842,7 +2035,6 @@ def generate_map(core, schema, state, top_schema=None, top_state=None, path=None return generated_schema, generated_state, top_schema, top_state - def generate_tree(core, schema, state, top_schema=None, top_state=None, path=None): schema = schema or {} state = state or core.default(schema) @@ -1913,7 +2105,6 @@ def generate_tree(core, schema, state, top_schema=None, top_state=None, path=Non return generate_schema, generate_state, top_schema, top_state - def generate_ports(core, schema, wires, top_schema=None, top_state=None, path=None): schema = schema or {} wires = wires or {} @@ -1959,7 +2150,6 @@ def generate_ports(core, schema, wires, top_schema=None, top_state=None, path=No return top_schema, top_state - def generate_edge(core, schema, state, top_schema=None, top_state=None, path=None): schema = schema or {} state = state or {} @@ -2006,42 +2196,6 @@ def generate_edge(core, schema, state, top_schema=None, top_state=None, path=Non return merged_schema, merged_state, top_schema, top_state - - - -def is_empty(value): - if isinstance(value, np.ndarray): - return False - elif value is None or value == {}: - return True - else: - return False - - - - -def find_union_type(core, schema, state): - parameters = core.parameters_for(schema) - - for possible in parameters: - if core.check(possible, state): - return core.access(possible) - - return None - - -def union_keys(schema, state): - keys = {} - for key in schema: - keys[key] = True - for key in state: - keys[key] = True - - return keys - - # return set(schema.keys()).union(state.keys()) - - def generate_any(core, schema, state, top_schema=None, top_state=None, path=None): schema = schema or {} if is_empty(state): @@ -2079,29 +2233,62 @@ def generate_any(core, schema, state, top_schema=None, top_state=None, path=None top_state=top_state, path=path+[key]) - generated_schema[key] = core.resolve_schemas( - schema.get(key, {}), - subschema) + generated_schema[key] = core.resolve_schemas( + schema.get(key, {}), + subschema) + + generated_state[key] = substate + + if path: + top_schema, top_state = core.set_slice( + top_schema, + top_state, + path, + generated_schema, + generated_state) + else: + top_state = core.merge_recur( + top_schema, + top_state, + generated_state) + + else: + generated_schema, generated_state = schema, state + + return generated_schema, generated_state, top_schema, top_state + + +def is_empty(value): + if isinstance(value, np.ndarray): + return False + elif value is None or value == {}: + return True + else: + return False + + + + +def find_union_type(core, schema, state): + parameters = core.parameters_for(schema) + + for possible in parameters: + if core.check(possible, state): + return core.access(possible) + + return None - generated_state[key] = substate - if path: - top_schema, top_state = core.set_slice( - top_schema, - top_state, - path, - generated_schema, - generated_state) - else: - top_state = core.merge_recur( - top_schema, - top_state, - generated_state) +def union_keys(schema, state): + keys = {} + for key in schema: + keys[key] = True + for key in state: + keys[key] = True - else: - generated_schema, generated_state = schema, state + return keys - return generated_schema, generated_state, top_schema, top_state + # return set(schema.keys()).union(state.keys()) def is_method_key(key, parameters): @@ -4226,85 +4413,9 @@ def replace(schema, current, update, core=None): def to_string(schema, value, core=None): return str(value) - - def evaluate(schema, encoded, core=None): return eval(encoded) - -def resolve_map(schema, update, core): - if isinstance(update, dict): - value_schema = update.get( - '_value', - schema.get('_value', {})) - - for key, subschema in update.items(): - if not is_schema_key(key): - value_schema = core.resolve_schemas( - value_schema, - subschema) - - schema['_type'] = update.get( - '_type', - schema.get('_type', 'map')) - schema['_value'] = value_schema - - return schema - - -def resolve_array(schema, update, core): - if not '_shape' in schema: - schema = core.access(schema) - if not '_shape' in schema: - raise Exception(f'array must have a "_shape" key, not {schema}') - - data_schema = schema.get('_data', {}) - - if '_type' in update: - data_schema = core.resolve_schemas( - data_schema, - update.get('_data', {})) - - if update['_type'] == 'array': - if '_shape' in update: - if update['_shape'] != schema['_shape']: - raise Exception(f'arrays must be of the same shape, not \n {schema}\nand\n {update}') - - elif core.inherits_from(update, schema): - schema.update(update) - - elif not core.inherits_from(schema, update): - raise Exception(f'cannot resolve incompatible array schemas:\n {schema}\n {update}') - - else: - for key, subschema in update.items(): - if isinstance(key, int): - key = (key,) - - if len(key) > len(schema['_shape']): - raise Exception(f'key is longer than array dimension: {key}\n{schema}\n{update}') - elif len(key) == len(schema['_shape']): - data_schema = core.resolve_schemas( - data_schema, - subschema) - else: - shape = tuple_from_type( - schema['_shape']) - - subshape = shape[len(key):] - inner_schema = schema.copy() - inner_schema['_shape'] = subshape - inner_schema = core.resolve_schemas( - inner_schema, - subschema) - - data_schema = inner_schema['_data'] - - schema['_data'] = data_schema - - return schema - - def tuple_from_type(tuple_type): if isinstance(tuple_type, tuple): return tuple_type @@ -4323,54 +4434,6 @@ def tuple_from_type(tuple_type): raise Exception(f'do not recognize this type as a tuple: {tuple_type}') -# def resolve_tree(schema, update, core): -# import ipdb; ipdb.set_trace() - -# if isinstance(update, dict): -# leaf_schema = schema.get('_leaf', {}) - -# if '_type' in update: -# if update['_type'] == 'map': -# value_schema = update.get('_value', {}) -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# value_schema) - -# elif update['_type'] == 'tree': -# for key, subschema in update.items(): -# if not key.startswith('_'): -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# subschema) -# else: -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# update) - -# schema['_leaf'] = leaf_schema -# else: -# for key, subupdate in - -# return schema - - - - - - -def check_units(schema, state, core): - # TODO: expand this to check the actual units for compatibility - return isinstance(state, Quantity) - - - - - - -def deserialize_edge(schema, encoded, core): - return encoded - - def array_shape(core, schema): if '_type_parameters' not in schema: schema = core.access(schema) @@ -4381,42 +4444,6 @@ def array_shape(core, schema): for parameter in schema['_type_parameters']]) -def apply_array(schema, current, update, core): - if isinstance(update, dict): - paths = hierarchy_depth(update) - for path, inner_update in paths.items(): - if len(path) > len(schema['_shape']): - raise Exception(f'index is too large for array update: {path}\n {schema}') - else: - index = tuple(path) - current[index] += inner_update - - return current - - else: - return current + update - - -def serialize_array(schema, value, core): - """ Serialize numpy array to list """ - - if isinstance(value, dict): - return value - elif isinstance(value, str): - import ipdb; ipdb.set_trace() - else: - array_data = 'string' - dtype = value.dtype.name - if dtype.startswith('int'): - array_data = 'integer' - elif dtype.startswith('float'): - array_data = 'float' - - return { - 'list': value.tolist(), - 'data': array_data, - 'shape': list(value.shape)} - DTYPE_MAP = { 'float': 'float64', @@ -4467,82 +4494,6 @@ def register_units(core, units): return core -def apply_enum(schema, current, update, core): - parameters = core.parameters_for(schema) - if update in parameters: - return update - else: - raise Exception(f'{update} is not in the enum, options are: {parameters}') - - -def check_enum(schema, state, core): - if not isinstance(state, str): - return False - - parameters = core.parameters_for(schema) - return state in parameters - - - - - - - -def dataclass_enum(schema, path, core): - parameters = type_parameters_for(schema) - subtypes = [] - - for index, key in enumerate(schema['type_parameters']): - subschema = schema.get(key, 'any') - subtype = core.dataclass( - subschema, - path + [index]) - - subtypes.append(subtype) - - parameter_block = ', '.join(subtypes) - return eval(f'tuple[{parameter_block}]') - - -def divide_enum(schema, state, values, core): - divisions = values.get('divisions', 2) - - return [ - tuple([item[index] for item in state]) - for index in range(divisions)] - - -# def merge_edge(schema, current_state, new_state, core): -# merge = deep_merge( -# current_state, -# new_state) - -# return core.deserialize( -# schema, -# merge) - - -def serialize_schema(schema, state, core): - return state - - -def deserialize_schema(schema, state, core): - return state - - -def default_enum(schema, core): - parameter = schema['_type_parameters'][0] - return schema[f'_{parameter}'] - - -def default_edge(schema, core): - edge = {} - for key in schema: - if not is_schema_key(key): - edge[key] = core.default( - schema[key]) - - return edge base_type_library = { From d562380a4fb51ea6b0003814996953c204630d83 Mon Sep 17 00:00:00 2001 From: Eran Date: Fri, 6 Dec 2024 21:10:46 -0500 Subject: [PATCH 5/8] move tests to their own file --- bigraph_schema/registry.py | 16 - bigraph_schema/test.py | 2321 +++++++++++++++++++++++++++++++++ bigraph_schema/type_system.py | 2319 +------------------------------- 3 files changed, 2326 insertions(+), 2330 deletions(-) create mode 100644 bigraph_schema/test.py diff --git a/bigraph_schema/registry.py b/bigraph_schema/registry.py index 99075b5..ee87f7b 100644 --- a/bigraph_schema/registry.py +++ b/bigraph_schema/registry.py @@ -371,19 +371,3 @@ def list(self): def validate(self, item): return True - - -def test_remove_omitted(): - result = remove_omitted( - {'a': {}, 'b': {'c': {}, 'd': {}}}, - {'b': {'c': {}}}, - {'a': {'X': 1111}, 'b': {'c': {'Y': 4444}, 'd': {'Z': 99999}}}) - - assert 'a' not in result - assert result['b']['c']['Y'] == 4444 - assert 'd' not in result['b'] - - -if __name__ == '__main__': - test_reregister_type() - test_remove_omitted() diff --git a/bigraph_schema/test.py b/bigraph_schema/test.py new file mode 100644 index 0000000..1512308 --- /dev/null +++ b/bigraph_schema/test.py @@ -0,0 +1,2321 @@ +import pytest +import pprint +import numpy as np +from dataclasses import asdict + +from bigraph_schema.type_system import ( + TypeSystem, divide_longest, base_type_library, accumulate, to_string, deserialize_integer, apply_schema +) +from bigraph_schema.units import units +from bigraph_schema.registry import establish_path, remove_omitted, NONE_SYMBOL +import bigraph_schema.data as data + + +@pytest.fixture +def core(): + core = TypeSystem() + return register_test_types(core) + + +def register_cube(core): + cube_schema = { + 'shape': { + '_type': 'shape', + '_description': 'abstract shape type'}, + + 'rectangle': { + '_type': 'rectangle', + '_divide': divide_longest, + '_description': 'a two-dimensional value', + '_inherit': 'shape', + 'width': {'_type': 'integer'}, + 'height': {'_type': 'integer'}, + }, + + # cannot override existing keys unless it is of a subtype + 'cube': { + '_type': 'cube', + '_inherit': 'rectangle', + 'depth': {'_type': 'integer'}, + }, + } + + for type_key, type_data in cube_schema.items(): + core.register(type_key, type_data) + + return core + + +def register_test_types(core): + register_cube(core) + + core.register('compartment', { + 'counts': 'tree[float]', + 'inner': 'tree[compartment]'}) + + core.register('metaedge', { + '_inherit': 'edge', + '_inputs': { + 'before': 'metaedge'}, + '_outputs': { + 'after': 'metaedge'}}) + + return core + + +def test_reregister_type(core): + core.register('A', {'_default': 'a'}) + with pytest.raises(Exception) as e: + core.register( + 'A', {'_default': 'b'}, + strict=True) + + core.register('A', {'_default': 'b'}) + + assert core.access('A')['_default'] == 'b' + + +def test_generate_default(core): + int_default = core.default( + {'_type': 'integer'} + ) + + assert int_default == 0 + + cube_default = core.default( + {'_type': 'cube'}) + + assert 'width' in cube_default + assert 'height' in cube_default + assert 'depth' in cube_default + + nested_default = core.default( + {'a': 'integer', + 'b': { + 'c': 'float', + 'd': 'cube'}, + 'e': 'string'}) + + assert nested_default['b']['d']['width'] == 0 + + +def test_apply_update(core): + schema = {'_type': 'cube'} + state = { + 'width': 11, + 'height': 13, + 'depth': 44, + } + + update = { + 'depth': -5 + } + + new_state = core.apply( + schema, + state, + update + ) + + assert new_state['width'] == 11 + assert new_state['depth'] == 39 + + +def print_schema_validation(core, library, should_pass): + for key, declaration in library.items(): + report = core.validate_schema(declaration) + if len(report) == 0: + message = f'valid schema: {key}' + if should_pass: + print(f'PASS: {message}') + pprint.pprint(declaration) + else: + raise Exception(f'FAIL: {message}\n{declaration}\n{report}') + else: + message = f'invalid schema: {key}' + if not should_pass: + print(f'PASS: {message}') + pprint.pprint(declaration) + else: + raise Exception(f'FAIL: {message}\n{declaration}\n{report}') + + +def test_validate_schema(core): + # good schemas + print_schema_validation(core, base_type_library, True) + + good = { + 'not quite int': { + '_default': 0, + '_apply': accumulate, + '_serialize': to_string, + '_deserialize': deserialize_integer, + '_description': '64-bit integer' + }, + 'ports match': { + 'a': { + '_type': 'integer', + '_value': 2 + }, + 'edge1': { + '_type': 'edge[a:integer]', + # '_type': 'edge', + # '_ports': { + # '1': {'_type': 'integer'}, + # }, + } + } + } + + # bad schemas + bad = { + 'empty': None, + 'str?': 'not a schema', + 'branch is weird': { + 'left': {'_type': 'ogre'}, + 'right': {'_default': 1, '_apply': accumulate}, + }, + } + + # test for ports and wires mismatch + + print_schema_validation(core, good, True) + print_schema_validation(core, bad, False) + + +def test_fill_integer(core): + test_schema = { + '_type': 'integer' + } + + full_state = core.fill(test_schema) + direct_state = core.fill('integer') + generated_schema, generated_state = core.generate( + test_schema, None) + + assert generated_schema['_type'] == 'integer' + assert full_state == direct_state == 0 == generated_state + + +def test_fill_cube(core): + test_schema = {'_type': 'cube'} + partial_state = {'height': 5} + + full_state = core.fill( + test_schema, + state=partial_state) + + assert 'width' in full_state + assert 'height' in full_state + assert 'depth' in full_state + assert full_state['height'] == 5 + assert full_state['depth'] == 0 + + +def test_fill_in_missing_nodes(core): + test_schema = { + 'edge 1': { + '_type': 'edge', + '_inputs': { + 'I': 'float'}, + '_outputs': { + 'O': 'float'}}} + + test_state = { + 'edge 1': { + 'inputs': { + 'I': ['a']}, + 'outputs': { + 'O': ['a']}}} + + filled = core.fill( + test_schema, + test_state) + + assert filled == { + 'a': 0.0, + 'edge 1': { + 'inputs': { + 'I': ['a']}, + 'outputs': { + 'O': ['a']}}} + + +def test_overwrite_existing(core): + test_schema = { + 'edge 1': { + '_type': 'edge', + '_inputs': { + 'I': 'float'}, + '_outputs': { + 'O': 'float'}}} + + test_state = { + 'a': 11.111, + 'edge 1': { + 'inputs': { + 'I': ['a']}, + 'outputs': { + 'O': ['a']}}} + + filled = core.fill( + test_schema, + test_state) + + assert filled == { + 'a': 11.111, + 'edge 1': { + 'inputs': { + 'I': ['a']}, + 'outputs': { + 'O': ['a']}}} + + +def test_fill_from_parse(core): + test_schema = { + 'edge 1': 'edge[I:float,O:float]'} + + test_state = { + 'edge 1': { + 'inputs': { + 'I': ['a']}, + 'outputs': { + 'O': ['a']}}} + + filled = core.fill( + test_schema, + test_state) + + assert filled == { + 'a': 0.0, + 'edge 1': { + 'inputs': { + 'I': ['a']}, + 'outputs': { + 'O': ['a']}}} + + +# def test_fill_in_disconnected_port(core): +# test_schema = { +# 'edge1': { +# '_type': 'edge', +# '_ports': { +# '1': {'_type': 'float'}}}} + +# test_state = {} + + +# def test_fill_type_mismatch(core): +# test_schema = { +# 'a': {'_type': 'integer', '_value': 2}, +# 'edge1': { +# '_type': 'edge', +# '_ports': { +# '1': {'_type': 'float'}, +# '2': {'_type': 'float'}}, +# 'wires': { +# '1': ['..', 'a'], +# '2': ['a']}, +# 'a': 5}} + + +# def test_edge_type_mismatch(core): +# test_schema = { +# 'edge1': { +# '_type': 'edge', +# '_ports': { +# '1': {'_type': 'float'}}, +# 'wires': { +# '1': ['..', 'a']}}, +# 'edge2': { +# '_type': 'edge', +# '_ports': { +# '1': {'_type': 'integer'}}, +# 'wires': { +# '1': ['..', 'a']}}} + + +def test_establish_path(core): + tree = {} + destination = establish_path( + tree, + ('some', + 'where', + 'deep', + 'inside', + 'lives', + 'a', + 'tiny', + 'creature', + 'made', + 'of', + 'light')) + + assert tree['some']['where']['deep']['inside']['lives']['a']['tiny']['creature']['made']['of'][ + 'light'] == destination + + +def test_fill_ports(core): + cell_state = { + 'cell1': { + 'nucleus': { + 'transcription': { + '_type': 'edge', + 'inputs': {'DNA': ['chromosome']}, + 'outputs': { + 'RNA': ['..', 'cytoplasm']}}}}} + + schema, state = core.complete( + {}, + cell_state) + + assert 'chromosome' in schema['cell1']['nucleus'] + + +def test_expected_schema(core): + # equivalent to previous schema: + + # expected = { + # 'store1': { + # 'store1.1': { + # '_value': 1.1, + # '_type': 'float', + # }, + # 'store1.2': { + # '_value': 2, + # '_type': 'integer', + # }, + # 'process1': { + # '_ports': { + # 'port1': {'_type': 'type'}, + # 'port2': {'_type': 'type'}, + # }, + # '_wires': { + # 'port1': 'store1.1', + # 'port2': 'store1.2', + # } + # }, + # 'process2': { + # '_ports': { + # 'port1': {'_type': 'type'}, + # 'port2': {'_type': 'type'}, + # }, + # '_wires': { + # 'port1': 'store1.1', + # 'port2': 'store1.2', + # } + # }, + # }, + # 'process3': { + # '_wires': { + # 'port1': 'store1', + # } + # } + # } + + dual_process_schema = { + 'process1': 'edge[input1:float|input2:integer,output1:float|output2:integer]', + 'process2': { + '_type': 'edge', + '_inputs': { + 'input1': 'float', + 'input2': 'integer'}, + '_outputs': { + 'output1': 'float', + 'output2': 'integer'}}} + + core.register( + 'dual_process', + dual_process_schema, + ) + + test_schema = { + # 'store1': 'process1.edge[port1.float|port2.int]|process2[port1.float|port2.int]', + 'store1': 'dual_process', + 'process3': 'edge[input_process:dual_process,output_process:dual_process]'} + + test_state = { + 'store1': { + 'process1': { + 'inputs': { + 'input1': ['store1.1'], + 'input2': ['store1.2']}, + 'outputs': { + 'output1': ['store2.1'], + 'output2': ['store2.2']}}, + 'process2': { + 'inputs': { + 'input1': ['store2.1'], + 'input2': ['store2.2']}, + 'outputs': { + 'output1': ['store1.1'], + 'output2': ['store1.2']}}}, + 'process3': { + 'inputs': { + 'input_process': ['store1']}, + 'outputs': { + 'output_process': ['store1']}}} + + outcome = core.fill(test_schema, test_state) + + assert outcome == { + 'process3': { + 'inputs': { + 'input_process': ['store1']}, + 'outputs': { + 'output_process': ['store1']}}, + 'store1': { + 'process1': { + 'inputs': { + 'input1': ['store1.1'], + 'input2': ['store1.2']}, + 'outputs': { + 'output1': ['store2.1'], + 'output2': ['store2.2']}}, + 'process2': { + 'inputs': {'input1': ['store2.1'], + 'input2': ['store2.2']}, + 'outputs': {'output1': ['store1.1'], + 'output2': ['store1.2']}}, + 'store1.1': 0.0, + 'store1.2': 0, + 'store2.1': 0.0, + 'store2.2': 0}} + + +def test_link_place(core): + # TODO: this form is more fundamental than the compressed/inline dict form, + # and we should probably derive that from this form + + bigraph = { + 'nodes': { + 'v0': 'integer', + 'v1': 'integer', + 'v2': 'integer', + 'v3': 'integer', + 'v4': 'integer', + 'v5': 'integer', + 'e0': 'edge[e0-0:int|e0-1:int|e0-2:int]', + 'e1': { + '_type': 'edge', + '_ports': { + 'e1-0': 'integer', + 'e2-0': 'integer'}}, + 'e2': { + '_type': 'edge[e2-0:int|e2-1:int|e2-2:int]'}}, + + 'place': { + 'v0': None, + 'v1': 'v0', + 'v2': 'v0', + 'v3': 'v2', + 'v4': None, + 'v5': 'v4', + 'e0': None, + 'e1': None, + 'e2': None}, + + 'link': { + 'e0': { + 'e0-0': 'v0', + 'e0-1': 'v1', + 'e0-2': 'v4'}, + 'e1': { + 'e1-0': 'v3', + 'e1-1': 'v1'}, + 'e2': { + 'e2-0': 'v3', + 'e2-1': 'v4', + 'e2-2': 'v5'}}, + + 'state': { + 'v0': '1', + 'v1': '1', + 'v2': '2', + 'v3': '3', + 'v4': '5', + 'v5': '8', + 'e0': { + 'wires': { + 'e0-0': 'v0', + 'e0-1': 'v1', + 'e0-2': 'v4'}}, + 'e1': { + 'wires': { + 'e1-0': 'v3', + 'e1-1': 'v1'}}, + 'e2': { + 'e2-0': 'v3', + 'e2-1': 'v4', + 'e2-2': 'v5'}}} + + placegraph = { # schema + 'v0': { + 'v1': int, + 'v2': { + 'v3': int}}, + 'v4': { + 'v5': int}, + 'e0': 'edge', + 'e1': 'edge', + 'e2': 'edge'} + + hypergraph = { # edges + 'e0': { + 'e0-0': 'v0', + 'e0-1': 'v1', + 'e0-2': 'v4'}, + 'e1': { + 'e1-0': 'v3', + 'e1-1': 'v1'}, + 'e2': { + 'e2-0': 'v3', + 'e2-1': 'v4', + 'e2-2': 'v5'}} + + merged = { + 'v0': { + 'v1': {}, + 'v2': { + 'v3': {}}}, + 'v4': { + 'v5': {}}, + 'e0': { + 'wires': { + 'e0.0': ['v0'], + 'e0.1': ['v0', 'v1'], + 'e0.2': ['v4']}}, + 'e1': { + 'wires': { + 'e0.0': ['v0', 'v2', 'v3'], + 'e0.1': ['v0', 'v1']}}, + 'e2': { + 'wires': { + 'e0.0': ['v0', 'v2', 'v3'], + 'e0.1': ['v4'], + 'e0.2': ['v4', 'v5']}}} + + result = core.link_place(placegraph, hypergraph) + # assert result == merged + + +def test_units(core): + schema_length = { + 'distance': {'_type': 'length'}} + + state = {'distance': 11 * units.meter} + update = {'distance': -5 * units.feet} + + new_state = core.apply( + schema_length, + state, + update + ) + + assert new_state['distance'] == 9.476 * units.meter + + +def test_unit_conversion(core): + # mass * length ^ 2 / second ^ 2 + + units_schema = { + 'force': 'length^2*mass/time^2'} + + force_units = units.meter ** 2 * units.kg / units.second ** 2 + + instance = { + 'force': 3.333 * force_units} + + +def test_serialize_deserialize(core): + schema = { + 'edge1': { + # '_type': 'edge[1:int|2:float|3:string|4:tree[int]]', + '_type': 'edge', + '_outputs': { + '1': 'integer', + '2': 'float', + '3': 'string', + '4': 'tree[integer]'}}, + 'a0': { + 'a0.0': 'integer', + 'a0.1': 'float', + 'a0.2': { + 'a0.2.0': 'string'}}, + 'a1': 'tree[integer]'} + + instance = { + 'edge1': { + 'outputs': { + '1': ['a0', 'a0.0'], + '2': ['a0', 'a0.1'], + '3': ['a0', 'a0.2', 'a0.2.0'], + '4': ['a1']}}, + 'a1': { + 'branch1': { + 'branch2': 11, + 'branch3': 22}, + 'branch4': 44}} + + instance = core.fill(schema, instance) + + encoded = core.serialize(schema, instance) + decoded = core.deserialize(schema, encoded) + + assert instance == decoded + + +# is this a lens? +def test_project(core): + schema = { + 'edge1': { + # '_type': 'edge[1:int|2:float|3:string|4:tree[int]]', + # '_type': 'edge', + '_type': 'edge', + '_inputs': { + '1': 'integer', + '2': 'float', + '3': 'string', + 'inner': { + 'chamber': 'tree[integer]'}, + '4': 'tree[integer]'}, + '_outputs': { + '1': 'integer', + '2': 'float', + '3': 'string', + 'inner': { + 'chamber': 'tree[integer]'}, + '4': 'tree[integer]'}}, + 'a0': { + 'a0.0': 'integer', + 'a0.1': 'float', + 'a0.2': { + 'a0.2.0': 'string'}}, + 'a1': { + '_type': 'tree[integer]'}} + + path_format = { + '1': 'a0>a0.0', + '2': 'a0>a0.1', + '3': 'a0>a0.2>a0.2.0'} + + # TODO: support separate schema/instance, and + # instances with '_type' and type parameter keys + # TODO: support overriding various type methods + instance = { + 'a0': { + 'a0.0': 11}, + 'edge1': { + 'inputs': { + '1': ['a0', 'a0.0'], + '2': ['a0', 'a0.1'], + '3': ['a0', 'a0.2', 'a0.2.0'], + 'inner': { + 'chamber': ['a1', 'a1.0']}, + '4': ['a1']}, + 'outputs': { + '1': ['a0', 'a0.0'], + '2': ['a0', 'a0.1'], + '3': ['a0', 'a0.2', 'a0.2.0'], + 'inner': { + 'chamber': { + 'X': ['a1', 'a1.0', 'Y']}}, + '4': ['a1']}}, + 'a1': { + 'a1.0': { + 'X': 555}, + 'branch1': { + 'branch2': 11, + 'branch3': 22}, + 'branch4': 44}} + + instance = core.fill(schema, instance) + + states = core.view_edge( + schema, + instance, + ['edge1']) + + update = core.project_edge( + schema, + instance, + ['edge1'], + states) + + assert update == { + 'a0': { + 'a0.0': 11, + 'a0.1': 0.0, + 'a0.2': { + 'a0.2.0': ''}}, + 'a1': { + 'a1.0': { + 'X': 555, + 'Y': {}}, + 'branch1': { + 'branch2': 11, + 'branch3': 22}, + 'branch4': 44}} + + # TODO: make sure apply does not mutate instance + updated_instance = core.apply( + schema, + instance, + update) + + add_update = { + '4': { + 'branch6': 111, + 'branch1': { + '_add': { + 'branch7': 4444, + 'branch8': 555, + }, + '_remove': ['branch2']}, + '_add': { + 'branch5': 55}, + '_remove': ['branch4']}} + + inverted_update = core.project_edge( + schema, + updated_instance, + ['edge1'], + add_update) + + modified_branch = core.apply( + schema, + updated_instance, + inverted_update) + + assert modified_branch == { + 'a0': { + 'a0.0': 22, + 'a0.1': 0.0, + 'a0.2': { + 'a0.2.0': ''}}, + 'edge1': {'inputs': {'1': ['a0', 'a0.0'], + '2': ['a0', 'a0.1'], + '3': ['a0', 'a0.2', 'a0.2.0'], + 'inner': { + 'chamber': ['a1', 'a1.0']}, + '4': ['a1']}, + 'outputs': {'1': ['a0', 'a0.0'], + '2': ['a0', 'a0.1'], + '3': ['a0', 'a0.2', 'a0.2.0'], + 'inner': { + 'chamber': { + 'X': ['a1', 'a1.0', 'Y']}}, + '4': ['a1']}}, + 'a1': { + 'a1.0': { + 'X': 1110, + 'Y': {}}, + 'branch1': { + 'branch3': 44, + 'branch7': 4444, + 'branch8': 555, }, + 'branch6': 111, + 'branch5': 55}} + + +def test_check(core): + assert core.check('float', 1.11) + assert core.check({'b': 'float'}, {'b': 1.11}) + + +def test_inherits_from(core): + assert core.inherits_from( + 'float', + 'number') + + assert core.inherits_from( + 'tree[float]', + 'tree[number]') + + assert core.inherits_from( + 'tree[path]', + 'tree[list[string]]') + + assert not core.inherits_from( + 'tree[path]', + 'tree[list[number]]') + + assert not core.inherits_from( + 'tree[float]', + 'tree[string]') + + assert not core.inherits_from( + 'tree[float]', + 'list[float]') + + assert core.inherits_from({ + 'a': 'float', + 'b': 'schema'}, { + + 'a': 'number', + 'b': 'tree'}) + + assert not core.inherits_from({ + 'a': 'float', + 'b': 'schema'}, { + + 'a': 'number', + 'b': 'number'}) + + +def test_resolve_schemas(core): + resolved = core.resolve_schemas({ + 'a': 'float', + 'b': 'map[list[string]]'}, { + 'a': 'number', + 'b': 'map[path]', + 'c': 'string'}) + + assert resolved['a']['_type'] == 'float' + assert resolved['b']['_value']['_type'] == 'path' + assert resolved['c']['_type'] == 'string' + + raises_on_incompatible_schemas = False + try: + core.resolve_schemas({ + 'a': 'string', + 'b': 'map[list[string]]'}, { + 'a': 'number', + 'b': 'map[path]', + 'c': 'string'}) + except: + raises_on_incompatible_schemas = True + + assert raises_on_incompatible_schemas + + +def test_apply_schema(core): + current = { + 'a': 'number', + 'b': 'map[path]', + 'd': ('float', 'number', 'list[string]')} + + update = { + 'a': 'float', + 'b': 'map[list[string]]', + 'c': 'string', + 'd': ('number', 'float', 'path')} + + applied = apply_schema( + 'schema', + current, + update, + core) + + assert applied['a']['_type'] == 'float' + assert applied['b']['_value']['_type'] == 'path' + assert applied['c']['_type'] == 'string' + assert applied['d']['_0']['_type'] == 'float' + assert applied['d']['_1']['_type'] == 'float' + assert applied['d']['_2']['_type'] == 'path' + + +def apply_foursquare(schema, current, update, core): + if isinstance(current, bool) or isinstance(update, bool): + return update + else: + for key, value in update.items(): + current[key] = apply_foursquare( + schema, + current[key], + value, + core) + + return current + + +def test_foursquare(core): + foursquare_schema = { + '_apply': apply_foursquare, + '00': 'boolean~foursquare', + '01': 'boolean~foursquare', + '10': 'boolean~foursquare', + '11': 'boolean~foursquare'} + + core.register( + 'foursquare', + foursquare_schema) + + example = { + '00': True, + '01': False, + '10': False, + '11': { + '00': True, + '01': False, + '10': False, + '11': { + '00': True, + '01': False, + '10': False, + '11': { + '00': True, + '01': False, + '10': False, + '11': { + '00': True, + '01': False, + '10': False, + '11': { + '00': True, + '01': False, + '10': False, + '11': False}}}}}} + + assert core.check( + 'foursquare', + example) + + example['10'] = 5 + + assert not core.check( + 'foursquare', + example) + + update = { + '01': True, + '11': { + '01': True, + '11': { + '11': True, + '10': { + '10': { + '00': True, + '11': False}}}}} + + result = core.apply( + 'foursquare', + example, + update) + + assert result == { + '00': True, + '01': True, + '10': 5, + '11': {'00': True, + '01': True, + '10': False, + '11': {'00': True, + '01': False, + '10': { + '10': { + '00': True, + '11': False}}, + '11': True}}} + + +def test_add_reaction(core): + single_node = { + 'environment': { + '_type': 'compartment', + 'counts': {'A': 144}, + 'inner': { + '0': { + 'counts': {'A': 13}, + 'inner': {}}}}} + + add_config = { + 'path': ['environment', 'inner'], + 'add': { + '1': { + 'counts': { + 'A': 8}}}} + + schema, state = core.infer_schema( + {}, + single_node) + + assert '0' in state['environment']['inner'] + assert '1' not in state['environment']['inner'] + + result = core.apply( + schema, + state, { + '_react': { + 'add': add_config}}) + + # '_react': { + # 'reaction': 'add', + # 'config': add_config}}) + + assert '0' in result['environment']['inner'] + assert '1' in result['environment']['inner'] + + +def test_remove_reaction(core): + single_node = { + 'environment': { + '_type': 'compartment', + 'counts': {'A': 144}, + 'inner': { + '0': { + 'counts': {'A': 13}, + 'inner': {}}, + '1': { + 'counts': {'A': 13}, + 'inner': {}}}}} + + remove_config = { + 'path': ['environment', 'inner'], + 'remove': ['0']} + + schema, state = core.infer_schema( + {}, + single_node) + + assert '0' in state['environment']['inner'] + assert '1' in state['environment']['inner'] + + result = core.apply( + schema, + state, { + '_react': { + 'remove': remove_config}}) + + assert '0' not in result['environment']['inner'] + assert '1' in state['environment']['inner'] + + +def test_replace_reaction(core): + single_node = { + 'environment': { + '_type': 'compartment', + 'counts': {'A': 144}, + 'inner': { + '0': { + 'counts': {'A': 13}, + 'inner': {}}, + '1': { + 'counts': {'A': 13}, + 'inner': {}}}}} + + # replace_config = { + # 'path': ['environment', 'inner'], + # 'before': {'0': {'A': '?1'}}, + # 'after': { + # '2': { + # 'counts': { + # 'A': {'function': 'divide', 'arguments': ['?1', 0.5], }}}, + # '3': { + # 'counts': { + # 'A': '@1'}}}} + + replace_config = { + 'path': ['environment', 'inner'], + 'before': {'0': {}}, + 'after': { + '2': { + 'counts': { + 'A': 3}}, + '3': { + 'counts': { + 'A': 88}}}} + + schema, state = core.infer_schema( + {}, + single_node) + + assert '0' in state['environment']['inner'] + assert '1' in state['environment']['inner'] + + result = core.apply( + schema, + state, { + '_react': { + 'replace': replace_config}}) + + assert '0' not in result['environment']['inner'] + assert '1' in result['environment']['inner'] + assert '2' in result['environment']['inner'] + assert '3' in result['environment']['inner'] + + +def test_reaction(core): + single_node = { + 'environment': { + 'counts': {}, + 'inner': { + '0': { + 'counts': {}}}}} + + # TODO: compartment type ends up as 'any' at leafs? + + # TODO: come at divide reaction from the other side: + # ie make a call for it, then figure out what the + # reaction needs to be + def divide_reaction(container, mother, divider): + daughters = divider(mother) + + return { + 'redex': mother, + 'reactum': daughters} + + embedded_tree = { + 'environment': { + '_type': 'compartment', + 'counts': {}, + 'inner': { + 'agent1': { + '_type': 'compartment', + 'counts': {}, + 'inner': { + 'agent2': { + '_type': 'compartment', + 'counts': {}, + 'inner': {}, + 'transport': { + 'wires': { + 'outer': ['..', '..'], + 'inner': ['inner']}}}}, + 'transport': { + 'wires': { + 'outer': ['..', '..'], + 'inner': ['inner']}}}}}} + + mother_tree = { + 'environment': { + '_type': 'compartment', + 'counts': { + 'A': 15}, + 'inner': { + 'mother': { + '_type': 'compartment', + 'counts': { + 'A': 5}}}}} + + divide_react = { + '_react': { + 'redex': { + 'mother': { + 'counts': '@counts'}}, + 'reactum': { + 'daughter1': { + 'counts': '@daughter1_counts'}, + 'daughter2': { + 'counts': '@daughter2_counts'}}, + 'calls': [{ + 'function': 'divide_counts', + 'arguments': ['@counts', [0.5, 0.5]], + 'bindings': ['@daughter1_counts', '@daughter2_counts']}]}} + + divide_update = { + '_react': { + 'reaction': 'divide_counts', + 'config': { + 'id': 'mother', + 'state_key': 'counts', + 'daughters': [ + {'id': 'daughter1', 'ratio': 0.3}, + {'id': 'daughter2', 'ratio': 0.7}]}}} + + divide_update_concise = { + '_react': { + 'divide_counts': { + 'id': 'mother', + 'state_key': 'counts', + 'daughters': [ + {'id': 'daughter1', 'ratio': 0.3}, + {'id': 'daughter2', 'ratio': 0.7}]}}} + + +def test_map_type(core): + schema = 'map[integer]' + + state = { + 'a': 12, + 'b': 13, + 'c': 15, + 'd': 18} + + update = { + 'b': 44, + 'd': 111} + + assert core.check(schema, state) + assert core.check(schema, update) + assert not core.check(schema, 15) + + result = core.apply( + schema, + state, + update) + + assert result['a'] == 12 + assert result['b'] == 57 + assert result['d'] == 129 + + encode = core.serialize(schema, update) + assert encode['d'] == '111' + + decode = core.deserialize(schema, encode) + assert decode == update + + +def test_tree_type(core): + schema = 'tree[maybe[integer]]' + + state = { + 'a': 12, + 'b': 13, + 'c': { + 'e': 5555, + 'f': 111}, + 'd': None} + + update = { + 'a': None, + 'c': { + 'e': 88888, + 'f': 2222, + 'G': None}, + 'd': 111} + + assert core.check(schema, state) + assert core.check(schema, update) + assert core.check(schema, 15) + assert core.check(schema, None) + assert core.check(schema, {'c': {'D': None, 'e': 11111}}) + assert not core.check(schema, 'yellow') + assert not core.check(schema, {'a': 5, 'b': 'green'}) + assert not core.check(schema, {'c': {'D': False, 'e': 11111}}) + + result = core.apply( + schema, + state, + update) + + assert result['a'] == None + assert result['b'] == 13 + assert result['c']['f'] == 2333 + assert result['d'] == 111 + + encode = core.serialize(schema, update) + assert encode['a'] == NONE_SYMBOL + assert encode['d'] == '111' + + decode = core.deserialize(schema, encode) + assert decode == update + + +def test_maybe_type(core): + schema = 'map[maybe[integer]]' + + state = { + 'a': 12, + 'b': 13, + 'c': None, + 'd': 18} + + update = { + 'a': None, + 'c': 44, + 'd': 111} + + assert core.check(schema, state) + assert core.check(schema, update) + assert not core.check(schema, 15) + + result = core.apply( + schema, + state, + update) + + assert result['a'] == None + assert result['b'] == 13 + assert result['c'] == 44 + assert result['d'] == 129 + + encode = core.serialize(schema, update) + assert encode['a'] == NONE_SYMBOL + assert encode['d'] == '111' + + decode = core.deserialize(schema, encode) + assert decode == update + + +def test_tuple_type(core): + schema = { + '_type': 'tuple', + '_type_parameters': ['0', '1', '2'], + '_0': 'string', + '_1': 'int', + '_2': 'map[maybe[float]]'} + + schema = ('string', 'int', 'map[maybe[float]]') + schema = 'tuple[string,int,map[maybe[float]]]' + schema = 'string|integer|map[maybe[float]]' + + state = ( + 'aaaaa', + 13, { + 'a': 1.1, + 'b': None}) + + update = ( + 'bbbbbb', + 10, { + 'a': 33.33, + 'b': 4.44444}) + + assert core.check(schema, state) + assert core.check(schema, update) + assert not core.check(schema, 15) + + result = core.apply( + schema, + state, + update) + + assert len(result) == 3 + assert result[0] == update[0] + assert result[1] == 23 + assert result[2]['a'] == 34.43 + assert result[2]['b'] == update[2]['b'] + + encode = core.serialize(schema, state) + assert encode[2]['b'] == NONE_SYMBOL + assert encode[1] == '13' + + decode = core.deserialize(schema, encode) + assert decode == state + + tuple_type = core.access('(3|4|10)') + assert '_2' in tuple_type + assert tuple_type['_2'] == '10' + + tuple_type = core.access('tuple[9,float,7]') + assert '_2' in tuple_type + assert tuple_type['_2'] == '7' + + +def test_union_type(core): + schema = { + '_type': 'union', + '_type_parameters': ['0', '1', '2'], + '_0': 'string', + '_1': 'integer', + '_2': 'map[maybe[float]]'} + + schema = 'string~integer~map[maybe[float]]' + + state = { + 'a': 1.1, + 'b': None} + + update = { + 'a': 33.33, + 'b': 4.44444} + + assert core.check(schema, state) + assert core.check(schema, update) + assert core.check(schema, 15) + + wrong_state = { + 'a': 1.1, + 'b': None} + + wrong_update = 'a different type' + + assert core.check(schema, wrong_state) + assert core.check(schema, wrong_update) + + # TODO: deal with union apply of different types + + result = core.apply( + schema, + state, + update) + + assert result['a'] == 34.43 + assert result['b'] == update['b'] + + encode = core.serialize(schema, state) + assert encode['b'] == NONE_SYMBOL + + decode = core.deserialize(schema, encode) + assert decode == state + + +def test_union_values(core): + schema = 'map[integer~string~map[maybe[float]]]' + + state = { + 'a': 'bbbbb', + 'b': 15} + + update = { + 'a': 'aaaaa', + 'b': 22} + + assert core.check(schema, state) + assert core.check(schema, update) + assert not core.check(schema, 15) + + result = core.apply( + schema, + state, + update) + + assert result['a'] == 'aaaaa' + assert result['b'] == 37 + + encode = core.serialize(schema, state) + decode = core.deserialize(schema, encode) + + assert decode == state + + +def test_array_type(core): + shape = (3, 4, 10) + shape_representation = core.representation(shape) + shape_commas = ','.join([ + str(x) + for x in shape]) + + schema = { + '_type': 'map', + '_value': { + '_type': 'array', + # '_shape': '(3|4|10)', + '_shape': shape_representation, + '_data': 'float'}} + + schema = f'map[array[tuple[{shape_commas}],float]]' + schema = f'map[array[{shape_representation},float]]' + + state = { + 'a': np.zeros(shape), + 'b': np.ones(shape)} + + update = { + 'a': np.full(shape, 5.555), + 'b': np.full(shape, 9.999)} + + assert core.check(schema, state) + assert core.check(schema, update) + assert not core.check(schema, 15) + + result = core.apply( + schema, + state, + update) + + assert result['a'][0, 0, 0] == 5.555 + assert result['b'][0, 0, 0] == 10.999 + + encode = core.serialize(schema, state) + assert encode['b']['shape'] == list(shape) + assert encode['a']['data'] == 'float' + + decode = core.deserialize(schema, encode) + + for key in state: + assert np.equal( + decode[key], + state[key]).all() + + found = core.find( + schema) + + default = core.default( + found['_value']) + + assert default.shape == shape + + +def test_infer_edge(core): + initial_schema = {} + initial_state = { + 'fade': { + '_type': 'edge', + '_inputs': { + 'yellow': 'array[(3|4|10),float]'}, + '_outputs': { + 'green': 'array[(11|5|8),float]'}, + 'inputs': { + 'yellow': ['yellow']}, + 'outputs': { + 'green': ['green']}}} + + update = { + 'yellow': np.ones((3, 4, 10)), + 'fade': { + 'inputs': { + 'yellow': ['red']}, + 'outputs': { + 'green': ['green', 'green', 'green']}}} + + schema, state = core.complete( + initial_schema, + initial_state) + + assert core.check(schema, state) + assert not core.check(schema, 15) + + result = core.apply( + schema, + state, + update) + + assert result['yellow'][0, 0, 0] == 1.0 + assert result['fade']['inputs']['yellow'] == ['red'] + + encode = core.serialize(schema, state) + decode = core.deserialize(schema, encode) + + assert np.equal( + decode['yellow'], + state['yellow']).all() + + +def test_edge_type(core): + schema = { + 'fade': { + '_type': 'edge', + '_inputs': { + 'yellow': { + '_type': 'array', + '_shape': 'tuple(3,4,10)', + '_data': 'float'}}, + '_outputs': { + 'green': { + '_type': 'array', + '_shape': 'tuple(11,5,8)', + '_data': 'float'}}}} + + initial_schema = { + 'fade': 'edge[yellow:array[(3|4|10),float],green:array[(11|5|8),float]]'} + + initial_state = { + # 'yellow': np.zeros((3, 4, 10)), + # 'green': np.ones((11, 5, 8)), + 'fade': { + 'inputs': { + 'yellow': ['yellow']}, + 'outputs': { + 'green': ['green']}}} + + schema, state = core.complete( + initial_schema, + initial_state) + + update = { + 'yellow': np.ones((3, 4, 10)), + 'fade': { + 'inputs': { + 'yellow': ['red']}, + 'outputs': { + 'green': ['green', 'green', 'green']}}} + + assert core.check(schema, state) + assert not core.check(schema, 15) + + result = core.apply( + schema, + state, + update) + + assert result['yellow'][0, 0, 0] == 1.0 + assert result['fade']['inputs']['yellow'] == ['red'] + + encode = core.serialize(schema, state) + decode = core.deserialize(schema, encode) + + assert np.equal( + decode['yellow'], + state['yellow']).all() + + +def test_edge_complete(core): + edge_schema = { + '_type': 'edge', + '_inputs': { + 'concentration': 'float', + 'field': 'map[boolean]'}, + '_outputs': { + 'target': 'boolean', + # 'inner': { + # 'nested': 'boolean'}, + 'total': 'integer', + 'delta': 'float'}} + + edge_state = { + 'inputs': { + 'concentration': ['molecules', 'glucose'], + 'field': ['states']}, + 'outputs': { + 'target': ['states', 'X'], + # 'inner': { + # 'nested': ['states', 'A']}, + 'total': ['emitter', 'total molecules'], + 'delta': ['molecules', 'glucose']}} + + # edge_state = { + # 'inputs': { + # 'concentration': ['..', 'molecules', 'glucose'], + # 'field': ['..', 'states']}, + # 'outputs': { + # 'target': ['..', 'states', 'X'], + # 'total': ['..', 'emitter', 'total molecules'], + # 'delta': ['..', 'molecules', 'glucose']}} + + full_schema, full_state = core.complete( + {'edge': edge_schema}, + {'edge': edge_state}) + + assert full_schema['states']['_type'] == 'map' + + +def test_divide(core): + schema = { + 'a': 'tree[maybe[float]]', + 'b': 'float~list[string]', + 'c': { + 'd': 'map[edge[GGG:float,OOO:float]]', + 'e': 'array[(3|4|10),float]'}} + + state = { + 'a': { + 'x': { + 'oooo': None, + 'y': 1.1, + 'z': 33.33}, + 'w': 44.444}, + 'b': ['1', '11', '111', '1111'], + 'c': { + 'd': { + 'A': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'w']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'x', 'y']}}, + 'B': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'x', 'y']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'w']}}}, + 'e': np.zeros((3, 4, 10))}} + + divisions = 3 + division = core.fold( + schema, + state, + 'divide', + {'divisions': divisions}) + + assert len(division) == divisions + assert 'a' in division[0].keys() + assert len(division[1]['b']) == len(state['b']) + + +def test_merge(core): + current_schema = { + 'a': 'tree[maybe[float]]', + 'b': 'float~list[string]', + 'c': { + 'd': 'map[edge[GGG:float,OOO:float]]', + 'e': 'array[(3|4|10),float]'}} + + current_state = { + 'a': { + 'x': { + 'oooo': None, + 'y': 1.1, + 'z': 33.33}, + 'w': 44.444}, + 'b': ['1', '11', '111', '1111'], + 'c': { + 'd': { + 'A': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'w']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'x', 'y']}}, + 'B': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'x', 'y']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'w']}}}, + 'e': np.zeros((3, 4, 10))}} + + merge_state = { + 'z': 555.55, + 'b': ['333333333'], + 'a': { + 'x': { + 'x': { + 'o': 99999.11}}}} + + result = core.merge_recur( + current_schema, + current_state, + merge_state) + + assert result['z'] == merge_state['z'] + assert result['b'] == merge_state['b'] + assert result['a']['x']['x']['o'] == merge_state['a']['x']['x']['o'] + + +def test_bind(core): + current_schema = { + 'a': 'tree[maybe[float]]', + 'b': 'float~list[string]', + 'c': { + 'd': 'map[edge[GGG:float,OOO:float]]', + 'e': 'array[(3|4|10),float]'}} + + current_state = { + 'a': { + 'x': { + 'oooo': None, + 'y': 1.1, + 'z': 33.33}, + 'w': 44.444}, + 'b': ['1', '11', '111', '1111'], + 'c': { + 'd': { + 'A': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'w']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'x', 'y']}}, + 'B': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'x', 'y']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'w']}}}, + 'e': np.zeros((3, 4, 10))}} + + result_schema, result_state = core.bind( + current_schema, + current_state, + 'z', + 'float', + 555.55) + + assert result_schema['z']['_type'] == 'float' + assert result_state['z'] == 555.55 + + +def test_slice(core): + schema, state = core.slice( + 'map[float]', + {'aaaa': 55.555}, + ['aaaa']) + + schema, state = core.complete({}, { + 'top': { + '_type': 'tree[list[maybe[(float|integer)~string]]]', + 'AAAA': { + 'BBBB': { + 'CCCC': [ + (1.3, 5), + 'okay', + (55.555, 1), + None, + 'what', + 'is']}}, + 'DDDD': [ + (3333.1, 88), + 'in', + 'between', + (66.8, -3), + None, + None, + 'later']}}) + + float_schema, float_state = core.slice( + schema, + state, + ['top', 'AAAA', 'BBBB', 'CCCC', 2, 0]) + + assert float_schema['_type'] == 'float' + assert float_state == 55.555 + + assert core.slice( + schema, + state, + ['top', 'AAAA', 'BBBB', 'CCCC', 3])[1] is None + + +def test_set_slice(core): + float_schema, float_state = core.set_slice( + 'map[float]', + {'aaaa': 55.555}, + ['bbbbb'], + 'float', + 888.88888) + + assert float_schema['_type'] == 'map' + assert float_state['bbbbb'] == 888.88888 + + schema, state = core.complete({}, { + 'top': { + '_type': 'tree[list[maybe[(float|integer)~string]]]', + 'AAAA': { + 'BBBB': { + 'CCCC': [ + (1.3, 5), + 'okay', + (55.555, 1), + None, + 'what', + 'is']}}, + 'DDDD': [ + (3333.1, 88), + 'in', + 'between', + (66.8, -3), + None, + None, + 'later']}}) + + leaf_schema, leaf_state = core.set_slice( + schema, + state, + ['top', 'AAAA', 'BBBB', 'CCCC', 2, 1], + 'integer', + 33) + + assert core.slice( + leaf_schema, + leaf_state, [ + 'top', + 'AAAA', + 'BBBB', + 'CCCC', + 2, + 1])[1] == 33 + + +def from_state(dataclass, state): + if hasattr(dataclass, '__dataclass_fields__'): + fields = dataclass.__dataclass_fields__ + state = state or {} + + init = {} + for key, field in fields.items(): + substate = from_state( + field.type, + state.get(key)) + init[key] = substate + instance = dataclass(**init) + # elif get_origin(dataclass) in [typing.Union, typing.Mapping]: + # instance = state + else: + instance = state + # instance = dataclass(state) + + return instance + + +def test_dataclass(core): + simple_schema = { + 'a': 'float', + 'b': 'integer', + 'c': 'boolean', + 'x': 'string'} + + # TODO: accept just a string instead of only a path + simple_dataclass = core.dataclass( + simple_schema, + ['simple']) + + simple_state = { + 'a': 88.888, + 'b': 11111, + 'c': False, + 'x': 'not a string'} + + simple_new = simple_dataclass( + a=1.11, + b=33, + c=True, + x='what') + + simple_from = from_state( + simple_dataclass, + simple_state) + + nested_schema = { + 'a': { + 'a': { + 'a': 'float', + 'b': 'float'}, + 'x': 'float'}} + + nested_dataclass = core.dataclass( + nested_schema, + ['nested']) + + nested_state = { + 'a': { + 'a': { + 'a': 13.4444, + 'b': 888.88}, + 'x': 111.11111}} + + nested_new = data.nested( + data.nested_a( + data.nested_a_a( + a=222.22, + b=3.3333), + 5555.55)) + + nested_from = from_state( + nested_dataclass, + nested_state) + + complex_schema = { + 'a': 'tree[maybe[float]]', + 'b': 'float~list[string]', + 'c': { + 'd': 'map[edge[GGG:float,OOO:float]]', + 'e': 'array[(3|4|10),float]'}} + + complex_dataclass = core.dataclass( + complex_schema, + ['complex']) + + complex_state = { + 'a': { + 'x': { + 'oooo': None, + 'y': 1.1, + 'z': 33.33}, + 'w': 44.444}, + 'b': ['1', '11', '111', '1111'], + 'c': { + 'd': { + 'A': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'w']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'x', 'y']}}, + 'B': { + 'inputs': { + 'GGG': ['..', '..', 'a', 'x', 'y']}, + 'outputs': { + 'OOO': ['..', '..', 'a', 'w']}}}, + 'e': np.zeros((3, 4, 10))}} + + complex_from = from_state( + complex_dataclass, + complex_state) + + complex_dict = asdict(complex_from) + + # assert complex_dict == complex_state ? + + assert complex_from.a['x']['oooo'] is None + assert len(complex_from.c.d['A']['inputs']['GGG']) + assert isinstance(complex_from.c.e, np.ndarray) + + +def test_enum_type(core): + core.register( + 'planet', + 'enum[mercury,venus,earth,mars,jupiter,saturn,neptune]') + + # core.register('planet', { + # '_type': 'enum', + # '_type_parameters': ['0', '1', '2', '3', '4', '5', '6'], + # '_0': 'mercury', + # '_1': 'venus', + # '_2': 'earth', + # '_3': 'mars', + # '_4': 'jupiter', + # '_5': 'saturn', + # '_6': 'neptune'}) + + assert core.default('planet') == 'mercury' + + solar_system_schema = { + 'planets': 'map[planet]'} + + solar_system = { + 'planets': { + '3': 'earth', + '4': 'mars'}} + + jupiter_update = { + 'planets': { + '5': 'jupiter'}} + + pluto_update = { + 'planets': { + '7': 'pluto'}} + + assert core.check( + solar_system_schema, + solar_system) + + assert core.check( + solar_system_schema, + jupiter_update) + + assert not core.check( + solar_system_schema, + pluto_update) + + with_jupiter = core.apply( + solar_system_schema, + solar_system, + jupiter_update) + + try: + core.apply( + solar_system_schema, + solar_system, + pluto_update) + + assert False + except Exception as e: + print(e) + assert True + + +def test_map_schema(core): + schema = { + 'greetings': 'map[hello:string]', + 'edge': { + '_type': 'edge', + '_inputs': { + 'various': { + '_type': 'map', + '_value': { + 'world': 'string'}}}, + '_outputs': { + 'referent': 'float'}}} + + state = { + 'edge': { + 'inputs': { + 'various': ['greetings']}, + 'outputs': { + 'referent': ['where']}}, + + 'greetings': { + 'a': { + 'hello': 'yes'}, + 'b': { + 'hello': 'again', + 'world': 'present'}, + 'c': { + 'other': 'other'}}} + + complete_schema, complete_state = core.complete( + schema, + state) + + assert complete_schema['greetings']['_value']['hello']['_type'] == 'string' + assert complete_schema['greetings']['_value']['world']['_type'] == 'string' + + assert 'world' in complete_state['greetings']['a'] + assert complete_schema['greetings']['_value']['world']['_type'] == 'string' + + +def test_representation(core): + schema_examples = [ + 'map[float]', + '(string|float)', + 'tree[(a:float|b:map[string])]', + 'array[(5|11),maybe[integer]]', + 'edge[(x:float|y:tree[(z:float)]),(w:(float|float|float))]'] + + for example in schema_examples: + full_type = core.access(example) + representation = core.representation(full_type) + + if example != representation: + raise Exception( + f'did not receive the same type after parsing and finding the representation:\n {example}\n {representation}') + + +def test_generate(core): + schema = { + 'A': 'float', + 'B': 'enum[one,two,three]', + 'units': 'map[float]'} + + state = { + 'C': { + '_type': 'enum[x,y,z]', + '_default': 'y'}, + 'units': { + 'x': 11.1111, + 'y': 22.833333}} + + generated_schema, generated_state = core.generate( + schema, + state) + + assert generated_state['A'] == 0.0 + assert generated_state['B'] == 'one' + assert generated_state['C'] == 'y' + assert generated_state['units']['y'] == 22.833333 + assert 'x' not in generated_schema['units'] + + +def test_edge_cycle(core): + empty_schema = {} + empty_state = {} + + A_schema = { + 'A': { + '_type': 'metaedge', + '_inputs': { + 'before': { + 'inputs': {'before': {'_default': ['B']}}, + 'outputs': {'after': {'_default': ['A']}}}}, + '_outputs': { + 'after': { + 'inputs': {'before': {'_default': ['A']}}, + 'outputs': {'after': {'_default': ['C']}}}}, + 'inputs': {'before': {'_default': ['C']}}, + 'outputs': {'after': {'_default': ['B']}}}} + + A_state = { + 'A': { + '_type': 'metaedge', + '_inputs': { + 'before': { + 'inputs': {'before': {'_default': ['B']}}, + 'outputs': {'after': {'_default': ['A']}}}}, + '_outputs': { + 'after': { + 'inputs': {'before': {'_default': ['A']}}, + 'outputs': {'after': {'_default': ['C']}}}}, + 'inputs': {'before': {'_default': ['C']}}, + 'outputs': {'after': {'_default': ['B']}}}} + + schema_from_schema, state_from_schema = core.generate( + A_schema, + empty_state) + + schema_from_state, state_from_state = core.generate( + empty_schema, + A_state) + + # print(diff(schema_from_schema, schema_from_state)) + # print(diff(state_from_schema, state_from_state)) + + if schema_from_schema != schema_from_state: + print(diff(schema_from_schema, schema_from_state)) + + if state_from_schema != state_from_state: + print(diff(state_from_schema, state_from_state)) + + assert schema_from_schema == schema_from_state + assert state_from_schema == state_from_state + + for key in ['A', 'B', 'C']: + for result in [schema_from_schema, state_from_schema, schema_from_state, state_from_state]: + assert key in result + + +def test_merge(core): + schema = { + 'A': 'float', + 'B': 'enum[one,two,three]', + 'units': 'map[float]'} + + state = { + 'C': { + '_type': 'enum[x,y,z]', + '_default': 'y'}, + 'units': { + 'x': 11.1111, + 'y': 22.833333}} + + generated_schema, generated_state = core.generate( + schema, + state) + + edge_state = { + '_type': 'edge', + '_inputs': { + 'input': 'float'}, + '_outputs': { + 'output': 'float'}, + 'inputs': { + 'input': ['A']}, + 'outputs': { + 'output': ['D']}} + + top_schema, top_state = core.merge( + generated_schema, + generated_state, + ['edge'], + {}, + edge_state) + + assert 'D' in top_state + assert top_schema['D']['_type'] == 'float' + +def test_remove_omitted(): + result = remove_omitted( + {'a': {}, 'b': {'c': {}, 'd': {}}}, + {'b': {'c': {}}}, + {'a': {'X': 1111}, 'b': {'c': {'Y': 4444}, 'd': {'Z': 99999}}}) + + assert 'a' not in result + assert result['b']['c']['Y'] == 4444 + assert 'd' not in result['b'] + + +if __name__ == '__main__': + core = TypeSystem() + core = register_test_types(core) + + test_reregister_type(core) + test_generate_default(core) + test_apply_update(core) + test_validate_schema(core) + test_fill_integer(core) + test_fill_cube(core) + test_establish_path(core) + test_overwrite_existing(core) + test_fill_in_missing_nodes(core) + test_fill_from_parse(core) + test_fill_ports(core) + test_expected_schema(core) + test_units(core) + test_serialize_deserialize(core) + test_project(core) + test_inherits_from(core) + test_apply_schema(core) + test_resolve_schemas(core) + test_add_reaction(core) + test_remove_reaction(core) + test_replace_reaction(core) + test_unit_conversion(core) + test_map_type(core) + test_tree_type(core) + test_maybe_type(core) + test_tuple_type(core) + test_array_type(core) + test_union_type(core) + test_union_values(core) + test_infer_edge(core) + test_edge_type(core) + test_edge_complete(core) + test_foursquare(core) + test_divide(core) + test_merge(core) + test_bind(core) + test_slice(core) + test_set_slice(core) + test_dataclass(core) + test_enum_type(core) + test_map_schema(core) + test_representation(core) + test_generate(core) + test_edge_cycle(core) + test_merge(core) + test_remove_omitted() diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index 141350c..eb3dab9 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -6,7 +6,6 @@ import copy import pprint -import pytest import random import inspect import numbers @@ -45,7 +44,6 @@ '_bind', '_merge'] - overridable_schema_keys = set([ '_type', '_default', @@ -64,21 +62,23 @@ '_inherit', ]) - SYMBOL_TYPES = ['enum'] nonoverridable_schema_keys = type_schema_keys - overridable_schema_keys - merge_schema_keys = ( '_ports', '_type_parameters', ) - TYPE_SCHEMAS = { 'float': 'float'} +DTYPE_MAP = { + 'float': 'float64', + 'integer': 'int64', + 'string': 'str'} + def diff(a, b): if isinstance(a, dict) and isinstance(b, dict): @@ -676,7 +676,6 @@ def divide_integer(schema, value, values, core): other_half += 1 return [half, other_half] - def divide_longest(schema, dimensions, values, core): # any way to declare the required keys for this function in the registry? # find a way to ask a function what type its domain and codomain are @@ -4444,13 +4443,6 @@ def array_shape(core, schema): for parameter in schema['_type_parameters']]) - -DTYPE_MAP = { - 'float': 'float64', - 'integer': 'int64', - 'string': 'str'} - - def lookup_dtype(data_name): data_name = data_name or 'string' dtype_name = DTYPE_MAP.get(data_name) @@ -4472,7 +4464,6 @@ def read_shape(shape): shape)]) - def register_units(core, units): for unit_name in units._units: try: @@ -4746,2303 +4737,3 @@ def register_base_reactions(core): core.register_reaction('remove', remove_reaction) core.register_reaction('replace', replace_reaction) core.register_reaction('divide', divide_reaction) - - -def register_cube(core): - cube_schema = { - 'shape': { - '_type': 'shape', - '_description': 'abstract shape type'}, - - 'rectangle': { - '_type': 'rectangle', - '_divide': divide_longest, - '_description': 'a two-dimensional value', - '_inherit': 'shape', - 'width': {'_type': 'integer'}, - 'height': {'_type': 'integer'}, - }, - - # cannot override existing keys unless it is of a subtype - 'cube': { - '_type': 'cube', - '_inherit': 'rectangle', - 'depth': {'_type': 'integer'}, - }, - } - - for type_key, type_data in cube_schema.items(): - core.register(type_key, type_data) - - return core - - -@pytest.fixture -def core(): - core = TypeSystem() - return register_test_types(core) - - -def register_test_types(core): - register_cube(core) - - core.register('compartment', { - 'counts': 'tree[float]', - 'inner': 'tree[compartment]'}) - - core.register('metaedge', { - '_inherit': 'edge', - '_inputs': { - 'before': 'metaedge'}, - '_outputs': { - 'after': 'metaedge'}}) - - return core - - -def test_reregister_type(core): - core.register('A', {'_default': 'a'}) - with pytest.raises(Exception) as e: - core.register( - 'A', {'_default': 'b'}, - strict=True) - - core.register('A', {'_default': 'b'}) - - assert core.access('A')['_default'] == 'b' - - -def test_generate_default(core): - int_default = core.default( - {'_type': 'integer'} - ) - - assert int_default == 0 - - cube_default = core.default( - {'_type': 'cube'}) - - assert 'width' in cube_default - assert 'height' in cube_default - assert 'depth' in cube_default - - nested_default = core.default( - {'a': 'integer', - 'b': { - 'c': 'float', - 'd': 'cube'}, - 'e': 'string'}) - - assert nested_default['b']['d']['width'] == 0 - - -def test_apply_update(core): - schema = {'_type': 'cube'} - state = { - 'width': 11, - 'height': 13, - 'depth': 44, - } - - update = { - 'depth': -5 - } - - new_state = core.apply( - schema, - state, - update - ) - - assert new_state['width'] == 11 - assert new_state['depth'] == 39 - - -def print_schema_validation(core, library, should_pass): - for key, declaration in library.items(): - report = core.validate_schema(declaration) - if len(report) == 0: - message = f'valid schema: {key}' - if should_pass: - print(f'PASS: {message}') - pprint.pprint(declaration) - else: - raise Exception(f'FAIL: {message}\n{declaration}\n{report}') - else: - message = f'invalid schema: {key}' - if not should_pass: - print(f'PASS: {message}') - pprint.pprint(declaration) - else: - raise Exception(f'FAIL: {message}\n{declaration}\n{report}') - - -def test_validate_schema(core): - # good schemas - print_schema_validation(core, base_type_library, True) - - good = { - 'not quite int': { - '_default': 0, - '_apply': accumulate, - '_serialize': to_string, - '_deserialize': deserialize_integer, - '_description': '64-bit integer' - }, - 'ports match': { - 'a': { - '_type': 'integer', - '_value': 2 - }, - 'edge1': { - '_type': 'edge[a:integer]', - # '_type': 'edge', - # '_ports': { - # '1': {'_type': 'integer'}, - # }, - } - } - } - - # bad schemas - bad = { - 'empty': None, - 'str?': 'not a schema', - 'branch is weird': { - 'left': {'_type': 'ogre'}, - 'right': {'_default': 1, '_apply': accumulate}, - }, - } - - # test for ports and wires mismatch - - print_schema_validation(core, good, True) - print_schema_validation(core, bad, False) - - -def test_fill_integer(core): - test_schema = { - '_type': 'integer' - } - - full_state = core.fill(test_schema) - direct_state = core.fill('integer') - generated_schema, generated_state = core.generate( - test_schema, None) - - assert generated_schema['_type'] == 'integer' - assert full_state == direct_state == 0 == generated_state - - -def test_fill_cube(core): - test_schema = {'_type': 'cube'} - partial_state = {'height': 5} - - full_state = core.fill( - test_schema, - state=partial_state) - - assert 'width' in full_state - assert 'height' in full_state - assert 'depth' in full_state - assert full_state['height'] == 5 - assert full_state['depth'] == 0 - - -def test_fill_in_missing_nodes(core): - test_schema = { - 'edge 1': { - '_type': 'edge', - '_inputs': { - 'I': 'float'}, - '_outputs': { - 'O': 'float'}}} - - test_state = { - 'edge 1': { - 'inputs': { - 'I': ['a']}, - 'outputs': { - 'O': ['a']}}} - - filled = core.fill( - test_schema, - test_state) - - assert filled == { - 'a': 0.0, - 'edge 1': { - 'inputs': { - 'I': ['a']}, - 'outputs': { - 'O': ['a']}}} - - -def test_overwrite_existing(core): - test_schema = { - 'edge 1': { - '_type': 'edge', - '_inputs': { - 'I': 'float'}, - '_outputs': { - 'O': 'float'}}} - - test_state = { - 'a': 11.111, - 'edge 1': { - 'inputs': { - 'I': ['a']}, - 'outputs': { - 'O': ['a']}}} - - filled = core.fill( - test_schema, - test_state) - - assert filled == { - 'a': 11.111, - 'edge 1': { - 'inputs': { - 'I': ['a']}, - 'outputs': { - 'O': ['a']}}} - - -def test_fill_from_parse(core): - test_schema = { - 'edge 1': 'edge[I:float,O:float]'} - - test_state = { - 'edge 1': { - 'inputs': { - 'I': ['a']}, - 'outputs': { - 'O': ['a']}}} - - filled = core.fill( - test_schema, - test_state) - - assert filled == { - 'a': 0.0, - 'edge 1': { - 'inputs': { - 'I': ['a']}, - 'outputs': { - 'O': ['a']}}} - - -# def test_fill_in_disconnected_port(core): -# test_schema = { -# 'edge1': { -# '_type': 'edge', -# '_ports': { -# '1': {'_type': 'float'}}}} - -# test_state = {} - - -# def test_fill_type_mismatch(core): -# test_schema = { -# 'a': {'_type': 'integer', '_value': 2}, -# 'edge1': { -# '_type': 'edge', -# '_ports': { -# '1': {'_type': 'float'}, -# '2': {'_type': 'float'}}, -# 'wires': { -# '1': ['..', 'a'], -# '2': ['a']}, -# 'a': 5}} - - -# def test_edge_type_mismatch(core): -# test_schema = { -# 'edge1': { -# '_type': 'edge', -# '_ports': { -# '1': {'_type': 'float'}}, -# 'wires': { -# '1': ['..', 'a']}}, -# 'edge2': { -# '_type': 'edge', -# '_ports': { -# '1': {'_type': 'integer'}}, -# 'wires': { -# '1': ['..', 'a']}}} - - -def test_establish_path(core): - tree = {} - destination = establish_path( - tree, - ('some', - 'where', - 'deep', - 'inside', - 'lives', - 'a', - 'tiny', - 'creature', - 'made', - 'of', - 'light')) - - assert tree['some']['where']['deep']['inside']['lives']['a']['tiny']['creature']['made']['of']['light'] == destination - - -def test_fill_ports(core): - cell_state = { - 'cell1': { - 'nucleus': { - 'transcription': { - '_type': 'edge', - 'inputs': {'DNA': ['chromosome']}, - 'outputs': { - 'RNA': [ '..', 'cytoplasm']}}}}} - - schema, state = core.complete( - {}, - cell_state) - - assert 'chromosome' in schema['cell1']['nucleus'] - - -def test_expected_schema(core): - # equivalent to previous schema: - - # expected = { - # 'store1': { - # 'store1.1': { - # '_value': 1.1, - # '_type': 'float', - # }, - # 'store1.2': { - # '_value': 2, - # '_type': 'integer', - # }, - # 'process1': { - # '_ports': { - # 'port1': {'_type': 'type'}, - # 'port2': {'_type': 'type'}, - # }, - # '_wires': { - # 'port1': 'store1.1', - # 'port2': 'store1.2', - # } - # }, - # 'process2': { - # '_ports': { - # 'port1': {'_type': 'type'}, - # 'port2': {'_type': 'type'}, - # }, - # '_wires': { - # 'port1': 'store1.1', - # 'port2': 'store1.2', - # } - # }, - # }, - # 'process3': { - # '_wires': { - # 'port1': 'store1', - # } - # } - # } - - dual_process_schema = { - 'process1': 'edge[input1:float|input2:integer,output1:float|output2:integer]', - 'process2': { - '_type': 'edge', - '_inputs': { - 'input1': 'float', - 'input2': 'integer'}, - '_outputs': { - 'output1': 'float', - 'output2': 'integer'}}} - - core.register( - 'dual_process', - dual_process_schema, - ) - - test_schema = { - # 'store1': 'process1.edge[port1.float|port2.int]|process2[port1.float|port2.int]', - 'store1': 'dual_process', - 'process3': 'edge[input_process:dual_process,output_process:dual_process]'} - - test_state = { - 'store1': { - 'process1': { - 'inputs': { - 'input1': ['store1.1'], - 'input2': ['store1.2']}, - 'outputs': { - 'output1': ['store2.1'], - 'output2': ['store2.2']}}, - 'process2': { - 'inputs': { - 'input1': ['store2.1'], - 'input2': ['store2.2']}, - 'outputs': { - 'output1': ['store1.1'], - 'output2': ['store1.2']}}}, - 'process3': { - 'inputs': { - 'input_process': ['store1']}, - 'outputs': { - 'output_process': ['store1']}}} - - outcome = core.fill(test_schema, test_state) - - assert outcome == { - 'process3': { - 'inputs': { - 'input_process': ['store1']}, - 'outputs': { - 'output_process': ['store1']}}, - 'store1': { - 'process1': { - 'inputs': { - 'input1': ['store1.1'], - 'input2': ['store1.2']}, - 'outputs': { - 'output1': ['store2.1'], - 'output2': ['store2.2']}}, - 'process2': { - 'inputs': {'input1': ['store2.1'], - 'input2': ['store2.2']}, - 'outputs': {'output1': ['store1.1'], - 'output2': ['store1.2']}}, - 'store1.1': 0.0, - 'store1.2': 0, - 'store2.1': 0.0, - 'store2.2': 0}} - - -def test_link_place(core): - # TODO: this form is more fundamental than the compressed/inline dict form, - # and we should probably derive that from this form - - bigraph = { - 'nodes': { - 'v0': 'integer', - 'v1': 'integer', - 'v2': 'integer', - 'v3': 'integer', - 'v4': 'integer', - 'v5': 'integer', - 'e0': 'edge[e0-0:int|e0-1:int|e0-2:int]', - 'e1': { - '_type': 'edge', - '_ports': { - 'e1-0': 'integer', - 'e2-0': 'integer'}}, - 'e2': { - '_type': 'edge[e2-0:int|e2-1:int|e2-2:int]'}}, - - 'place': { - 'v0': None, - 'v1': 'v0', - 'v2': 'v0', - 'v3': 'v2', - 'v4': None, - 'v5': 'v4', - 'e0': None, - 'e1': None, - 'e2': None}, - - 'link': { - 'e0': { - 'e0-0': 'v0', - 'e0-1': 'v1', - 'e0-2': 'v4'}, - 'e1': { - 'e1-0': 'v3', - 'e1-1': 'v1'}, - 'e2': { - 'e2-0': 'v3', - 'e2-1': 'v4', - 'e2-2': 'v5'}}, - - 'state': { - 'v0': '1', - 'v1': '1', - 'v2': '2', - 'v3': '3', - 'v4': '5', - 'v5': '8', - 'e0': { - 'wires': { - 'e0-0': 'v0', - 'e0-1': 'v1', - 'e0-2': 'v4'}}, - 'e1': { - 'wires': { - 'e1-0': 'v3', - 'e1-1': 'v1'}}, - 'e2': { - 'e2-0': 'v3', - 'e2-1': 'v4', - 'e2-2': 'v5'}}} - - placegraph = { # schema - 'v0': { - 'v1': int, - 'v2': { - 'v3': int}}, - 'v4': { - 'v5': int}, - 'e0': 'edge', - 'e1': 'edge', - 'e2': 'edge'} - - hypergraph = { # edges - 'e0': { - 'e0-0': 'v0', - 'e0-1': 'v1', - 'e0-2': 'v4'}, - 'e1': { - 'e1-0': 'v3', - 'e1-1': 'v1'}, - 'e2': { - 'e2-0': 'v3', - 'e2-1': 'v4', - 'e2-2': 'v5'}} - - merged = { - 'v0': { - 'v1': {}, - 'v2': { - 'v3': {}}}, - 'v4': { - 'v5': {}}, - 'e0': { - 'wires': { - 'e0.0': ['v0'], - 'e0.1': ['v0', 'v1'], - 'e0.2': ['v4']}}, - 'e1': { - 'wires': { - 'e0.0': ['v0', 'v2', 'v3'], - 'e0.1': ['v0', 'v1']}}, - 'e2': { - 'wires': { - 'e0.0': ['v0', 'v2', 'v3'], - 'e0.1': ['v4'], - 'e0.2': ['v4', 'v5']}}} - - result = core.link_place(placegraph, hypergraph) - # assert result == merged - - -def test_units(core): - schema_length = { - 'distance': {'_type': 'length'}} - - state = {'distance': 11 * units.meter} - update = {'distance': -5 * units.feet} - - new_state = core.apply( - schema_length, - state, - update - ) - - assert new_state['distance'] == 9.476 * units.meter - - -def test_unit_conversion(core): - # mass * length ^ 2 / second ^ 2 - - units_schema = { - 'force': 'length^2*mass/time^2'} - - force_units = units.meter ** 2 * units.kg / units.second ** 2 - - instance = { - 'force': 3.333 * force_units} - - -def test_serialize_deserialize(core): - schema = { - 'edge1': { - # '_type': 'edge[1:int|2:float|3:string|4:tree[int]]', - '_type': 'edge', - '_outputs': { - '1': 'integer', - '2': 'float', - '3': 'string', - '4': 'tree[integer]'}}, - 'a0': { - 'a0.0': 'integer', - 'a0.1': 'float', - 'a0.2': { - 'a0.2.0': 'string'}}, - 'a1': 'tree[integer]'} - - instance = { - 'edge1': { - 'outputs': { - '1': ['a0', 'a0.0'], - '2': ['a0', 'a0.1'], - '3': ['a0', 'a0.2', 'a0.2.0'], - '4': ['a1']}}, - 'a1': { - 'branch1': { - 'branch2': 11, - 'branch3': 22}, - 'branch4': 44}} - - instance = core.fill(schema, instance) - - encoded = core.serialize(schema, instance) - decoded = core.deserialize(schema, encoded) - - assert instance == decoded - - -# is this a lens? -def test_project(core): - schema = { - 'edge1': { - # '_type': 'edge[1:int|2:float|3:string|4:tree[int]]', - # '_type': 'edge', - '_type': 'edge', - '_inputs': { - '1': 'integer', - '2': 'float', - '3': 'string', - 'inner': { - 'chamber': 'tree[integer]'}, - '4': 'tree[integer]'}, - '_outputs': { - '1': 'integer', - '2': 'float', - '3': 'string', - 'inner': { - 'chamber': 'tree[integer]'}, - '4': 'tree[integer]'}}, - 'a0': { - 'a0.0': 'integer', - 'a0.1': 'float', - 'a0.2': { - 'a0.2.0': 'string'}}, - 'a1': { - '_type': 'tree[integer]'}} - - path_format = { - '1': 'a0>a0.0', - '2': 'a0>a0.1', - '3': 'a0>a0.2>a0.2.0'} - - # TODO: support separate schema/instance, and - # instances with '_type' and type parameter keys - # TODO: support overriding various type methods - instance = { - 'a0': { - 'a0.0': 11}, - 'edge1': { - 'inputs': { - '1': ['a0', 'a0.0'], - '2': ['a0', 'a0.1'], - '3': ['a0', 'a0.2', 'a0.2.0'], - 'inner': { - 'chamber': ['a1', 'a1.0']}, - '4': ['a1']}, - 'outputs': { - '1': ['a0', 'a0.0'], - '2': ['a0', 'a0.1'], - '3': ['a0', 'a0.2', 'a0.2.0'], - 'inner': { - 'chamber': { - 'X': ['a1', 'a1.0', 'Y']}}, - '4': ['a1']}}, - 'a1': { - 'a1.0': { - 'X': 555}, - 'branch1': { - 'branch2': 11, - 'branch3': 22}, - 'branch4': 44}} - - instance = core.fill(schema, instance) - - states = core.view_edge( - schema, - instance, - ['edge1']) - - update = core.project_edge( - schema, - instance, - ['edge1'], - states) - - assert update == { - 'a0': { - 'a0.0': 11, - 'a0.1': 0.0, - 'a0.2': { - 'a0.2.0': ''}}, - 'a1': { - 'a1.0': { - 'X': 555, - 'Y': {}}, - 'branch1': { - 'branch2': 11, - 'branch3': 22}, - 'branch4': 44}} - - # TODO: make sure apply does not mutate instance - updated_instance = core.apply( - schema, - instance, - update) - - add_update = { - '4': { - 'branch6': 111, - 'branch1': { - '_add': { - 'branch7': 4444, - 'branch8': 555, - }, - '_remove': ['branch2']}, - '_add': { - 'branch5': 55}, - '_remove': ['branch4']}} - - inverted_update = core.project_edge( - schema, - updated_instance, - ['edge1'], - add_update) - - modified_branch = core.apply( - schema, - updated_instance, - inverted_update) - - assert modified_branch == { - 'a0': { - 'a0.0': 22, - 'a0.1': 0.0, - 'a0.2': { - 'a0.2.0': ''}}, - 'edge1': {'inputs': {'1': ['a0', 'a0.0'], - '2': ['a0', 'a0.1'], - '3': ['a0', 'a0.2', 'a0.2.0'], - 'inner': { - 'chamber': ['a1', 'a1.0']}, - '4': ['a1']}, - 'outputs': {'1': ['a0', 'a0.0'], - '2': ['a0', 'a0.1'], - '3': ['a0', 'a0.2', 'a0.2.0'], - 'inner': { - 'chamber': { - 'X': ['a1', 'a1.0', 'Y']}}, - '4': ['a1']}}, - 'a1': { - 'a1.0': { - 'X': 1110, - 'Y': {}}, - 'branch1': { - 'branch3': 44, - 'branch7': 4444, - 'branch8': 555,}, - 'branch6': 111, - 'branch5': 55}} - - -def test_check(core): - assert core.check('float', 1.11) - assert core.check({'b': 'float'}, {'b': 1.11}) - - -def test_inherits_from(core): - assert core.inherits_from( - 'float', - 'number') - - assert core.inherits_from( - 'tree[float]', - 'tree[number]') - - assert core.inherits_from( - 'tree[path]', - 'tree[list[string]]') - - assert not core.inherits_from( - 'tree[path]', - 'tree[list[number]]') - - assert not core.inherits_from( - 'tree[float]', - 'tree[string]') - - assert not core.inherits_from( - 'tree[float]', - 'list[float]') - - assert core.inherits_from({ - 'a': 'float', - 'b': 'schema'}, { - - 'a': 'number', - 'b': 'tree'}) - - assert not core.inherits_from({ - 'a': 'float', - 'b': 'schema'}, { - - 'a': 'number', - 'b': 'number'}) - - -def test_resolve_schemas(core): - resolved = core.resolve_schemas({ - 'a': 'float', - 'b': 'map[list[string]]'}, { - 'a': 'number', - 'b': 'map[path]', - 'c': 'string'}) - - assert resolved['a']['_type'] == 'float' - assert resolved['b']['_value']['_type'] == 'path' - assert resolved['c']['_type'] == 'string' - - raises_on_incompatible_schemas = False - try: - core.resolve_schemas({ - 'a': 'string', - 'b': 'map[list[string]]'}, { - 'a': 'number', - 'b': 'map[path]', - 'c': 'string'}) - except: - raises_on_incompatible_schemas = True - - assert raises_on_incompatible_schemas - - -def test_apply_schema(core): - current = { - 'a': 'number', - 'b': 'map[path]', - 'd': ('float', 'number', 'list[string]')} - - update = { - 'a': 'float', - 'b': 'map[list[string]]', - 'c': 'string', - 'd': ('number', 'float', 'path')} - - applied = apply_schema( - 'schema', - current, - update, - core) - - assert applied['a']['_type'] == 'float' - assert applied['b']['_value']['_type'] == 'path' - assert applied['c']['_type'] == 'string' - assert applied['d']['_0']['_type'] == 'float' - assert applied['d']['_1']['_type'] == 'float' - assert applied['d']['_2']['_type'] == 'path' - - -def apply_foursquare(schema, current, update, core): - if isinstance(current, bool) or isinstance(update, bool): - return update - else: - for key, value in update.items(): - current[key] = apply_foursquare( - schema, - current[key], - value, - core) - - return current - - -def test_foursquare(core): - foursquare_schema = { - '_apply': apply_foursquare, - '00': 'boolean~foursquare', - '01': 'boolean~foursquare', - '10': 'boolean~foursquare', - '11': 'boolean~foursquare'} - - core.register( - 'foursquare', - foursquare_schema) - - example = { - '00': True, - '01': False, - '10': False, - '11': { - '00': True, - '01': False, - '10': False, - '11': { - '00': True, - '01': False, - '10': False, - '11': { - '00': True, - '01': False, - '10': False, - '11': { - '00': True, - '01': False, - '10': False, - '11': { - '00': True, - '01': False, - '10': False, - '11': False}}}}}} - - assert core.check( - 'foursquare', - example) - - example['10'] = 5 - - assert not core.check( - 'foursquare', - example) - - update = { - '01': True, - '11': { - '01': True, - '11': { - '11': True, - '10': { - '10': { - '00': True, - '11': False}}}}} - - result = core.apply( - 'foursquare', - example, - update) - - assert result == { - '00': True, - '01': True, - '10': 5, - '11': {'00': True, - '01': True, - '10': False, - '11': {'00': True, - '01': False, - '10': { - '10': { - '00': True, - '11': False}}, - '11': True}}} - - -def test_add_reaction(core): - single_node = { - 'environment': { - '_type': 'compartment', - 'counts': {'A': 144}, - 'inner': { - '0': { - 'counts': {'A': 13}, - 'inner': {}}}}} - - add_config = { - 'path': ['environment', 'inner'], - 'add': { - '1': { - 'counts': { - 'A': 8}}}} - - schema, state = core.infer_schema( - {}, - single_node) - - assert '0' in state['environment']['inner'] - assert '1' not in state['environment']['inner'] - - result = core.apply( - schema, - state, { - '_react': { - 'add': add_config}}) - - # '_react': { - # 'reaction': 'add', - # 'config': add_config}}) - - assert '0' in result['environment']['inner'] - assert '1' in result['environment']['inner'] - - -def test_remove_reaction(core): - single_node = { - 'environment': { - '_type': 'compartment', - 'counts': {'A': 144}, - 'inner': { - '0': { - 'counts': {'A': 13}, - 'inner': {}}, - '1': { - 'counts': {'A': 13}, - 'inner': {}}}}} - - remove_config = { - 'path': ['environment', 'inner'], - 'remove': ['0']} - - schema, state = core.infer_schema( - {}, - single_node) - - assert '0' in state['environment']['inner'] - assert '1' in state['environment']['inner'] - - result = core.apply( - schema, - state, { - '_react': { - 'remove': remove_config}}) - - assert '0' not in result['environment']['inner'] - assert '1' in state['environment']['inner'] - - -def test_replace_reaction(core): - single_node = { - 'environment': { - '_type': 'compartment', - 'counts': {'A': 144}, - 'inner': { - '0': { - 'counts': {'A': 13}, - 'inner': {}}, - '1': { - 'counts': {'A': 13}, - 'inner': {}}}}} - - # replace_config = { - # 'path': ['environment', 'inner'], - # 'before': {'0': {'A': '?1'}}, - # 'after': { - # '2': { - # 'counts': { - # 'A': {'function': 'divide', 'arguments': ['?1', 0.5], }}}, - # '3': { - # 'counts': { - # 'A': '@1'}}}} - - replace_config = { - 'path': ['environment', 'inner'], - 'before': {'0': {}}, - 'after': { - '2': { - 'counts': { - 'A': 3}}, - '3': { - 'counts': { - 'A': 88}}}} - - schema, state = core.infer_schema( - {}, - single_node) - - assert '0' in state['environment']['inner'] - assert '1' in state['environment']['inner'] - - result = core.apply( - schema, - state, { - '_react': { - 'replace': replace_config}}) - - assert '0' not in result['environment']['inner'] - assert '1' in result['environment']['inner'] - assert '2' in result['environment']['inner'] - assert '3' in result['environment']['inner'] - - -def test_reaction(core): - single_node = { - 'environment': { - 'counts': {}, - 'inner': { - '0': { - 'counts': {}}}}} - - # TODO: compartment type ends up as 'any' at leafs? - - # TODO: come at divide reaction from the other side: - # ie make a call for it, then figure out what the - # reaction needs to be - def divide_reaction(container, mother, divider): - daughters = divider(mother) - - return { - 'redex': mother, - 'reactum': daughters} - - embedded_tree = { - 'environment': { - '_type': 'compartment', - 'counts': {}, - 'inner': { - 'agent1': { - '_type': 'compartment', - 'counts': {}, - 'inner': { - 'agent2': { - '_type': 'compartment', - 'counts': {}, - 'inner': {}, - 'transport': { - 'wires': { - 'outer': ['..', '..'], - 'inner': ['inner']}}}}, - 'transport': { - 'wires': { - 'outer': ['..', '..'], - 'inner': ['inner']}}}}}} - - - mother_tree = { - 'environment': { - '_type': 'compartment', - 'counts': { - 'A': 15}, - 'inner': { - 'mother': { - '_type': 'compartment', - 'counts': { - 'A': 5}}}}} - - divide_react = { - '_react': { - 'redex': { - 'mother': { - 'counts': '@counts'}}, - 'reactum': { - 'daughter1': { - 'counts': '@daughter1_counts'}, - 'daughter2': { - 'counts': '@daughter2_counts'}}, - 'calls': [{ - 'function': 'divide_counts', - 'arguments': ['@counts', [0.5, 0.5]], - 'bindings': ['@daughter1_counts', '@daughter2_counts']}]}} - - divide_update = { - '_react': { - 'reaction': 'divide_counts', - 'config': { - 'id': 'mother', - 'state_key': 'counts', - 'daughters': [ - {'id': 'daughter1', 'ratio': 0.3}, - {'id': 'daughter2', 'ratio': 0.7}]}}} - - divide_update_concise = { - '_react': { - 'divide_counts': { - 'id': 'mother', - 'state_key': 'counts', - 'daughters': [ - {'id': 'daughter1', 'ratio': 0.3}, - {'id': 'daughter2', 'ratio': 0.7}]}}} - - -def test_map_type(core): - schema = 'map[integer]' - - state = { - 'a': 12, - 'b': 13, - 'c': 15, - 'd': 18} - - update = { - 'b': 44, - 'd': 111} - - assert core.check(schema, state) - assert core.check(schema, update) - assert not core.check(schema, 15) - - result = core.apply( - schema, - state, - update) - - assert result['a'] == 12 - assert result['b'] == 57 - assert result['d'] == 129 - - encode = core.serialize(schema, update) - assert encode['d'] == '111' - - decode = core.deserialize(schema, encode) - assert decode == update - - -def test_tree_type(core): - schema = 'tree[maybe[integer]]' - - state = { - 'a': 12, - 'b': 13, - 'c': { - 'e': 5555, - 'f': 111}, - 'd': None} - - update = { - 'a': None, - 'c': { - 'e': 88888, - 'f': 2222, - 'G': None}, - 'd': 111} - - assert core.check(schema, state) - assert core.check(schema, update) - assert core.check(schema, 15) - assert core.check(schema, None) - assert core.check(schema, {'c': {'D': None, 'e': 11111}}) - assert not core.check(schema, 'yellow') - assert not core.check(schema, {'a': 5, 'b': 'green'}) - assert not core.check(schema, {'c': {'D': False, 'e': 11111}}) - - result = core.apply( - schema, - state, - update) - - assert result['a'] == None - assert result['b'] == 13 - assert result['c']['f'] == 2333 - assert result['d'] == 111 - - encode = core.serialize(schema, update) - assert encode['a'] == NONE_SYMBOL - assert encode['d'] == '111' - - decode = core.deserialize(schema, encode) - assert decode == update - - -def test_maybe_type(core): - schema = 'map[maybe[integer]]' - - state = { - 'a': 12, - 'b': 13, - 'c': None, - 'd': 18} - - update = { - 'a': None, - 'c': 44, - 'd': 111} - - assert core.check(schema, state) - assert core.check(schema, update) - assert not core.check(schema, 15) - - result = core.apply( - schema, - state, - update) - - assert result['a'] == None - assert result['b'] == 13 - assert result['c'] == 44 - assert result['d'] == 129 - - encode = core.serialize(schema, update) - assert encode['a'] == NONE_SYMBOL - assert encode['d'] == '111' - - decode = core.deserialize(schema, encode) - assert decode == update - - -def test_tuple_type(core): - schema = { - '_type': 'tuple', - '_type_parameters': ['0', '1', '2'], - '_0': 'string', - '_1': 'int', - '_2': 'map[maybe[float]]'} - - schema = ('string', 'int', 'map[maybe[float]]') - schema = 'tuple[string,int,map[maybe[float]]]' - schema = 'string|integer|map[maybe[float]]' - - state = ( - 'aaaaa', - 13, { - 'a': 1.1, - 'b': None}) - - update = ( - 'bbbbbb', - 10, { - 'a': 33.33, - 'b': 4.44444}) - - assert core.check(schema, state) - assert core.check(schema, update) - assert not core.check(schema, 15) - - result = core.apply( - schema, - state, - update) - - assert len(result) == 3 - assert result[0] == update[0] - assert result[1] == 23 - assert result[2]['a'] == 34.43 - assert result[2]['b'] == update[2]['b'] - - encode = core.serialize(schema, state) - assert encode[2]['b'] == NONE_SYMBOL - assert encode[1] == '13' - - decode = core.deserialize(schema, encode) - assert decode == state - - tuple_type = core.access('(3|4|10)') - assert '_2' in tuple_type - assert tuple_type['_2'] == '10' - - tuple_type = core.access('tuple[9,float,7]') - assert '_2' in tuple_type - assert tuple_type['_2'] == '7' - - - -def test_union_type(core): - schema = { - '_type': 'union', - '_type_parameters': ['0', '1', '2'], - '_0': 'string', - '_1': 'integer', - '_2': 'map[maybe[float]]'} - - schema = 'string~integer~map[maybe[float]]' - - state = { - 'a': 1.1, - 'b': None} - - update = { - 'a': 33.33, - 'b': 4.44444} - - assert core.check(schema, state) - assert core.check(schema, update) - assert core.check(schema, 15) - - wrong_state = { - 'a': 1.1, - 'b': None} - - wrong_update = 'a different type' - - assert core.check(schema, wrong_state) - assert core.check(schema, wrong_update) - - # TODO: deal with union apply of different types - - result = core.apply( - schema, - state, - update) - - assert result['a'] == 34.43 - assert result['b'] == update['b'] - - encode = core.serialize(schema, state) - assert encode['b'] == NONE_SYMBOL - - decode = core.deserialize(schema, encode) - assert decode == state - - -def test_union_values(core): - schema = 'map[integer~string~map[maybe[float]]]' - - state = { - 'a': 'bbbbb', - 'b': 15} - - update = { - 'a': 'aaaaa', - 'b': 22} - - assert core.check(schema, state) - assert core.check(schema, update) - assert not core.check(schema, 15) - - result = core.apply( - schema, - state, - update) - - assert result['a'] == 'aaaaa' - assert result['b'] == 37 - - encode = core.serialize(schema, state) - decode = core.deserialize(schema, encode) - - assert decode == state - - -def test_array_type(core): - shape = (3, 4, 10) - shape_representation = core.representation(shape) - shape_commas = ','.join([ - str(x) - for x in shape]) - - schema = { - '_type': 'map', - '_value': { - '_type': 'array', - # '_shape': '(3|4|10)', - '_shape': shape_representation, - '_data': 'float'}} - - schema = f'map[array[tuple[{shape_commas}],float]]' - schema = f'map[array[{shape_representation},float]]' - - state = { - 'a': np.zeros(shape), - 'b': np.ones(shape)} - - update = { - 'a': np.full(shape, 5.555), - 'b': np.full(shape, 9.999)} - - assert core.check(schema, state) - assert core.check(schema, update) - assert not core.check(schema, 15) - - result = core.apply( - schema, - state, - update) - - assert result['a'][0, 0, 0] == 5.555 - assert result['b'][0, 0, 0] == 10.999 - - encode = core.serialize(schema, state) - assert encode['b']['shape'] == list(shape) - assert encode['a']['data'] == 'float' - - decode = core.deserialize(schema, encode) - - for key in state: - assert np.equal( - decode[key], - state[key]).all() - - found = core.find( - schema) - - default = core.default( - found['_value']) - - assert default.shape == shape - - -def test_infer_edge(core): - initial_schema = {} - initial_state = { - 'fade': { - '_type': 'edge', - '_inputs': { - 'yellow': 'array[(3|4|10),float]'}, - '_outputs': { - 'green': 'array[(11|5|8),float]'}, - 'inputs': { - 'yellow': ['yellow']}, - 'outputs': { - 'green': ['green']}}} - - update = { - 'yellow': np.ones((3, 4, 10)), - 'fade': { - 'inputs': { - 'yellow': ['red']}, - 'outputs': { - 'green': ['green', 'green', 'green']}}} - - schema, state = core.complete( - initial_schema, - initial_state) - - assert core.check(schema, state) - assert not core.check(schema, 15) - - result = core.apply( - schema, - state, - update) - - assert result['yellow'][0, 0, 0] == 1.0 - assert result['fade']['inputs']['yellow'] == ['red'] - - encode = core.serialize(schema, state) - decode = core.deserialize(schema, encode) - - assert np.equal( - decode['yellow'], - state['yellow']).all() - - -def test_edge_type(core): - schema = { - 'fade': { - '_type': 'edge', - '_inputs': { - 'yellow': { - '_type': 'array', - '_shape': 'tuple(3,4,10)', - '_data': 'float'}}, - '_outputs': { - 'green': { - '_type': 'array', - '_shape': 'tuple(11,5,8)', - '_data': 'float'}}}} - - initial_schema = { - 'fade': 'edge[yellow:array[(3|4|10),float],green:array[(11|5|8),float]]'} - - initial_state = { - # 'yellow': np.zeros((3, 4, 10)), - # 'green': np.ones((11, 5, 8)), - 'fade': { - 'inputs': { - 'yellow': ['yellow']}, - 'outputs': { - 'green': ['green']}}} - - schema, state = core.complete( - initial_schema, - initial_state) - - update = { - 'yellow': np.ones((3, 4, 10)), - 'fade': { - 'inputs': { - 'yellow': ['red']}, - 'outputs': { - 'green': ['green', 'green', 'green']}}} - - assert core.check(schema, state) - assert not core.check(schema, 15) - - result = core.apply( - schema, - state, - update) - - assert result['yellow'][0, 0, 0] == 1.0 - assert result['fade']['inputs']['yellow'] == ['red'] - - encode = core.serialize(schema, state) - decode = core.deserialize(schema, encode) - - assert np.equal( - decode['yellow'], - state['yellow']).all() - - -def test_edge_complete(core): - edge_schema = { - '_type': 'edge', - '_inputs': { - 'concentration': 'float', - 'field': 'map[boolean]'}, - '_outputs': { - 'target': 'boolean', - # 'inner': { - # 'nested': 'boolean'}, - 'total': 'integer', - 'delta': 'float'}} - - edge_state = { - 'inputs': { - 'concentration': ['molecules', 'glucose'], - 'field': ['states']}, - 'outputs': { - 'target': ['states', 'X'], - # 'inner': { - # 'nested': ['states', 'A']}, - 'total': ['emitter', 'total molecules'], - 'delta': ['molecules', 'glucose']}} - - # edge_state = { - # 'inputs': { - # 'concentration': ['..', 'molecules', 'glucose'], - # 'field': ['..', 'states']}, - # 'outputs': { - # 'target': ['..', 'states', 'X'], - # 'total': ['..', 'emitter', 'total molecules'], - # 'delta': ['..', 'molecules', 'glucose']}} - - full_schema, full_state = core.complete( - {'edge': edge_schema}, - {'edge': edge_state}) - - assert full_schema['states']['_type'] == 'map' - - - -def test_divide(core): - schema = { - 'a': 'tree[maybe[float]]', - 'b': 'float~list[string]', - 'c': { - 'd': 'map[edge[GGG:float,OOO:float]]', - 'e': 'array[(3|4|10),float]'}} - - state = { - 'a': { - 'x': { - 'oooo': None, - 'y': 1.1, - 'z': 33.33}, - 'w': 44.444}, - 'b': ['1', '11', '111', '1111'], - 'c': { - 'd': { - 'A': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'w']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'x', 'y']}}, - 'B': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'x', 'y']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'w']}}}, - 'e': np.zeros((3, 4, 10))}} - - divisions = 3 - division = core.fold( - schema, - state, - 'divide', - {'divisions': divisions}) - - assert len(division) == divisions - assert 'a' in division[0].keys() - assert len(division[1]['b']) == len(state['b']) - - -def test_merge(core): - current_schema = { - 'a': 'tree[maybe[float]]', - 'b': 'float~list[string]', - 'c': { - 'd': 'map[edge[GGG:float,OOO:float]]', - 'e': 'array[(3|4|10),float]'}} - - current_state = { - 'a': { - 'x': { - 'oooo': None, - 'y': 1.1, - 'z': 33.33}, - 'w': 44.444}, - 'b': ['1', '11', '111', '1111'], - 'c': { - 'd': { - 'A': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'w']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'x', 'y']}}, - 'B': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'x', 'y']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'w']}}}, - 'e': np.zeros((3, 4, 10))}} - - merge_state = { - 'z': 555.55, - 'b': ['333333333'], - 'a': { - 'x': { - 'x': { - 'o': 99999.11}}}} - - result = core.merge_recur( - current_schema, - current_state, - merge_state) - - assert result['z'] == merge_state['z'] - assert result['b'] == merge_state['b'] - assert result['a']['x']['x']['o'] == merge_state['a']['x']['x']['o'] - - -def test_bind(core): - current_schema = { - 'a': 'tree[maybe[float]]', - 'b': 'float~list[string]', - 'c': { - 'd': 'map[edge[GGG:float,OOO:float]]', - 'e': 'array[(3|4|10),float]'}} - - current_state = { - 'a': { - 'x': { - 'oooo': None, - 'y': 1.1, - 'z': 33.33}, - 'w': 44.444}, - 'b': ['1', '11', '111', '1111'], - 'c': { - 'd': { - 'A': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'w']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'x', 'y']}}, - 'B': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'x', 'y']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'w']}}}, - 'e': np.zeros((3, 4, 10))}} - - result_schema, result_state = core.bind( - current_schema, - current_state, - 'z', - 'float', - 555.55) - - assert result_schema['z']['_type'] == 'float' - assert result_state['z'] == 555.55 - - -def test_slice(core): - schema, state = core.slice( - 'map[float]', - {'aaaa': 55.555}, - ['aaaa']) - - schema, state = core.complete({}, { - 'top': { - '_type': 'tree[list[maybe[(float|integer)~string]]]', - 'AAAA': { - 'BBBB': { - 'CCCC': [ - (1.3, 5), - 'okay', - (55.555, 1), - None, - 'what', - 'is']}}, - 'DDDD': [ - (3333.1, 88), - 'in', - 'between', - (66.8, -3), - None, - None, - 'later']}}) - - float_schema, float_state = core.slice( - schema, - state, - ['top', 'AAAA', 'BBBB', 'CCCC', 2, 0]) - - assert float_schema['_type'] == 'float' - assert float_state == 55.555 - - assert core.slice( - schema, - state, - ['top', 'AAAA', 'BBBB', 'CCCC', 3])[1] is None - - -def test_set_slice(core): - float_schema, float_state = core.set_slice( - 'map[float]', - {'aaaa': 55.555}, - ['bbbbb'], - 'float', - 888.88888) - - assert float_schema['_type'] == 'map' - assert float_state['bbbbb'] == 888.88888 - - schema, state = core.complete({}, { - 'top': { - '_type': 'tree[list[maybe[(float|integer)~string]]]', - 'AAAA': { - 'BBBB': { - 'CCCC': [ - (1.3, 5), - 'okay', - (55.555, 1), - None, - 'what', - 'is']}}, - 'DDDD': [ - (3333.1, 88), - 'in', - 'between', - (66.8, -3), - None, - None, - 'later']}}) - - leaf_schema, leaf_state = core.set_slice( - schema, - state, - ['top', 'AAAA', 'BBBB', 'CCCC', 2, 1], - 'integer', - 33) - - assert core.slice( - leaf_schema, - leaf_state, [ - 'top', - 'AAAA', - 'BBBB', - 'CCCC', - 2, - 1])[1] == 33 - - -def from_state(dataclass, state): - if hasattr(dataclass, '__dataclass_fields__'): - fields = dataclass.__dataclass_fields__ - state = state or {} - - init = {} - for key, field in fields.items(): - substate = from_state( - field.type, - state.get(key)) - init[key] = substate - instance = dataclass(**init) - # elif get_origin(dataclass) in [typing.Union, typing.Mapping]: - # instance = state - else: - instance = state - # instance = dataclass(state) - - return instance - - -def test_dataclass(core): - simple_schema = { - 'a': 'float', - 'b': 'integer', - 'c': 'boolean', - 'x': 'string'} - - # TODO: accept just a string instead of only a path - simple_dataclass = core.dataclass( - simple_schema, - ['simple']) - - simple_state = { - 'a': 88.888, - 'b': 11111, - 'c': False, - 'x': 'not a string'} - - simple_new = simple_dataclass( - a=1.11, - b=33, - c=True, - x='what') - - simple_from = from_state( - simple_dataclass, - simple_state) - - nested_schema = { - 'a': { - 'a': { - 'a': 'float', - 'b': 'float'}, - 'x': 'float'}} - - nested_dataclass = core.dataclass( - nested_schema, - ['nested']) - - nested_state = { - 'a': { - 'a': { - 'a': 13.4444, - 'b': 888.88}, - 'x': 111.11111}} - - nested_new = data.nested( - data.nested_a( - data.nested_a_a( - a=222.22, - b=3.3333), - 5555.55)) - - nested_from = from_state( - nested_dataclass, - nested_state) - - complex_schema = { - 'a': 'tree[maybe[float]]', - 'b': 'float~list[string]', - 'c': { - 'd': 'map[edge[GGG:float,OOO:float]]', - 'e': 'array[(3|4|10),float]'}} - - complex_dataclass = core.dataclass( - complex_schema, - ['complex']) - - complex_state = { - 'a': { - 'x': { - 'oooo': None, - 'y': 1.1, - 'z': 33.33}, - 'w': 44.444}, - 'b': ['1', '11', '111', '1111'], - 'c': { - 'd': { - 'A': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'w']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'x', 'y']}}, - 'B': { - 'inputs': { - 'GGG': ['..', '..', 'a', 'x', 'y']}, - 'outputs': { - 'OOO': ['..', '..', 'a', 'w']}}}, - 'e': np.zeros((3, 4, 10))}} - - complex_from = from_state( - complex_dataclass, - complex_state) - - complex_dict = asdict(complex_from) - - # assert complex_dict == complex_state ? - - assert complex_from.a['x']['oooo'] is None - assert len(complex_from.c.d['A']['inputs']['GGG']) - assert isinstance(complex_from.c.e, np.ndarray) - - -def test_enum_type(core): - core.register( - 'planet', - 'enum[mercury,venus,earth,mars,jupiter,saturn,neptune]') - - # core.register('planet', { - # '_type': 'enum', - # '_type_parameters': ['0', '1', '2', '3', '4', '5', '6'], - # '_0': 'mercury', - # '_1': 'venus', - # '_2': 'earth', - # '_3': 'mars', - # '_4': 'jupiter', - # '_5': 'saturn', - # '_6': 'neptune'}) - - assert core.default('planet') == 'mercury' - - solar_system_schema = { - 'planets': 'map[planet]'} - - solar_system = { - 'planets': { - '3': 'earth', - '4': 'mars'}} - - jupiter_update = { - 'planets': { - '5': 'jupiter'}} - - pluto_update = { - 'planets': { - '7': 'pluto'}} - - assert core.check( - solar_system_schema, - solar_system) - - assert core.check( - solar_system_schema, - jupiter_update) - - assert not core.check( - solar_system_schema, - pluto_update) - - with_jupiter = core.apply( - solar_system_schema, - solar_system, - jupiter_update) - - try: - core.apply( - solar_system_schema, - solar_system, - pluto_update) - - assert False - except Exception as e: - print(e) - assert True - - -def test_map_schema(core): - schema = { - 'greetings': 'map[hello:string]', - 'edge': { - '_type': 'edge', - '_inputs': { - 'various': { - '_type': 'map', - '_value': { - 'world': 'string'}}}, - '_outputs': { - 'referent': 'float'}}} - - state = { - 'edge': { - 'inputs': { - 'various': ['greetings']}, - 'outputs': { - 'referent': ['where']}}, - - 'greetings': { - 'a': { - 'hello': 'yes'}, - 'b': { - 'hello': 'again', - 'world': 'present'}, - 'c': { - 'other': 'other'}}} - - complete_schema, complete_state = core.complete( - schema, - state) - - assert complete_schema['greetings']['_value']['hello']['_type'] == 'string' - assert complete_schema['greetings']['_value']['world']['_type'] == 'string' - - assert 'world' in complete_state['greetings']['a'] - assert complete_schema['greetings']['_value']['world']['_type'] == 'string' - - -def test_representation(core): - schema_examples = [ - 'map[float]', - '(string|float)', - 'tree[(a:float|b:map[string])]', - 'array[(5|11),maybe[integer]]', - 'edge[(x:float|y:tree[(z:float)]),(w:(float|float|float))]'] - - for example in schema_examples: - full_type = core.access(example) - representation = core.representation(full_type) - - if example != representation: - raise Exception(f'did not receive the same type after parsing and finding the representation:\n {example}\n {representation}') - - -def test_generate(core): - schema = { - 'A': 'float', - 'B': 'enum[one,two,three]', - 'units': 'map[float]'} - - state = { - 'C': { - '_type': 'enum[x,y,z]', - '_default': 'y'}, - 'units': { - 'x': 11.1111, - 'y': 22.833333}} - - generated_schema, generated_state = core.generate( - schema, - state) - - assert generated_state['A'] == 0.0 - assert generated_state['B'] == 'one' - assert generated_state['C'] == 'y' - assert generated_state['units']['y'] == 22.833333 - assert 'x' not in generated_schema['units'] - - -def test_edge_cycle(core): - empty_schema = {} - empty_state = {} - - A_schema = { - 'A': { - '_type': 'metaedge', - '_inputs': { - 'before': { - 'inputs': {'before': {'_default': ['B']}}, - 'outputs': {'after': {'_default': ['A']}}}}, - '_outputs': { - 'after': { - 'inputs': {'before': {'_default': ['A']}}, - 'outputs': {'after': {'_default': ['C']}}}}, - 'inputs': {'before': {'_default': ['C']}}, - 'outputs': {'after': {'_default': ['B']}}}} - - A_state = { - 'A': { - '_type': 'metaedge', - '_inputs': { - 'before': { - 'inputs': {'before': {'_default': ['B']}}, - 'outputs': {'after': {'_default': ['A']}}}}, - '_outputs': { - 'after': { - 'inputs': {'before': {'_default': ['A']}}, - 'outputs': {'after': {'_default': ['C']}}}}, - 'inputs': {'before': {'_default': ['C']}}, - 'outputs': {'after': {'_default': ['B']}}}} - - schema_from_schema, state_from_schema = core.generate( - A_schema, - empty_state) - - schema_from_state, state_from_state = core.generate( - empty_schema, - A_state) - - # print(diff(schema_from_schema, schema_from_state)) - # print(diff(state_from_schema, state_from_state)) - - if schema_from_schema != schema_from_state: - print(diff(schema_from_schema, schema_from_state)) - - if state_from_schema != state_from_state: - print(diff(state_from_schema, state_from_state)) - - assert schema_from_schema == schema_from_state - assert state_from_schema == state_from_state - - for key in ['A', 'B', 'C']: - for result in [schema_from_schema, state_from_schema, schema_from_state, state_from_state]: - assert key in result - - -def test_merge(core): - schema = { - 'A': 'float', - 'B': 'enum[one,two,three]', - 'units': 'map[float]'} - - state = { - 'C': { - '_type': 'enum[x,y,z]', - '_default': 'y'}, - 'units': { - 'x': 11.1111, - 'y': 22.833333}} - - generated_schema, generated_state = core.generate( - schema, - state) - - edge_state = { - '_type': 'edge', - '_inputs': { - 'input': 'float'}, - '_outputs': { - 'output': 'float'}, - 'inputs': { - 'input': ['A']}, - 'outputs': { - 'output': ['D']}} - - top_schema, top_state = core.merge( - generated_schema, - generated_state, - ['edge'], - {}, - edge_state) - - assert 'D' in top_state - assert top_schema['D']['_type'] == 'float' - - -if __name__ == '__main__': - core = TypeSystem() - core = register_test_types(core) - - test_reregister_type(core) - test_generate_default(core) - test_apply_update(core) - test_validate_schema(core) - test_fill_integer(core) - test_fill_cube(core) - test_establish_path(core) - test_overwrite_existing(core) - test_fill_in_missing_nodes(core) - test_fill_from_parse(core) - test_fill_ports(core) - test_expected_schema(core) - test_units(core) - test_serialize_deserialize(core) - test_project(core) - test_inherits_from(core) - test_apply_schema(core) - test_resolve_schemas(core) - test_add_reaction(core) - test_remove_reaction(core) - test_replace_reaction(core) - test_unit_conversion(core) - test_map_type(core) - test_tree_type(core) - test_maybe_type(core) - test_tuple_type(core) - test_array_type(core) - test_union_type(core) - test_union_values(core) - test_infer_edge(core) - test_edge_type(core) - test_edge_complete(core) - test_foursquare(core) - test_divide(core) - test_merge(core) - test_bind(core) - test_slice(core) - test_set_slice(core) - test_dataclass(core) - test_enum_type(core) - test_map_schema(core) - test_representation(core) - test_generate(core) - test_edge_cycle(core) - test_merge(core) From 0e41aa2600b5a028dfda1f448fed378c732a06b6 Mon Sep 17 00:00:00 2001 From: Eran Date: Fri, 6 Dec 2024 21:12:43 -0500 Subject: [PATCH 6/8] minor refactor --- bigraph_schema/type_system.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index eb3dab9..989b62c 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -156,7 +156,6 @@ def get_path(tree, path): else: return get_path(tree[head], path[1:]) - def remove_path(tree, path): """ Removes whatever subtree lives at the given path @@ -213,6 +212,14 @@ def type_parameters_for(schema): # Apply functions # --------------- +def accumulate(schema, current, update, core): + if current is None: + return update + if update is None: + return current + else: + return current + update + def apply_schema(schema, current, update, core): """ Apply an update to a schema, returning the new schema @@ -4368,15 +4375,6 @@ def interface(self): 'outputs': self.outputs()} -def accumulate(schema, current, update, core): - if current is None: - return update - if update is None: - return current - else: - return current + update - - def set_apply(schema, current, update, core): if isinstance(current, dict) and isinstance(update, dict): for key, value in update.items(): From d8b262e1abb7fb210bef5df2d5160afe5947e567 Mon Sep 17 00:00:00 2001 From: Eran Date: Fri, 6 Dec 2024 21:19:44 -0500 Subject: [PATCH 7/8] order type methods as set out in TYPE_FUNCTION_KEYS --- bigraph_schema/type_system.py | 1647 +++++++++++++++++---------------- 1 file changed, 825 insertions(+), 822 deletions(-) diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index 989b62c..e8d817e 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -454,39 +454,153 @@ def apply_array(schema, current, update, core): return current + update -# Sort functions -# -------------- -def sort_quote(core, schema, state): - return schema, state +# Check functions +# --------------- +def check_any(schema, state, core): + if isinstance(schema, dict): + for key, subschema in schema.items(): + if not key.startswith('_'): + if isinstance(state, dict): + if key in state: + check = core.check_state( + subschema, + state[key]) + + if not check: + return False + else: + return False + else: + return False + + return True + else: + return True + +def check_tuple(schema, state, core): + if not isinstance(state, (tuple, list)): + return False + + parameters = core.parameters_for(schema) + for parameter, element in zip(parameters, state): + if not core.check(parameter, element): + return False + + return True + +def check_union(schema, state, core): + found = find_union_type( + core, + schema, + state) + + return found is not None and len(found) > 0 + +def check_number(schema, state, core=None): + return isinstance(state, numbers.Number) + +def check_boolean(schema, state, core=None): + return isinstance(state, bool) + +def check_integer(schema, state, core=None): + return isinstance(state, int) and not isinstance(state, bool) + +def check_float(schema, state, core=None): + return isinstance(state, float) + +def check_string(schema, state, core=None): + return isinstance(state, str) + +def check_list(schema, state, core): + element_type = core.find_parameter( + schema, + 'element') + + if isinstance(state, list): + for element in state: + check = core.check( + element_type, + element) + + if not check: + return False + + return True + else: + return False + +def check_tree(schema, state, core): + leaf_type = core.find_parameter( + schema, + 'leaf') + + if isinstance(state, dict): + for key, value in state.items(): + check = core.check({ + '_type': 'tree', + '_leaf': leaf_type}, + value) + + if not check: + return core.check( + leaf_type, + value) + + return True + else: + return core.check(leaf_type, state) + +def check_map(schema, state, core=None): + value_type = core.find_parameter( + schema, + 'value') -def sort_any(core, schema, state): - if not isinstance(schema, dict): - schema = core.find(schema) if not isinstance(state, dict): - return schema, state + return False - merged_schema = {} - merged_state = {} + for key, substate in state.items(): + if not core.check(value_type, substate): + return False - for key in union_keys(schema, state): - if is_schema_key(key): - if key in state: - merged_schema[key] = core.merge_schemas( - schema.get(key, {}), - state[key]) - else: - merged_schema[key] = schema[key] - else: - subschema, merged_state[key] = core.sort( - schema.get(key, {}), - state.get(key, None)) - if subschema: - merged_schema[key] = subschema + return True - return merged_schema, merged_state +def check_ports(state, core, key): + return key in state and core.check( + 'wires', + state[key]) + +def check_edge(schema, state, core): + return isinstance(state, dict) and check_ports(state, core, 'inputs') and check_ports(state, core, 'outputs') + +def check_maybe(schema, state, core): + if state is None: + return True + else: + value_type = core.find_parameter( + schema, + 'value') + + return core.check(value_type, state) + +def check_array(schema, state, core): + shape_type = core.find_parameter( + schema, + 'shape') + + return isinstance(state, np.ndarray) and state.shape == array_shape(core, shape_type) # and state.dtype == bindings['data'] # TODO align numpy data types so we can validate the types of the arrays + +def check_enum(schema, state, core): + if not isinstance(state, str): + return False + + parameters = core.parameters_for(schema) + return state in parameters +def check_units(schema, state, core): + # TODO: expand this to check the actual units for compatibility + return isinstance(state, Quantity) # Fold functions # -------------- @@ -518,7 +632,6 @@ def fold_any(schema, state, method, values, core): return visit - def fold_tuple(schema, state, method, values, core): if not isinstance(state, (tuple, list)): return visit_method( @@ -547,7 +660,6 @@ def fold_tuple(schema, state, method, values, core): values, core) - def fold_union(schema, state, method, values, core): union_type = find_union_type( core, @@ -562,75 +674,153 @@ def fold_union(schema, state, method, values, core): return result +def fold_list(schema, state, method, values, core): + element_type = core.find_parameter( + schema, + 'element') -def resolve_any(schema, update, core): - schema = schema or {} - outcome = schema.copy() - - for key, subschema in update.items(): - if key == '_type' and key in outcome: - if outcome[key] != subschema: - if core.inherits_from(outcome[key], subschema): - continue - elif core.inherits_from(subschema, outcome[key]): - outcome[key] = subschema - else: - raise Exception(f'cannot resolve types when updating\ncurrent type: {schema}\nupdate type: {update}') + if core.check(element_type, state): + result = core.fold( + element_type, + state, + method, + values) - elif not key in outcome or type_parameter_key(update, key): - if subschema: - outcome[key] = subschema - else: - outcome[key] = core.resolve_schemas( - outcome.get(key), - subschema) + elif isinstance(state, list): + subresult = [ + fold_list( + schema, + element, + method, + values, + core) + for element in state] - return outcome + result = visit_method( + schema, + subresult, + method, + values, + core) -# def resolve_tree(schema, update, core): -# if isinstance(update, dict): -# leaf_schema = schema.get('_leaf', {}) + else: + raise Exception(f'state does not seem to be a list or an eelement:\n state: {state}\n schema: {schema}') -# if '_type' in update: -# if update['_type'] == 'map': -# value_schema = update.get('_value', {}) -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# value_schema) + return result -# elif update['_type'] == 'tree': -# for key, subschema in update.items(): -# if not key.startswith('_'): -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# subschema) -# else: -# leaf_schema = core.resolve_schemas( -# leaf_schema, -# update) -# schema['_leaf'] = leaf_schema -# else: -# for key, subupdate in +def fold_tree(schema, state, method, values, core): + leaf_type = core.find_parameter( + schema, + 'leaf') -# return schema + if core.check(leaf_type, state): + result = core.fold( + leaf_type, + state, + method, + values) -def resolve_path(path): - """ - Given a path that includes '..' steps, resolve the path to a canonical form - """ - resolve = [] + elif isinstance(state, dict): + subresult = {} - for step in path: - if step == '..': - if len(resolve) == 0: - raise Exception(f'cannot go above the top in path: "{path}"') + for key, branch in state.items(): + if key.startswith('_'): + subresult[key] = branch else: - resolve = resolve[:-1] - else: - resolve.append(step) + subresult[key] = fold_tree( + schema[key] if key in schema else schema, + branch, + method, + values, + core) + + result = visit_method( + schema, + subresult, + method, + values, + core) + + else: + raise Exception(f'state does not seem to be a tree or a leaf:\n state: {state}\n schema: {schema}') + + return result + + +def fold_map(schema, state, method, values, core): + value_type = core.find_parameter( + schema, + 'value') + + subresult = {} + + for key, value in state.items(): + subresult[key] = core.fold( + value_type, + value, + method, + values) + + result = visit_method( + schema, + subresult, + method, + values, + core) + + return result + + +def fold_maybe(schema, state, method, values, core): + value_type = core.find_parameter( + schema, + 'value') + + if state is None: + result = core.fold( + 'any', + state, + method, + values) + + else: + result = core.fold( + value_type, + state, + method, + values) + + return result + +def fold_enum(schema, state, method, values, core): + if not isinstance(state, (tuple, list)): + return visit_method( + schema, + state, + method, + values, + core) + else: + parameters = core.parameters_for(schema) + result = [] + for parameter, element in zip(parameters, state): + fold = core.fold( + parameter, + element, + method, + values) + result.append(fold) + + result = tuple(result) + + return visit_method( + schema, + result, + method, + values, + core) - return tuple(resolve) # Divide functions # ---------------- @@ -802,110 +992,360 @@ def divide_enum(schema, state, values, core): tuple([item[index] for item in state]) for index in range(divisions)] -# Default functions -# ----------------- - -def default_any(schema, core): - default = {} - - for key, subschema in schema.items(): - if not is_schema_key(key): - default[key] = core.default( - subschema) - return default +# Serialize functions +# ------------------- -def default_tuple(schema, core): - parts = [] - for parameter in schema['_type_parameters']: - subschema = schema[f'_{parameter}'] - part = core.default(subschema) - parts.append(part) +def serialize_any(schema, state, core): + if isinstance(state, dict): + tree = {} - return tuple(parts) + for key in non_schema_keys(schema): + encoded = core.serialize( + schema.get(key, schema), + state.get(key)) + tree[key] = encoded -def default_union(schema, core): - final_parameter = schema['_type_parameters'][-1] - subschema = schema[f'_{final_parameter}'] + return tree - return core.default(subschema) + else: + return str(state) -def default_tree(schema, core): - leaf_schema = core.find_parameter( +def serialize_union(schema, value, core): + union_type = find_union_type( + core, schema, - 'leaf') + value) - default = {} + return core.serialize( + union_type, + value) - non_schema_keys = [ - key - for key in schema - if not is_schema_key(key)] +def serialize_tuple(schema, value, core): + parameters = core.parameters_for(schema) + result = [] - if non_schema_keys: - base_schema = { - key: subschema - for key, subschema in schema.items() - if is_schema_key(key)} + for parameter, element in zip(parameters, value): + encoded = core.serialize( + parameter, + element) - for key in non_schema_keys: - subschema = core.merge_schemas( - base_schema, - schema[key]) + result.append(encoded) - subdefault = core.default( - subschema) + return tuple(result) - if subdefault: - default[key] = subdefault +def serialize_string(schema, value, core=None): + return value - return default +def serialize_boolean(schema, value: bool, core) -> str: + return str(value) -def default_array(schema, core): - data_schema = core.find_parameter( +def serialize_list(schema, value, core=None): + element_type = core.find_parameter( schema, - 'data') - - dtype = read_datatype( - data_schema) + 'element') - shape = read_shape( - schema['_shape']) + return [ + core.serialize( + element_type, + element) + for element in value] - return np.zeros( - shape, - dtype=dtype) +def serialize_tree(schema, value, core): + if isinstance(value, dict): + encoded = {} + for key, subvalue in value.items(): + encoded[key] = serialize_tree( + schema, + subvalue, + core) -def default_enum(schema, core): - parameter = schema['_type_parameters'][0] - return schema[f'_{parameter}'] + else: + leaf_type = core.find_parameter( + schema, + 'leaf') + if core.check(leaf_type, value): + encoded = core.serialize( + leaf_type, + value) + else: + raise Exception(f'trying to serialize a tree but unfamiliar with this form of tree: {value} - current schema:\n {pf(schema)}') -def default_edge(schema, core): - edge = {} - for key in schema: - if not is_schema_key(key): - edge[key] = core.default( - schema[key]) + return encoded - return edge +def serialize_units(schema, value, core): + return str(value) -# Slice functions -# --------------- +def serialize_maybe(schema, value, core): + if value is None: + return NONE_SYMBOL + else: + value_type = core.find_parameter( + schema, + 'value') -def slice_any(schema, state, path, core): - if not isinstance(path, (list, tuple)): - if path is None: - path = () - else: - path = [path] + return core.serialize( + value_type, + value) - if len(path) == 0: - return schema, state +def serialize_map(schema, value, core=None): + value_type = core.find_parameter( + schema, + 'value') - elif len(path) > 0: - head = path[0] - tail = path[1:] + return { + key: core.serialize( + value_type, + subvalue) if not is_schema_key(key) else subvalue + for key, subvalue in value.items()} + + +def serialize_edge(schema, value, core): + return value + +def serialize_enum(schema, value, core): + return value + +def serialize_schema(schema, state, core): + return state + +def serialize_array(schema, value, core): + """ Serialize numpy array to list """ + + if isinstance(value, dict): + return value + elif isinstance(value, str): + import ipdb; ipdb.set_trace() + else: + array_data = 'string' + dtype = value.dtype.name + if dtype.startswith('int'): + array_data = 'integer' + elif dtype.startswith('float'): + array_data = 'float' + + return { + 'list': value.tolist(), + 'data': array_data, + 'shape': list(value.shape)} + +# Deserialize functions +# --------------------- + +def deserialize_any(schema, state, core): + if isinstance(state, dict): + tree = {} + + for key, value in state.items(): + if is_schema_key(key): + decoded = value + else: + decoded = core.deserialize( + schema.get(key, 'any'), + value) + + tree[key] = decoded + + for key in non_schema_keys(schema): + if key not in tree: + # if key not in state: + # decoded = core.default( + # schema[key]) + # else: + if key in state: + decoded = core.deserialize( + schema[key], + state[key]) + + tree[key] = decoded + + return tree + + else: + return state + +def deserialize_tuple(schema, state, core): + parameters = core.parameters_for(schema) + result = [] + + if isinstance(state, str): + if (state[0] == '(' and state[-1] == ')') or (state[0] == '[' and state[-1] == ']'): + state = state[1:-1].split(',') + else: + return None + + for parameter, code in zip(parameters, state): + element = core.deserialize( + parameter, + code) + + result.append(element) + + return tuple(result) + +def deserialize_union(schema, encoded, core): + if encoded == NONE_SYMBOL: + return None + else: + parameters = core.parameters_for(schema) + + for parameter in parameters: + value = core.deserialize( + parameter, + encoded) + + if value is not None: + return value + +def deserialize_string(schema, encoded, core=None): + if isinstance(encoded, str): + return encoded + +def deserialize_integer(schema, encoded, core=None): + value = None + try: + value = int(encoded) + except: + pass + + return value + +def deserialize_float(schema, encoded, core=None): + value = None + try: + value = float(encoded) + except: + pass + + return value + +def deserialize_list(schema, encoded, core=None): + if isinstance(encoded, list): + element_type = core.find_parameter( + schema, + 'element') + + return [ + core.deserialize( + element_type, + element) + for element in encoded] + +def deserialize_maybe(schema, encoded, core): + if encoded == NONE_SYMBOL or encoded is None: + return None + else: + value_type = core.find_parameter( + schema, + 'value') + + return core.deserialize(value_type, encoded) + +def deserialize_boolean(schema, encoded, core) -> bool: + if encoded == 'true': + return True + elif encoded == 'false': + return False + elif encoded == True or encoded == False: + return encoded + +def deserialize_tree(schema, encoded, core): + if isinstance(encoded, dict): + tree = {} + for key, value in encoded.items(): + if key.startswith('_'): + tree[key] = value + else: + tree[key] = deserialize_tree(schema, value, core) + + return tree + + else: + leaf_type = core.find_parameter( + schema, + 'leaf') + + if leaf_type: + return core.deserialize( + leaf_type, + encoded) + else: + return encoded + +def deserialize_units(schema, encoded, core): + if isinstance(encoded, Quantity): + return encoded + else: + return units(encoded) + +def deserialize_map(schema, encoded, core=None): + if isinstance(encoded, dict): + value_type = core.find_parameter( + schema, + 'value') + + return { + key: core.deserialize( + value_type, + subvalue) if not is_schema_key(key) else subvalue + for key, subvalue in encoded.items()} + +def deserialize_enum(schema, state, core): + return value + +def deserialize_array(schema, encoded, core): + if isinstance(encoded, np.ndarray): + return encoded + + elif isinstance(encoded, dict): + if 'value' in encoded: + return encoded['value'] + else: + found = core.retrieve( + encoded.get( + 'data', + schema['_data'])) + + dtype = read_datatype( + found) + + shape = read_shape( + schema['_shape']) + + if 'list' in encoded: + return np.array( + encoded['list'], + dtype=dtype).reshape( + shape) + else: + return np.zeros( + tuple(shape), + dtype=dtype) + +def deserialize_edge(schema, encoded, core): + return encoded + +def deserialize_schema(schema, state, core): + return state + + +# Slice functions +# --------------- + +def slice_any(schema, state, path, core): + if not isinstance(path, (list, tuple)): + if path is None: + path = () + else: + path = [path] + + if len(path) == 0: + return schema, state + + elif len(path) > 0: + head = path[0] + tail = path[1:] step = None if isinstance(state, dict): @@ -1061,158 +1501,7 @@ def slice_string(schema, state, path, core): raise Exception(f'cannot slice into an string: {path}\n{state}\n{schema}') -# Fold functions -# -------------- - -def fold_list(schema, state, method, values, core): - element_type = core.find_parameter( - schema, - 'element') - - if core.check(element_type, state): - result = core.fold( - element_type, - state, - method, - values) - - elif isinstance(state, list): - subresult = [ - fold_list( - schema, - element, - method, - values, - core) - for element in state] - - result = visit_method( - schema, - subresult, - method, - values, - core) - - else: - raise Exception(f'state does not seem to be a list or an eelement:\n state: {state}\n schema: {schema}') - - return result - - -def fold_tree(schema, state, method, values, core): - leaf_type = core.find_parameter( - schema, - 'leaf') - - if core.check(leaf_type, state): - result = core.fold( - leaf_type, - state, - method, - values) - - elif isinstance(state, dict): - subresult = {} - - for key, branch in state.items(): - if key.startswith('_'): - subresult[key] = branch - else: - subresult[key] = fold_tree( - schema[key] if key in schema else schema, - branch, - method, - values, - core) - - result = visit_method( - schema, - subresult, - method, - values, - core) - - else: - raise Exception(f'state does not seem to be a tree or a leaf:\n state: {state}\n schema: {schema}') - - return result - - -def fold_map(schema, state, method, values, core): - value_type = core.find_parameter( - schema, - 'value') - - subresult = {} - - for key, value in state.items(): - subresult[key] = core.fold( - value_type, - value, - method, - values) - - result = visit_method( - schema, - subresult, - method, - values, - core) - - return result - - -def fold_maybe(schema, state, method, values, core): - value_type = core.find_parameter( - schema, - 'value') - - if state is None: - result = core.fold( - 'any', - state, - method, - values) - - else: - result = core.fold( - value_type, - state, - method, - values) - - return result - -def fold_enum(schema, state, method, values, core): - if not isinstance(state, (tuple, list)): - return visit_method( - schema, - state, - method, - values, - core) - else: - parameters = core.parameters_for(schema) - result = [] - for parameter, element in zip(parameters, state): - fold = core.fold( - parameter, - element, - method, - values) - result.append(fold) - - result = tuple(result) - - return visit_method( - schema, - result, - method, - values, - core) - - -# Bind functions +# Bind functions # -------------- def bind_any(schema, state, key, subschema, substate, core): @@ -1257,6 +1546,7 @@ def bind_enum(schema, state, key, subschema, substate, core): return new_schema, tuple(open) + # Resolve functions # ---------------- @@ -1299,523 +1589,40 @@ def resolve_array(schema, update, core): raise Exception(f'arrays must be of the same shape, not \n {schema}\nand\n {update}') elif core.inherits_from(update, schema): - schema.update(update) - - elif not core.inherits_from(schema, update): - raise Exception(f'cannot resolve incompatible array schemas:\n {schema}\n {update}') - - else: - for key, subschema in update.items(): - if isinstance(key, int): - key = (key,) - - if len(key) > len(schema['_shape']): - raise Exception(f'key is longer than array dimension: {key}\n{schema}\n{update}') - elif len(key) == len(schema['_shape']): - data_schema = core.resolve_schemas( - data_schema, - subschema) - else: - shape = tuple_from_type( - schema['_shape']) - - subshape = shape[len(key):] - inner_schema = schema.copy() - inner_schema['_shape'] = subshape - inner_schema = core.resolve_schemas( - inner_schema, - subschema) - - data_schema = inner_schema['_data'] - - schema['_data'] = data_schema - - return schema - - -# Check functions -# --------------- - -def check_any(schema, state, core): - if isinstance(schema, dict): - for key, subschema in schema.items(): - if not key.startswith('_'): - if isinstance(state, dict): - if key in state: - check = core.check_state( - subschema, - state[key]) - - if not check: - return False - else: - return False - else: - return False - - return True - else: - return True - -def check_tuple(schema, state, core): - if not isinstance(state, (tuple, list)): - return False - - parameters = core.parameters_for(schema) - for parameter, element in zip(parameters, state): - if not core.check(parameter, element): - return False - - return True - -def check_union(schema, state, core): - found = find_union_type( - core, - schema, - state) - - return found is not None and len(found) > 0 - -def check_number(schema, state, core=None): - return isinstance(state, numbers.Number) - -def check_boolean(schema, state, core=None): - return isinstance(state, bool) - -def check_integer(schema, state, core=None): - return isinstance(state, int) and not isinstance(state, bool) - -def check_float(schema, state, core=None): - return isinstance(state, float) - -def check_string(schema, state, core=None): - return isinstance(state, str) - -def check_list(schema, state, core): - element_type = core.find_parameter( - schema, - 'element') - - if isinstance(state, list): - for element in state: - check = core.check( - element_type, - element) - - if not check: - return False - - return True - else: - return False - -def check_tree(schema, state, core): - leaf_type = core.find_parameter( - schema, - 'leaf') - - if isinstance(state, dict): - for key, value in state.items(): - check = core.check({ - '_type': 'tree', - '_leaf': leaf_type}, - value) - - if not check: - return core.check( - leaf_type, - value) - - return True - else: - return core.check(leaf_type, state) - -def check_map(schema, state, core=None): - value_type = core.find_parameter( - schema, - 'value') - - if not isinstance(state, dict): - return False - - for key, substate in state.items(): - if not core.check(value_type, substate): - return False - - return True - -def check_ports(state, core, key): - return key in state and core.check( - 'wires', - state[key]) - -def check_edge(schema, state, core): - return isinstance(state, dict) and check_ports(state, core, 'inputs') and check_ports(state, core, 'outputs') - -def check_maybe(schema, state, core): - if state is None: - return True - else: - value_type = core.find_parameter( - schema, - 'value') - - return core.check(value_type, state) - -def check_array(schema, state, core): - shape_type = core.find_parameter( - schema, - 'shape') - - return isinstance(state, np.ndarray) and state.shape == array_shape(core, shape_type) # and state.dtype == bindings['data'] # TODO align numpy data types so we can validate the types of the arrays - -def check_enum(schema, state, core): - if not isinstance(state, str): - return False - - parameters = core.parameters_for(schema) - return state in parameters - -def check_units(schema, state, core): - # TODO: expand this to check the actual units for compatibility - return isinstance(state, Quantity) - - -# Serialize functions -# ------------------- - -def serialize_any(schema, state, core): - if isinstance(state, dict): - tree = {} - - for key in non_schema_keys(schema): - encoded = core.serialize( - schema.get(key, schema), - state.get(key)) - tree[key] = encoded - - return tree - - else: - return str(state) - -def serialize_union(schema, value, core): - union_type = find_union_type( - core, - schema, - value) - - return core.serialize( - union_type, - value) - -def serialize_tuple(schema, value, core): - parameters = core.parameters_for(schema) - result = [] - - for parameter, element in zip(parameters, value): - encoded = core.serialize( - parameter, - element) - - result.append(encoded) - - return tuple(result) - -def serialize_string(schema, value, core=None): - return value - -def serialize_boolean(schema, value: bool, core) -> str: - return str(value) - -def serialize_list(schema, value, core=None): - element_type = core.find_parameter( - schema, - 'element') - - return [ - core.serialize( - element_type, - element) - for element in value] - -def serialize_tree(schema, value, core): - if isinstance(value, dict): - encoded = {} - for key, subvalue in value.items(): - encoded[key] = serialize_tree( - schema, - subvalue, - core) - - else: - leaf_type = core.find_parameter( - schema, - 'leaf') - - if core.check(leaf_type, value): - encoded = core.serialize( - leaf_type, - value) - else: - raise Exception(f'trying to serialize a tree but unfamiliar with this form of tree: {value} - current schema:\n {pf(schema)}') - - return encoded - -def serialize_units(schema, value, core): - return str(value) - -def serialize_maybe(schema, value, core): - if value is None: - return NONE_SYMBOL - else: - value_type = core.find_parameter( - schema, - 'value') - - return core.serialize( - value_type, - value) - -def serialize_map(schema, value, core=None): - value_type = core.find_parameter( - schema, - 'value') - - return { - key: core.serialize( - value_type, - subvalue) if not is_schema_key(key) else subvalue - for key, subvalue in value.items()} - - -def serialize_edge(schema, value, core): - return value - -def serialize_enum(schema, value, core): - return value - -def serialize_schema(schema, state, core): - return state - -def serialize_array(schema, value, core): - """ Serialize numpy array to list """ - - if isinstance(value, dict): - return value - elif isinstance(value, str): - import ipdb; ipdb.set_trace() - else: - array_data = 'string' - dtype = value.dtype.name - if dtype.startswith('int'): - array_data = 'integer' - elif dtype.startswith('float'): - array_data = 'float' - - return { - 'list': value.tolist(), - 'data': array_data, - 'shape': list(value.shape)} - -# Deserialize functions -# --------------------- - -def deserialize_any(schema, state, core): - if isinstance(state, dict): - tree = {} - - for key, value in state.items(): - if is_schema_key(key): - decoded = value - else: - decoded = core.deserialize( - schema.get(key, 'any'), - value) - - tree[key] = decoded - - for key in non_schema_keys(schema): - if key not in tree: - # if key not in state: - # decoded = core.default( - # schema[key]) - # else: - if key in state: - decoded = core.deserialize( - schema[key], - state[key]) - - tree[key] = decoded - - return tree - - else: - return state - -def deserialize_tuple(schema, state, core): - parameters = core.parameters_for(schema) - result = [] - - if isinstance(state, str): - if (state[0] == '(' and state[-1] == ')') or (state[0] == '[' and state[-1] == ']'): - state = state[1:-1].split(',') - else: - return None - - for parameter, code in zip(parameters, state): - element = core.deserialize( - parameter, - code) - - result.append(element) - - return tuple(result) - -def deserialize_union(schema, encoded, core): - if encoded == NONE_SYMBOL: - return None - else: - parameters = core.parameters_for(schema) - - for parameter in parameters: - value = core.deserialize( - parameter, - encoded) - - if value is not None: - return value - -def deserialize_string(schema, encoded, core=None): - if isinstance(encoded, str): - return encoded - -def deserialize_integer(schema, encoded, core=None): - value = None - try: - value = int(encoded) - except: - pass - - return value - -def deserialize_float(schema, encoded, core=None): - value = None - try: - value = float(encoded) - except: - pass - - return value - -def deserialize_list(schema, encoded, core=None): - if isinstance(encoded, list): - element_type = core.find_parameter( - schema, - 'element') - - return [ - core.deserialize( - element_type, - element) - for element in encoded] - -def deserialize_maybe(schema, encoded, core): - if encoded == NONE_SYMBOL or encoded is None: - return None - else: - value_type = core.find_parameter( - schema, - 'value') - - return core.deserialize(value_type, encoded) - -def deserialize_boolean(schema, encoded, core) -> bool: - if encoded == 'true': - return True - elif encoded == 'false': - return False - elif encoded == True or encoded == False: - return encoded - -def deserialize_tree(schema, encoded, core): - if isinstance(encoded, dict): - tree = {} - for key, value in encoded.items(): - if key.startswith('_'): - tree[key] = value - else: - tree[key] = deserialize_tree(schema, value, core) - - return tree - - else: - leaf_type = core.find_parameter( - schema, - 'leaf') - - if leaf_type: - return core.deserialize( - leaf_type, - encoded) - else: - return encoded - -def deserialize_units(schema, encoded, core): - if isinstance(encoded, Quantity): - return encoded - else: - return units(encoded) - -def deserialize_map(schema, encoded, core=None): - if isinstance(encoded, dict): - value_type = core.find_parameter( - schema, - 'value') + schema.update(update) - return { - key: core.deserialize( - value_type, - subvalue) if not is_schema_key(key) else subvalue - for key, subvalue in encoded.items()} + elif not core.inherits_from(schema, update): + raise Exception(f'cannot resolve incompatible array schemas:\n {schema}\n {update}') -def deserialize_enum(schema, state, core): - return value + else: + for key, subschema in update.items(): + if isinstance(key, int): + key = (key,) -def deserialize_array(schema, encoded, core): - if isinstance(encoded, np.ndarray): - return encoded + if len(key) > len(schema['_shape']): + raise Exception(f'key is longer than array dimension: {key}\n{schema}\n{update}') + elif len(key) == len(schema['_shape']): + data_schema = core.resolve_schemas( + data_schema, + subschema) + else: + shape = tuple_from_type( + schema['_shape']) - elif isinstance(encoded, dict): - if 'value' in encoded: - return encoded['value'] - else: - found = core.retrieve( - encoded.get( - 'data', - schema['_data'])) + subshape = shape[len(key):] + inner_schema = schema.copy() + inner_schema['_shape'] = subshape + inner_schema = core.resolve_schemas( + inner_schema, + subschema) - dtype = read_datatype( - found) + data_schema = inner_schema['_data'] - shape = read_shape( - schema['_shape']) + schema['_data'] = data_schema - if 'list' in encoded: - return np.array( - encoded['list'], - dtype=dtype).reshape( - shape) - else: - return np.zeros( - tuple(shape), - dtype=dtype) + return schema -def deserialize_edge(schema, encoded, core): - return encoded -def deserialize_schema(schema, state, core): - return state # Dataclass functions # ------------------- @@ -1990,6 +1797,95 @@ def dataclass_enum(schema, path, core): def dataclass_array(schema, path, core): return np.ndarray + +# Default functions +# ----------------- + +def default_any(schema, core): + default = {} + + for key, subschema in schema.items(): + if not is_schema_key(key): + default[key] = core.default( + subschema) + + return default + +def default_tuple(schema, core): + parts = [] + for parameter in schema['_type_parameters']: + subschema = schema[f'_{parameter}'] + part = core.default(subschema) + parts.append(part) + + return tuple(parts) + +def default_union(schema, core): + final_parameter = schema['_type_parameters'][-1] + subschema = schema[f'_{final_parameter}'] + + return core.default(subschema) + +def default_tree(schema, core): + leaf_schema = core.find_parameter( + schema, + 'leaf') + + default = {} + + non_schema_keys = [ + key + for key in schema + if not is_schema_key(key)] + + if non_schema_keys: + base_schema = { + key: subschema + for key, subschema in schema.items() + if is_schema_key(key)} + + for key in non_schema_keys: + subschema = core.merge_schemas( + base_schema, + schema[key]) + + subdefault = core.default( + subschema) + + if subdefault: + default[key] = subdefault + + return default + +def default_array(schema, core): + data_schema = core.find_parameter( + schema, + 'data') + + dtype = read_datatype( + data_schema) + + shape = read_shape( + schema['_shape']) + + return np.zeros( + shape, + dtype=dtype) + +def default_enum(schema, core): + parameter = schema['_type_parameters'][0] + return schema[f'_{parameter}'] + + +def default_edge(schema, core): + edge = {} + for key in schema: + if not is_schema_key(key): + edge[key] = core.default( + schema[key]) + + return edge + # Generate functions # ------------------ @@ -2264,6 +2160,113 @@ def generate_any(core, schema, state, top_schema=None, top_state=None, path=None return generated_schema, generated_state, top_schema, top_state +# Sort functions +# -------------- + +def sort_quote(core, schema, state): + return schema, state + + +def sort_any(core, schema, state): + if not isinstance(schema, dict): + schema = core.find(schema) + if not isinstance(state, dict): + return schema, state + + merged_schema = {} + merged_state = {} + + for key in union_keys(schema, state): + if is_schema_key(key): + if key in state: + merged_schema[key] = core.merge_schemas( + schema.get(key, {}), + state[key]) + else: + merged_schema[key] = schema[key] + else: + subschema, merged_state[key] = core.sort( + schema.get(key, {}), + state.get(key, None)) + if subschema: + merged_schema[key] = subschema + + return merged_schema, merged_state + + +# Resolve functions +# ---------------- + +def resolve_any(schema, update, core): + schema = schema or {} + outcome = schema.copy() + + for key, subschema in update.items(): + if key == '_type' and key in outcome: + if outcome[key] != subschema: + if core.inherits_from(outcome[key], subschema): + continue + elif core.inherits_from(subschema, outcome[key]): + outcome[key] = subschema + else: + raise Exception(f'cannot resolve types when updating\ncurrent type: {schema}\nupdate type: {update}') + + elif not key in outcome or type_parameter_key(update, key): + if subschema: + outcome[key] = subschema + else: + outcome[key] = core.resolve_schemas( + outcome.get(key), + subschema) + + return outcome + +# def resolve_tree(schema, update, core): +# if isinstance(update, dict): +# leaf_schema = schema.get('_leaf', {}) + +# if '_type' in update: +# if update['_type'] == 'map': +# value_schema = update.get('_value', {}) +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# value_schema) + +# elif update['_type'] == 'tree': +# for key, subschema in update.items(): +# if not key.startswith('_'): +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# subschema) +# else: +# leaf_schema = core.resolve_schemas( +# leaf_schema, +# update) + +# schema['_leaf'] = leaf_schema +# else: +# for key, subupdate in + +# return schema + +def resolve_path(path): + """ + Given a path that includes '..' steps, resolve the path to a canonical form + """ + resolve = [] + + for step in path: + if step == '..': + if len(resolve) == 0: + raise Exception(f'cannot go above the top in path: "{path}"') + else: + resolve = resolve[:-1] + else: + resolve.append(step) + + return tuple(resolve) + + def is_empty(value): if isinstance(value, np.ndarray): return False From 945a613bb5dd722bce8830cefaf68acd9e9b2c57 Mon Sep 17 00:00:00 2001 From: Eran Date: Fri, 6 Dec 2024 21:24:29 -0500 Subject: [PATCH 8/8] add some notes to dostring --- bigraph_schema/type_system.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bigraph_schema/type_system.py b/bigraph_schema/type_system.py index e8d817e..b47380d 100644 --- a/bigraph_schema/type_system.py +++ b/bigraph_schema/type_system.py @@ -2,6 +2,9 @@ =========== Type System =========== + +Includes Type Functions: apply, check, fold, divide, serialize, deserialize, slice, bind, merge +TODO: describe these functions """ import copy @@ -1130,6 +1133,7 @@ def serialize_array(schema, value, core): 'data': array_data, 'shape': list(value.shape)} + # Deserialize functions # --------------------- @@ -1546,7 +1550,6 @@ def bind_enum(schema, state, key, subschema, substate, core): return new_schema, tuple(open) - # Resolve functions # ---------------- @@ -1569,7 +1572,6 @@ def resolve_map(schema, update, core): return schema - def resolve_array(schema, update, core): if not '_shape' in schema: schema = core.access(schema) @@ -2221,6 +2223,7 @@ def resolve_any(schema, update, core): return outcome + # def resolve_tree(schema, update, core): # if isinstance(update, dict): # leaf_schema = schema.get('_leaf', {}) @@ -2249,6 +2252,7 @@ def resolve_any(schema, update, core): # return schema + def resolve_path(path): """ Given a path that includes '..' steps, resolve the path to a canonical form @@ -2276,8 +2280,6 @@ def is_empty(value): return False - - def find_union_type(core, schema, state): parameters = core.parameters_for(schema)