Source code for psynet.trial.graph

# pylint: disable=unused-argument,abstract-method

from typing import List, Optional, Type

from dallinger import db

from ..field import claim_field
from .chain import ChainNetwork, ChainNode, ChainTrial, ChainTrialMaker

# from psynet.trial.main import with_trial_maker_namespace
from .main import with_trial_maker_namespace


[docs] class GraphChainNetwork(ChainNetwork): """ A Network class for graph chains. A graph chain corresponds to the evolution of a vertex within a graph. Parameters (for now stating the new ones) ----------------------------------------- vertex_id The id of the vertex that the network is representing within the graph. dependent_vertex_ids A list of the vertex ids on which the current node depends (incoming edges). source_seed Source seed to use when initializing the graph in the trialmaker. """ __extra_vars__ = ChainNetwork.__extra_vars__.copy() vertex_id = claim_field("vertex_id", __extra_vars__, int) dependent_vertex_ids = claim_field("dependent_vertex_ids", __extra_vars__) source_seed = claim_field("source_seed", __extra_vars__) def __init__( # overriden self, trial_maker_id: str, experiment, start_node: "GraphChainNode", chain_type: str, trials_per_node: int, target_n_nodes: int, participant=None, id_within_participant: Optional[int] = None, ): self.vertex_id = start_node.vertex_id self.dependent_vertex_ids = start_node.dependent_vertex_ids self.source_seed = start_node.seed super().__init__( trial_maker_id=trial_maker_id, start_node=start_node, experiment=experiment, chain_type=chain_type, trials_per_node=trials_per_node, target_n_nodes=target_n_nodes, participant=participant, id_within_participant=id_within_participant, )
[docs] class GraphChainTrial(ChainTrial): """ A Trial class for graph chains. """
[docs] def make_definition(self, experiment, participant): """ (Built-in) In an graph chain, the trial's definition equals the definition of the node that created it. Parameters ---------- experiment An instantiation of :class:`psynet.experiment.Experiment`, corresponding to the current experiment. participant Optional participant with which to associate the trial. Returns ------- object The trial's definition, equal to the node's definition. """ return self.node.definition
[docs] class GraphChainNode(ChainNode): """ A Node class for graph chains. Parameters (for now stating the new ones) ----------------------------------------- vertex_id The id of the vertex that the network is representing within the graph. dependent_vertex_ids A list of the vertex ids on which the current node depends (incoming edges). """ __extra_vars__ = ChainNode.__extra_vars__.copy() def __init__( self, seed, degree: int, network, experiment, propagate_failure: bool, vertex_id: int, dependent_vertex_ids: List[int], participant=None, ): # pylint: disable=unused-argument self.vertex_id = vertex_id self.dependent_vertex_ids = dependent_vertex_ids super().__init__( seed=seed, degree=degree, network=network, experiment=experiment, propagate_failure=propagate_failure, participant=participant, ) # def create_initial_seed(self, experiment, participant): # return self.network.source_seed @staticmethod def generate_class_seed(): raise NotImplementedError
[docs] def create_definition_from_seed(self, seed, experiment, participant): """ (Built-in) In a graph chain, the next node in the chain is a faithful reproduction of the previous iteration. Parameters ---------- seed The seed being passed to the node. experiment An instantiation of :class:`psynet.experiment.Experiment`, corresponding to the current experiment. participant Current participant, if relevant. Returns ------- object The node's new definition, which is a faithful reproduction of the seed that it was passed. """ # The next node in the chain is a faithful reproduction of the previous iteration. return seed
[docs] def summarize_trials(self, trials: list, experiment, participant): """ (Abstract method, to be overridden) This method should summarize the answers to the provided trials. A default method is implemented for cases when there is just one trial per node; in this case, the method extracts and returns the trial's answer, available in ``trial.answer``. The method must be extended if it is to cope with multiple trials per node, however. Parameters ---------- trials Trials to be summarized. By default only trials that are completed (i.e. have received a response) and processed (i.e. aren't waiting for an asynchronous process) are provided here. experiment An instantiation of :class:`psynet.experiment.Experiment`, corresponding to the current experiment. participant The participant who initiated the creation of the node. Returns ------- object The derived seed. Should be suitable for serialisation to JSON. """ if len(trials) == 1: return trials[0].answer raise NotImplementedError
vertex_id = claim_field("vertex_id", __extra_vars__, int) dependent_vertex_ids = claim_field("dependent_vertex_ids", __extra_vars__) @property def ready_to_spawn(self): parents = ( self.get_parents() ) # These are parent nodes from the same layer, to be passed to the next layer if len(parents) == len( self.dependent_vertex_ids ): # Make sure all parents exist all_parents_ready = all([p.reached_target_n_trials for p in parents]) current_vertex_ready = self.reached_target_n_trials return all_parents_ready and current_vertex_ready elif len(parents) < len(self.dependent_vertex_ids): return False else: raise ValueError("Invalid number of parent nodes!") def get_parents(self): trial_maker_id = self.network.trial_maker_id degree = self.degree nodes = GraphChainNode.query.all() current_layer = [ n for n in nodes if n.network.trial_maker_id == trial_maker_id and n.degree == degree ] parents = [n for n in current_layer if n.vertex_id in self.dependent_vertex_ids] return parents
[docs] class GraphChainTrialMaker(ChainTrialMaker): """ A TrialMaker class for graph chains; see the documentation for :class:`~psynet.trial.chain.ChainTrialMaker` for usage instructions. Parameters ---------- network_structure A representation of the graph structure to instantiate. The representation consistes of a dictionary of vertices and edges. E.g. {"vertices": [1,2], "edges": [{"origin": 1, "target": 2, "properties": {"type": "default"}}]} """ def __init__( self, *, id_, node_class: Type[GraphChainNode], trial_class: Type[GraphChainTrial], network_structure, chain_type: str, expected_trials_per_participant: int, max_trials_per_participant: int, chains_per_participant: Optional[int], # chains_per_experiment: Optional[int], trials_per_node: int, balance_across_chains: bool, check_performance_at_end: bool, check_performance_every_trial: bool, recruit_mode: str, target_n_participants=Optional[int], max_nodes_per_chain: Optional[int] = None, fail_trials_on_premature_exit: bool = False, fail_trials_on_participant_performance_check: bool = False, propagate_failure: bool = True, n_repeat_trials: int = 0, wait_for_networks: bool = False, allow_revisiting_networks_in_across_chains: bool = False, sync_group_type: Optional[str] = None, ): if chain_type == "within": raise NotImplementedError # UNCLEAR TO ME HOW TO UNITE THE ON-DEMAND CREATION OF WITHIN CHAINS AND THE PRE-DFINED GRAPH NETWORK STRUCTURE chains_per_experiment = len(network_structure["vertices"]) self.network_structure = network_structure super().__init__( id_=id_, node_class=node_class, trial_class=trial_class, chain_type=chain_type, expected_trials_per_participant=expected_trials_per_participant, max_trials_per_participant=max_trials_per_participant, chains_per_participant=chains_per_participant, chains_per_experiment=chains_per_experiment, trials_per_node=trials_per_node, balance_across_chains=balance_across_chains, check_performance_at_end=check_performance_at_end, check_performance_every_trial=check_performance_every_trial, recruit_mode=recruit_mode, target_n_participants=target_n_participants, max_nodes_per_chain=max_nodes_per_chain, fail_trials_on_premature_exit=fail_trials_on_premature_exit, fail_trials_on_participant_performance_check=fail_trials_on_participant_performance_check, propagate_failure=propagate_failure, n_repeat_trials=n_repeat_trials, wait_for_networks=wait_for_networks, allow_revisiting_networks_in_across_chains=allow_revisiting_networks_in_across_chains, sync_group_type=sync_group_type, ) @property def default_network_class(self): return GraphChainNetwork
[docs] def pre_deploy_routine(self, experiment): if self.chain_type == "across": experiment.var.set( with_trial_maker_namespace(self.id, "network_structure"), self.network_structure, ) super().pre_deploy_routine(experiment)
def create_networks_across(self, experiment): network_structure = self.network_structure vertices = network_structure["vertices"] source_seeds = self.generate_source_seed_bundles() for i in range(self.chains_per_experiment): vertex_id = vertices[i] source_seed = [ seed["bundle"] for seed in source_seeds if seed["vertex_id"] == vertex_id ][0] dependent_vertex_ids = self.get_dependent_vertex_ids( vertex_id, network_structure ) start_node = self.node_class( seed=source_seed, degree=0, network=None, experiment=experiment, propagate_failure=self.propagate_failure, vertex_id=vertex_id, dependent_vertex_ids=dependent_vertex_ids, participant=None, ) self.create_graph_network(experiment, start_node) def create_graph_network( self, experiment, start_node, participant=None, id_within_participant=None, ): network = self.network_class( trial_maker_id=self.id, start_node=start_node, experiment=experiment, chain_type=self.chain_type, trials_per_node=self.trials_per_node, target_n_nodes=self.max_nodes_per_chain, participant=participant, id_within_participant=id_within_participant, ) db.session.add(network) db.session.commit() return network def get_dependent_vertex_ids(self, target, network_structure): edges = network_structure["edges"] dependent_vertex_ids = [e["origin"] for e in edges if e["target"] == target] return dependent_vertex_ids
[docs] def grow_network(self, network, experiment): # We set participant = None because of Dallinger's constraint of not allowing participants # to create nodes after they have finished working. participant = None head = network.head if head.ready_to_spawn: if head.degree > 0: seed_bundle = self.create_seed_bundle(head, experiment, participant) else: seed_bundle = head.create_seed(experiment, participant) node = self.node_class( seed_bundle, head.degree + 1, network, experiment, self.propagate_failure, network.vertex_id, network.dependent_vertex_ids, participant, ) db.session.add(node) network.add_node(node) db.session.commit() node.check_on_deploy() db.session.commit() return True return False
def create_seed_bundle(self, head, experiment, participant): head_seed = head.create_seed(experiment, participant) parents = head.get_parents() bundle = [ { "vertex_id": head.network.vertex_id, "content": head_seed, "is_center": True, } ] + [ { "vertex_id": p.network.vertex_id, "content": p.create_seed( experiment, participant ), # might require some thought if participant becomes relevant "is_center": False, } for p in parents ] return bundle def generate_source_seed_bundles(self): network_structure = self.network_structure vertices = network_structure["vertices"] centers = [ { "vertex_id": v, "content": self.node_class.generate_class_seed(), "is_center": True, } for v in vertices ] bundles = [] for i in range(len(centers)): center = centers[i] dependent_vertex_ids = self.get_dependent_vertex_ids( center["vertex_id"], network_structure ) bundle = [center] for j in dependent_vertex_ids: content = [c["content"] for c in centers if c["vertex_id"] == j] bundle = bundle + [ {"vertex_id": j, "content": content[0], "is_center": False} ] bundles = bundles + [{"vertex_id": center["vertex_id"], "bundle": bundle}] return bundles