def test_full_continuity(): model_text = """ A=B B=A""" knockouts: Dict[str, int] = {} # no knockout continuous: Dict[str, bool] = { "A": True, "B": True } # all variables continuous variables, update_fn, equation_system = process_model_text( model_text, knockouts, continuous) assert variables == ("A", "B") # small changes unchanged for a, b in itertools.product(range(3), repeat=2): if abs(a - b) <= 1: assert np.all(update_fn([a, b]) == [b, a]) # all (esp. larger) changes `muted` for a, b in itertools.product(range(3), repeat=2): assert np.all( update_fn([a, b]) == np.mod( [ b**2 + a + 2 * a**2 + a**2 * b + 2 * a * b**2, a**2 + b + 2 * b**2 + a * b**2 + 2 * a**2 * b, ], 3, ))
def test_parse(): model_text = """ A=B B=A""" knockouts = {} # no knockout continuous: Dict[str, bool] = {} # no continuous variables variables, update_fn, equation_system = process_model_text( model_text, knockouts, continuous ) assert variables == ("A", "B") for a, b in itertools.product(range(3), repeat=2): assert np.all(update_fn([a, b]) == [b, a])
def test_knockouts(): model_text = """ A=B B=A""" knockouts: Dict[str, int] = {"A": 1} # knockout A continuous: Dict[str, bool] = {} # no variables continuous variables, update_fn, equation_system = process_model_text( model_text, knockouts, continuous ) assert variables == ("A", "B") # function becomes constant on 2nd step for a, b in itertools.product(range(3), repeat=2): assert np.all(update_fn(update_fn([a, b])) == [1, 1])
def test_update_fn1(): model_text = """ A=B B=A""" knockouts: Dict[str, int] = {} # no knockout continuous: Dict[str, bool] = {} # all variables continuous variables, update_fn, equation_system = process_model_text( model_text, knockouts, continuous ) assert variables == ("A", "B") # function is order 2 for a, b in itertools.product(range(3), repeat=2): assert np.all(update_fn(update_fn([a, b])) == [a, b])
def download_model(modeltype: str) -> Response: use_sbml_qual_format = modeltype[-5:] == ".sbml" use_text_format = modeltype[-4:] == ".txt" # get the variable list and right hand sides model_text: str = request.form["model_text"].strip() variables, update_fn, submitted_equation_system = process_model_text( model_text, dict(), dict()) if use_text_format: response = make_response(str(submitted_equation_system)) response.headers["Content-Type"] = "text/plain" return response if use_sbml_qual_format: sbml_qual: BeautifulSoup = submitted_equation_system.as_sbml_qual() response = make_response(str(sbml_qual.prettify())) response.headers["Content-Type"] = "application/xml" return response return make_response(error_report("Unknown Error"))
def compute_trace( *, model_text: str, knockouts: Dict[str, int], continuous: Dict[str, bool], init_state: Dict[str, str], visualize_variables: Dict[str, bool], check_nearby: bool, ): """Run the cycle finding simulation for an initial state""" # create an update function and equation system variables, update_fn, equation_system = process_model_text( model_text, knockouts, continuous) # construction of initial values can fail with an exception that contains a response try: edge_lists, init_states, limit_points = run_model_variable_initial_values( init_state_prototype=init_state, variables=variables, update_fn=update_fn, equation_system=equation_system, check_nearby=check_nearby, ) except ComputeTraceException as e: payload = e.args[0] if type(payload) in {str, Response}: return payload else: # should not occur return make_response(error_report("Unknown error")) ################################ # give numeric labels to vertices and record their type i.e. initial node, limit cycle, other def to_key(edges): return frozenset(edges.items()) class NodeType(IntEnum): Other = 0 InitialNode = 1 LimitNode = 2 labels: Dict[frozenset, int] = dict() # keys: frozenset of (var, val) pairs defining a point in config space # values: integer 'id's for the points of config space node_type: Dict[int, NodeType] = dict() # keys: integer 'id's for the points of config space # values: type of node init_states_as_keys = set( to_key(dict(zip(variables, state))) for state in init_states) limit_points_as_keys = set( to_key(dict(zip(variables, state.array))) for state in limit_points) def get_node_type(key) -> NodeType: if key in limit_points_as_keys: return NodeType.LimitNode elif key in init_states_as_keys: return NodeType.InitialNode else: return NodeType.Other count = 0 for edge_list in edge_lists: for edge in edge_list: source = to_key(edge["source"]) if source not in labels: labels[source] = count node_type[count] = get_node_type(source) count += 1 target = to_key(edge["target"]) if target not in labels: labels[target] = count node_type[count] = get_node_type(target) count += 1 return_states = [] for edge_list in edge_lists: return_state = to_key(edge_list[-1]["target"]) return_states.append(labels[return_state]) source_labels = [[labels[to_key(edge["source"])] for edge in edge_list] for edge_list in edge_lists] variable_level_plots = plot_variable_levels_for_trajectories( edge_lists=edge_lists, return_states=return_states, source_labels=source_labels, variables=variables, visualize_variables=visualize_variables, ) # create data for the javascript edge_lists_by_labels = [[{ "source": labels[to_key(edge["source"])], "target": labels[to_key(edge["target"])], } for edge in edge_list] for edge_list in edge_lists] num_nodes = len(labels) height_percent = min(95, math.ceil(50 + 50 / (1 - math.exp(-num_nodes / 100)))) width_px = 640 height_px = math.ceil(320 + 320 / (1 - math.exp(-num_nodes / 100))) initial_node_positions = graph_layout(edge_lists_by_labels, height_px, width_px) nodes_json = json.dumps([{ "id": str(label), "group": node_type[label], "x": float(initial_node_positions[label][0]), "y": float(initial_node_positions[label][1]), } for label in labels.values()]) edge_tuples = set() for edge_list in edge_lists: edge_tuples.update({ (labels[to_key(edge["source"])], labels[to_key(edge["target"])]) for edge in edge_list if to_key(edge["source"]) != to_key(edge["target"]) }) # `if` blocks self loops, which the force-directed graph freaks out about edge_json = json.dumps([{ "source": str(source), "target": str(target) } for (source, target) in edge_tuples]) # respond with the results-of-computation page return make_response( render_template( "compute-trace.html", variables=equation_system.target_variables(), num_edge_lists=len(edge_lists), trajectories=list( zip( edge_lists, return_states, source_labels, map(len, edge_lists), variable_level_plots, )), nodes=nodes_json, height_percent=height_percent, width_px=width_px, height_px=height_px, links=edge_json, ))
def compute_cycles( *, model_text: str, knockouts: Dict[str, int], continuous: Dict[str, bool], num_iterations: int, visualize_variables: Dict[str, bool], ): """ Parameters ---------- model_text knockouts continuous num_iterations visualize_variables Returns ------- """ # turn boolean-valued dictionary to list of variables to visualize visualized_variables: Tuple[str] = tuple(var for var in visualize_variables if visualize_variables[var]) # create an update function and equation system try: variables, update_fn, equation_system = process_model_text( model_text, knockouts, continuous) except ParseError as e: return make_response(error_report(e.message)) # compile and warm up the jitter # update_fn = numba.jit(update_fn) # update_fn(np.zeros(len(variables), dtype=np.int64)) # associate variable names with their index in vectors variable_idx: Dict[str, int] = dict(zip(variables, range(len(variables)))) constants: List[str] = equation_system.constant_variables() constants_vals: Dict[str, int] = { const: int(equation_system[const]) for const in constants } # decide if we will perform a complete state space search or not state_space_size = 3**(len(variables) - len(constants)) perform_complete_search = state_space_size <= num_iterations state_generator: Iterator[np.ndarray] if perform_complete_search: state_generator = complete_search_generator( variables=variables, constants_vals=constants_vals) num_iterations = state_space_size else: state_generator = random_search_generator( num_iterations=num_iterations, variables=variables, constants_vals=constants_vals, ) max_threads = max( 1, min(pathos.multiprocessing.cpu_count() - 1, math.floor(state_space_size / 1000)), ) batch_generator = batcher(state_generator, variables, batch_size=min(1000, num_iterations // max_threads)) trajectory_length_counts: Dict[HashableNdArray, BinCounter] = dict() data_counts: Dict[HashableNdArray, np.ndarray] = dict() means: Dict[HashableNdArray, np.ndarray] = dict() variances: Dict[HashableNdArray, np.ndarray] = dict() with pathos.multiprocessing.ProcessPool(nodes=max_threads) as pool: trajectory_length_counts, data_counts, means, variances = functools.reduce( reducer, pool.uimap(batch_trajectory_process, batch_generator, itertools.repeat(update_fn)), TrajectoryStatistics(trajectory_length_counts, data_counts, means, variances), ) phase_points = trajectory_length_counts.keys() # recreate limit cycles from phase point keys limit_cycles: Dict[ HashableNdArray, List] = dict() # record limit sets with their ordering. i.e. as cycles for phase_point in phase_points: cycle = list() t_state = phase_point state = phase_point.array # do-while cycle.append(t_state) state = update_fn(state) t_state = HashableNdArray(state) while t_state != phase_point: cycle.append(t_state) state = update_fn(state) t_state = HashableNdArray(state) # record cycle limit_cycles[phase_point] = cycle # create images for each variable limit_set_stats_images = { phase_point: dict() for phase_point in phase_points } with tempfile.TemporaryDirectory() as tmp_dir_name: plt.rcParams["svg.fonttype"] = "none" for phase_point, var in itertools.product(phase_points, visualized_variables): cycle = limit_cycles[phase_point] var_idx = variable_idx[var] var_means = np.flipud(means[phase_point][:, var_idx]) var_stdevs = np.flipud(np.sqrt(variances[phase_point][:, var_idx])) plt.figure(figsize=(6, 4)) # means, since phased will fix on single value at end plt.plot(list(var_means) + [cycle[0][var_idx]], color="#1f3d87") plt.fill_between( range(len(var_means) + 1), list(var_means - var_stdevs) + [cycle[0][var_idx]], list(var_means + var_stdevs) + [cycle[0][var_idx]], color="grey", alpha=0.25, ) # dashed cycle in `converging` region plt.plot( range(len(var_means) - len(cycle), len(var_means) + 1), [cycle[idx][var_idx] for idx in range(len(cycle))] + [cycle[0][var_idx]], color="#F24F00", linestyle="dashed", ) # solid cycle in `converged` region plt.plot( range(len(var_means), len(var_means) + len(cycle) + 1), [cycle[idx][var_idx] for idx in range(len(cycle))] + [cycle[0][var_idx]], color="#F24F00", ) plt.title(f"Phased Envelope of trajectories for {var}") plt.xlabel("Distance to phase-fixing point on limit cycle") max_tick_goal = 40 tick_gap = max( 1, ceil((len(var_means) + len(cycle) + 1) / max_tick_goal)) x_tick_locations = [ x for x in range(0, len(var_means) + len(cycle) + 1) if (x - len(var_means)) % tick_gap == 0 ] x_tick_labels = list( map(lambda x: len(var_means) - x, x_tick_locations)) plt.xticks(x_tick_locations, x_tick_labels, rotation=90) plt.xlim([0, len(var_means) + len(cycle)]) plt.axvline(len(var_means), linestyle="dotted", color="grey") plt.axvline(len(var_means) - len(cycle), linestyle="dotted", color="grey") plt.ylabel("State") plt.yticks([0, 1, 2]) for y_val in range(3): plt.axhline(y=y_val, linestyle="dotted", color="grey", alpha=0.5) plt.ylim([0 - 0.1, 2 + 0.1]) plt.tight_layout() image_filename = "dist-" + str( hash(phase_point)) + "-" + var + ".svg" plt.savefig(tmp_dir_name + "/" + image_filename, transparent=True, pad_inches=0.0) plt.close() with open(tmp_dir_name + "/" + image_filename, "r") as image: limit_set_stats_images[phase_point][var] = Markup(image.read()) # print('var images complete') # cycle list sorted by frequency cycle_list = list(limit_cycles.values()) cycle_list.sort(key=lambda cyc: trajectory_length_counts[cyc[0]].total(), reverse=True) # tally of number of cycles of each length, sorted by cycle length cycle_len_counts = defaultdict(lambda: 0) for cycle in cycle_list: cycle_len_counts[len(cycle)] += 1 cycle_len_counts = tuple(sorted(cycle_len_counts.items())) length_distribution_images = dict() with tempfile.TemporaryDirectory() as tmp_dir_name: # create length distribution plots plt.rcParams["svg.fonttype"] = "none" for phase_point in phase_points: length_dist = trajectory_length_counts[phase_point].trimmed_bins() plt.figure(figsize=(4, 3)) plt.bar(x=range(len(length_dist)), height=length_dist, color="#002868") plt.title("Distribution of lengths of paths") plt.xlabel("Length of path") plt.ylabel("Number of states") plt.tight_layout() image_filename = "dist-" + str(hash(phase_point)) + ".svg" plt.savefig(tmp_dir_name + "/" + image_filename, transparent=True, pad_inches=0.0) plt.close() with open(tmp_dir_name + "/" + image_filename, "r") as image: length_distribution_images[phase_point] = Markup(image.read()) def get_key(cyc): return cyc[0] def get_count(cyc): return trajectory_length_counts[get_key(cyc)].total() cycles = [{ "states": cycle, "len": len(cycle), "count": get_count(cycle), "percent": 100 * get_count(cycle) / num_iterations, "len-dist-image": length_distribution_images[get_key(cycle)] if get_key(cycle) in length_distribution_images else "", "limit-set-stats-images": limit_set_stats_images[get_key(cycle)] if get_key(cycle) in limit_set_stats_images else dict(), } for cycle in cycle_list] # respond with the results-of-computation page return make_response( render_template( "compute-cycles.html", cycles=cycles, variables=variables, cycle_len_counts=cycle_len_counts, complete_results=perform_complete_search, ))