From baf049969d53c36b05151de4b898e8d8e266baed Mon Sep 17 00:00:00 2001 From: Geoforger Date: Fri, 3 Feb 2023 16:40:47 +0000 Subject: [PATCH 1/2] Messy implementation of afferent population save/load --- touchsim/classes.py | 125 +++++++++++++++++++++++++++++++++++++++++--- touchsim/surface.py | 1 + 2 files changed, 118 insertions(+), 8 deletions(-) diff --git a/touchsim/classes.py b/touchsim/classes.py index 346bdf5..470ba73 100644 --- a/touchsim/classes.py +++ b/touchsim/classes.py @@ -3,11 +3,13 @@ import warnings from math import isclose from scipy.signal import resample +from pathlib import Path +import json from .transduction import skin_touch_profile, circ_load_vert_stress,\ circ_load_dyn_wave, lif_neuron, check_pin_radius from . import constants -from .surface import null_surface +from .surface import Surface, null_surface class Afferent(object): """A single afferent, which can be placed on a surface and respond to tactile @@ -43,6 +45,10 @@ def __init__(self,affclass,**args): self.delay = args.get('delay',False) self.surface = args.get('surface',null_surface) + # Meta dictionary containing all data required to recreate exact afferent + self.meta = args + self.meta["type"] = affclass + if self.depth is None: # Set afferent depth self.depth = Afferent.affdepths.get(self.affclass) @@ -114,15 +120,23 @@ def __init__(self,*afferents,**args): Kwargs: surface (Surface object): The surface on which Afferent is located (default: a1.surface if set, otherwise null_surface). + load (bool): Enables loading population from json file. Implements the load_population method (defaults: False) + filename (string): Path to file to load population from. Defaults: False """ - self.afferents = list(afferents) - if len(self.afferents)==0: - sur = null_surface + load = args.get("load", False) + + if load: + filename = args.get("filename", None) + self.load_population(filename) else: - sur = self.afferents[0].surface - self.surface = args.get('surface',sur) - for a in self.afferents: - a.surface = self.surface + self.afferents = list(afferents) + if len(self.afferents)==0: + sur = null_surface + else: + sur = self.afferents[0].surface + self.surface = args.get('surface', sur) + for a in self.afferents: + a.surface = self.surface def __str__(self): return 'AfferentPopulation with ' + str(len(self)) + ' afferent(s): ' +\ @@ -256,6 +270,94 @@ def response(self,stim): r.append(lif_neuron(self,strain,udyn)) return Response(self,stim,r) + def save_population(self, filename:str, make_dirs:bool=True, overwrite:bool=True) -> None: + """ Saves afferent population into a json file + + Method creates a dictionary containing the relevant meta information of each afferent and surface within population. + Errors are raised by path.parent.mkdir function if make_dirs and overwrite bools are not correctly set for your setup. + + Args: + filename (string): Path to json file. Must include .json suffix + make_dirs (bool) : Indicate whether directories on output path should be created (default: True) + overwrite (bool) : Allows overwrite of file if it already exists on output path (default: True) + Returns: + None + """ + path = Path(filename) + + # Make parent directories if allowed by make_dirs and overwrite bools + path.parent.mkdir(parents=make_dirs, exist_ok=overwrite) + + population_dict = {} + + # Prepare afferents for json + for idx, afferent in enumerate(self.afferents): + afferent.meta.pop("surface", None) + population_dict[idx] = afferent.meta + + # Prepare surface for json + # Problem lies in the density subdict using tuples as keys - json does not like this + # TODO: Clean this up probably + density_dict = self.surface.meta["density"].copy() + + for key in list(density_dict.keys()): + if type(key) == tuple: + string_key = f"{key[0]}/{key[1]}" + density_dict[string_key] = density_dict.pop(key) + + surface_dict = self.surface.meta.copy() + surface_dict["density"] = density_dict + + population_dict["surface"] = surface_dict + + # TODO: Clean up this code using proper dictionary iterators + # Clean up arrays to make json serializable + for component in population_dict: + for param in population_dict[component]: + if type(population_dict[component][param]) == np.ndarray: + population_dict[component][param] = population_dict[component][param].tolist() + + with path.open("w") as out: + json.dump(population_dict, out) + + def load_population(self, filename:str) -> None: + + """ Loads an afferent population from a json file. To be used in conjunction with save_population method + + Arguments: + filename: Path to json file. Must include .json suffix + Returns: + None: + """ + path = Path(filename) + self.afferents = [] + + with path.open("r") as input: + population_data = json.load(input) + + # Get surface for population, reset lists back to arrays + surface_params = population_data.pop("surface") + surface_params["orig"] = np.array(surface_params["orig"]) + # Gross converting of keys back to tuples + density_dict = surface_params["density"] + for key in list(density_dict.keys()): + tuple_key = tuple(key.split("/")) + density_dict[tuple_key] = density_dict.pop(key) + self.surface = Surface(**surface_params) + + # Build afferent population + for component, params in population_data.items(): + # Convert lists back to arrays (lists needed for serialisation) + # for key, data in list(population_data[component].items()): + # if type(data) == list: + # population_data[component][key] = np.array(population_data[component][key]) + + #params["surface"] = self.surface + t = params.pop("type") + self.afferents.append(Afferent(t, **params)) + + self.afferents = list(self.afferents) + class Stimulus(object): """A tactile stimulus. @@ -275,6 +377,7 @@ def __init__(self,**args): self.fs = args.get('fs',1000.) self.pin_radius = args.get('pin_radius',.05) self.compute_profile() + self.meta = args def __str__(self): return 'Stimulus with ' + str(self.location.shape[0]) +\ @@ -441,3 +544,9 @@ def psth(self,bin=10.): """ bins = np.r_[0:self.duration+bin/1000.:bin/1000.] return np.array(list(map(lambda x:np.histogram(x,bins=bins)[0],self.spikes))) + + def save_stimulus(self, filename:str) -> None: + pass + + def load_stimulus(self, filename:str) -> None: + pass \ No newline at end of file diff --git a/touchsim/surface.py b/touchsim/surface.py index 3b42f57..db0bc43 100644 --- a/touchsim/surface.py +++ b/touchsim/surface.py @@ -41,6 +41,7 @@ def __init__(self,**args): afferent class and 2) string denoting density tag, and float denoting afferent density in cm^2 (default: 10. for each mapping). """ + self.meta = args self.orig = args.get('orig',np.array([0., 0.])) self.pxl_per_mm = args.get('pxl_per_mm',1.) self.theta = args.get('theta',0.) From 0afafe2b209a765722f910a51f4f1d247c4c82bb Mon Sep 17 00:00:00 2001 From: Geoforger Date: Mon, 6 Feb 2023 13:54:32 +0000 Subject: [PATCH 2/2] Stimulus saving/loading and code refactoring --- touchsim/classes.py | 114 +++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/touchsim/classes.py b/touchsim/classes.py index 470ba73..7d9767e 100644 --- a/touchsim/classes.py +++ b/touchsim/classes.py @@ -120,23 +120,15 @@ def __init__(self,*afferents,**args): Kwargs: surface (Surface object): The surface on which Afferent is located (default: a1.surface if set, otherwise null_surface). - load (bool): Enables loading population from json file. Implements the load_population method (defaults: False) - filename (string): Path to file to load population from. Defaults: False """ - load = args.get("load", False) - - if load: - filename = args.get("filename", None) - self.load_population(filename) + self.afferents = list(afferents) + if len(self.afferents)==0: + sur = null_surface else: - self.afferents = list(afferents) - if len(self.afferents)==0: - sur = null_surface - else: - sur = self.afferents[0].surface - self.surface = args.get('surface', sur) - for a in self.afferents: - a.surface = self.surface + sur = self.afferents[0].surface + self.surface = args.get('surface', sur) + for a in self.afferents: + a.surface = self.surface def __str__(self): return 'AfferentPopulation with ' + str(len(self)) + ' afferent(s): ' +\ @@ -297,31 +289,24 @@ def save_population(self, filename:str, make_dirs:bool=True, overwrite:bool=True # Prepare surface for json # Problem lies in the density subdict using tuples as keys - json does not like this - # TODO: Clean this up probably - density_dict = self.surface.meta["density"].copy() - - for key in list(density_dict.keys()): - if type(key) == tuple: - string_key = f"{key[0]}/{key[1]}" - density_dict[string_key] = density_dict.pop(key) + # Convert tuple key to string with "/" seperator + density_dict = {f"{key[0]}/{key[1]}" if type(key) is tuple else key:_ for key, _ in self.surface.meta["density"].items()} surface_dict = self.surface.meta.copy() surface_dict["density"] = density_dict population_dict["surface"] = surface_dict - # TODO: Clean up this code using proper dictionary iterators # Clean up arrays to make json serializable - for component in population_dict: - for param in population_dict[component]: - if type(population_dict[component][param]) == np.ndarray: - population_dict[component][param] = population_dict[component][param].tolist() + for component, params in population_dict.items(): + component_dict = {_: param.tolist() if type(param) is np.ndarray else param for _, param in params.items()} + population_dict[component] = component_dict with path.open("w") as out: json.dump(population_dict, out) - def load_population(self, filename:str) -> None: - + @classmethod + def load_population(cls, filename:str) -> None: """ Loads an afferent population from a json file. To be used in conjunction with save_population method Arguments: @@ -330,7 +315,7 @@ def load_population(self, filename:str) -> None: None: """ path = Path(filename) - self.afferents = [] + afferents = [] with path.open("r") as input: population_data = json.load(input) @@ -343,21 +328,18 @@ def load_population(self, filename:str) -> None: for key in list(density_dict.keys()): tuple_key = tuple(key.split("/")) density_dict[tuple_key] = density_dict.pop(key) - self.surface = Surface(**surface_params) # Build afferent population - for component, params in population_data.items(): - # Convert lists back to arrays (lists needed for serialisation) - # for key, data in list(population_data[component].items()): - # if type(data) == list: - # population_data[component][key] = np.array(population_data[component][key]) - - #params["surface"] = self.surface + for _, params in population_data.items(): t = params.pop("type") - self.afferents.append(Afferent(t, **params)) + if "location" in params: + params["location"] = np.array(params["location"]) + afferents.append(Afferent(t, **params)) - self.afferents = list(self.afferents) - + surface = Surface(**surface_params) + afferents = list(afferents) + + return cls(*afferents, surface=surface) class Stimulus(object): """A tactile stimulus. @@ -440,6 +422,48 @@ def propagate(self,aff): aff.depth,self.fs,aff.surface) return stat_comp, dyn_comp, self.fs + def save_stimulus(self, filename:str, make_dirs:bool=True, overwrite:bool=True) -> None: + """ Saves stimulus into a json file + + Method creates a dictionary containing the relevant meta information of recreating a stimulus. + Errors are raised by path.parent.mkdir function if make_dirs and overwrite bools are not correctly set for your setup. + + Args: + filename (string): Path to json file. Must include .json suffix + make_dirs (bool) : Indicate whether directories on output path should be created (default: True) + overwrite (bool) : Allows overwrite of file if it already exists on output path (default: True) + Returns: + None + """ + path = Path(filename) + + # Make parent directories if allowed by make_dirs and overwrite bools + path.parent.mkdir(parents=make_dirs, exist_ok=overwrite) + + new_params = {key: param.tolist() if type(param) is np.ndarray else param for key, param in self.meta.items()} + + with path.open("w") as out: + json.dump(new_params, out) + + @classmethod + def load_stimulus(cls, filename:str) -> None: + + """ Loads a stimulus from a json file. To be used in conjunction with save_stimulus method + + Arguments: + filename: Path to json file. Must include .json suffix + Returns: + None: + """ + path = Path(filename) + + with path.open("r") as input: + stimulus_data = json.load(input) + + params = {key: np.array(param) if type(param) is list else param for key, param in stimulus_data.items()} + + return cls(**params) + class Response(object): """A Response by an AfferentPopulation to a Stimulus. @@ -543,10 +567,4 @@ def psth(self,bin=10.): NxB array of firing rates (N: number of afferents, B: number of bins). """ bins = np.r_[0:self.duration+bin/1000.:bin/1000.] - return np.array(list(map(lambda x:np.histogram(x,bins=bins)[0],self.spikes))) - - def save_stimulus(self, filename:str) -> None: - pass - - def load_stimulus(self, filename:str) -> None: - pass \ No newline at end of file + return np.array(list(map(lambda x:np.histogram(x,bins=bins)[0],self.spikes))) \ No newline at end of file