From 3a4720c657be15523ec45dca0f2e5d581cbe9e4f Mon Sep 17 00:00:00 2001 From: Niclas Esser <nesser@mpifr-bonn.mpg.de> Date: Fri, 2 Aug 2024 11:52:46 +0200 Subject: [PATCH] Pylint changes --- pafsim/chain.py | 41 +++++++++++----------- pafsim/connector.py | 23 ++++++------- pafsim/executor.py | 37 +++++++++++++------- pafsim/processor/__init__.py | 61 ++++++++++++++++++++++++++------- pafsim/processor/_processor.py | 13 ++++--- pafsim/processor/beamformer.py | 10 +++--- pafsim/processor/calibrator.py | 15 ++++---- pafsim/processor/channelizer.py | 15 ++++---- pafsim/processor/correlator.py | 12 +++---- pafsim/processor/generator.py | 24 ++++++------- pafsim/processor/waveform.py | 16 ++++----- pafsim/vector.py | 8 +++-- 12 files changed, 163 insertions(+), 112 deletions(-) diff --git a/pafsim/chain.py b/pafsim/chain.py index 576ef48..18eb7f9 100644 --- a/pafsim/chain.py +++ b/pafsim/chain.py @@ -1,6 +1,6 @@ import os import shutil -from typing import List, Dict +from typing import Dict import networkx as nx import matplotlib.pyplot as plt @@ -20,11 +20,12 @@ class ProcessingChain(): name (str, optional): When creating multiple ProcessingChain objects with dependencies, a name can be provided to refer to dependend ProcessingChains. Defaults to "". """ - self.name: str=name - self.conf: dict=conf - self.setup_flag: bool=False - self.processed: bool=False - self.dag: nx.DiGraph=nx.DiGraph() + self.name: str = name + self.conf: dict = conf + self.setup_flag: bool = False + self.processed: bool = False + self.dag: nx.DiGraph = nx.DiGraph() + self.dag_list: list = [] self.processors: Dict[str,processing.Processor] = {} def setup(self): @@ -81,11 +82,11 @@ class ProcessingChain(): return proc return None - def plot(self, dir: str="", figsize: tuple=(8,4)): + def plot(self, path: str="", figsize: tuple=(8,4)): """Plots the DAG Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). """ fig = plt.figure(num = 1, figsize=figsize) @@ -98,8 +99,8 @@ class ProcessingChain(): linewidths=2, font_size=9) fig.suptitle("Directed acyclic graph (DAG)") - if dir: - fig.savefig(dir + 'dag.png') + if path: + fig.savefig(path + 'dag.png') plt.close() else: plt.show() @@ -117,11 +118,11 @@ class ProcessingChain(): self.processors[pname].process() self.processed = True - def plotAll(self, dir: str="", figsize: tuple=(8,4)): + def plotAll(self, path: str="", figsize: tuple=(8,4)): """Calls all plot methods of the processors Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). Raises: @@ -131,7 +132,7 @@ class ProcessingChain(): if self.processed is False: raise Exception("Chain not processed, use process() before plot()") for pname in self.dag_list: - self.processors[pname].plot(dir, figsize) + self.processors[pname].plot(path, figsize) def save(self, plot: bool=True, overwrite:bool=True): """Stores the configurations and marked vectors of the procesing chains in dedicated folders @@ -145,18 +146,18 @@ class ProcessingChain(): return directory = os.path.abspath(self.conf["location"]) - if overwrite == True and os.path.exists(directory): + if overwrite is True and os.path.exists(directory): shutil.rmtree(directory) if not os.path.exists(directory): os.makedirs(directory) - if directory[-1]!="/": directory+="/" - with open(directory + "/conf.json", "w") as o: + + with open(os.path.join(directory, "conf.json"), "w", encoding="utf-8") as o: o.write(str(self.conf)) - os.mkdir(directory + '/data/') + os.mkdir(os.path.join(directory, 'data/')) if plot: - os.mkdir(directory + '/plot/') - self.plotAll(directory + '/plot/') + os.mkdir(os.path.join(directory, 'plot/')) + self.plotAll(os.path.join(directory, 'plot/')) for tv_name in self.conf["store"]: tv = Vector(self.getProcessor(tv_name)) - tv.store(directory + '/data/') \ No newline at end of file + tv.store(os.path.join(directory, 'data/')) diff --git a/pafsim/connector.py b/pafsim/connector.py index 8240f9a..89fb4dc 100644 --- a/pafsim/connector.py +++ b/pafsim/connector.py @@ -1,10 +1,9 @@ # import numpy as np from pafsim.processor import Processor -class ConnectionError(Exception): +class EdgeConnectionError(Exception): """Exception class used for invalid connections """ - pass class Connector: """The Connector class is used as edges in the ProcessingChain. It transposes output @@ -20,31 +19,31 @@ class Connector: """Connects to Processors and checks if the connection is valid Raises: - ConnectionError: If a connection is invalid + EdgeConnectionError: If a connection is invalid """ if self.a_node.O_FORMAT in self.b_node.I_FORMAT: print("Connection without transpose") - self._assign() + self.assign() elif set(self.a_node.O_FORMAT) in [set(x) for x in self.b_node.I_FORMAT]: print("Connection needs transpose") - self._transpose() - self._assign() + self.transpose() + self.assign() else: - raise ConnectionError("Output format of node A does not match input formats of node B") + raise EdgeConnectionError("Output format of node A does not match input formats of node B") - def _assign(self): + def assign(self): """Assigns the Processors Raises: - ConnectionError: Mismatch of inputs + EdgeConnectionError: Mismatch of inputs """ if self.b_node.N_INPUTS > len(self.b_node.pp): - raise ConnectionError(f"Can not assign more than {self.b_node.N_INPUTS} connections") + raise EdgeConnectionError(f"Can not assign more than {self.b_node.N_INPUTS} connections") self.b_node.pp.append(self.a_node) - def _transpose(self): + def transpose(self): """Transpose the output of the pre processor to the expected input of the post processor ToDo: Not implemented yet """ - pass \ No newline at end of file + diff --git a/pafsim/executor.py b/pafsim/executor.py index e7f0fa0..687755d 100644 --- a/pafsim/executor.py +++ b/pafsim/executor.py @@ -1,6 +1,8 @@ -import networkx as nx +import typing as ty from collections import OrderedDict +import networkx as nx from pafsim.chain import ProcessingChain +from pafsim.vector import Vector class Executor(): """The Executor enables the simulation of comprehensive simulation configuration with nested @@ -12,10 +14,12 @@ class Executor(): Args: conf (dict, optional): The discrition of the simulation. Defaults to None. """ - self.test_vectors = [] - self.chains = [] - self.dag = nx.DiGraph() - self.conf = conf + self.test_vectors: ty.List[Vector] = [] + self.chains: ty.List[ProcessingChain] = [] + self.dag: nx.DiGraph = nx.DiGraph() + self.conf: dict = conf + self.input_vectors: ty.List[Vector] = [] + self.output_vectors: ty.List[Vector] = [] def update(self, conf: dict): """Updates the current configuration @@ -29,7 +33,7 @@ class Executor(): try: self.conf.update(conf) except Exception as e: - raise Exception("Failed to update configuration file with {}".format(e)) + raise Exception(f"Failed to update configuration file with {e}") def _sort_chains(self): """Used to sort the processing chains according to the dependencies speciefied in @@ -74,27 +78,34 @@ class Executor(): self.update(chain.conf) self.chains.append(chain) - def getVectorById(self, id: str): + def getVectorById(self, name: str): """Get a test vector by its ID Args: - id (str): The ID of a test vector + name (str): The ID of a test vector Returns: Vector: The Vector """ for i in self.input_vectors: - if id == i.id: + if name == i.name: return i for i in self.output_vectors: - if id == i.id: + if name == i.name: return i - print("No Vector with id={} loaded".format(id)) + print(f"No Vector with name '{name}' found") return None - def getChain(self, name): + def getChain(self, name: str) -> ProcessingChain: + """Get a registered ProcessingChain object by its name + + Args: + name (str): The name of the ProcessingChain + + Returns: + ProcessingChain: The ProcessingChain object if it exists, otherwise None + """ for chain in self.chains: if chain.name == name: return chain return None - diff --git a/pafsim/processor/__init__.py b/pafsim/processor/__init__.py index af1a20e..7fbdc0c 100644 --- a/pafsim/processor/__init__.py +++ b/pafsim/processor/__init__.py @@ -1,31 +1,68 @@ import numpy as np +from pafsim.processor._processor import Processor +from pafsim.processor.beamformer import Beamformer +from pafsim.processor.calibrator import WeightGenerator +from pafsim.processor.channelizer import Channelizer +from pafsim.processor.correlator import Correlator +from pafsim.processor.waveform import WaveformGenerator +from pafsim.processor.generator import Generator + +def createProcessor(class_name: str, name: str, kwargs: dict) -> Processor: + """Factory function to create a Processor by and name and configuration options + + Args: + class_name (str): The class name of the Processor (e.g. Channelizer) + name (str): The unique name of the processor + kwargs (dict): The configuration options of the processor + + Returns: + Processor: The created processor + """ + return globals()[class_name](name, **kwargs) + +def scale(data: np.ndarray, bits: int) -> np.ndarray: + """Scales a numpy array to passed bit size -def createProcessor(class_name, id, kwargs): - return globals()[class_name](id, **kwargs) + Args: + data (np.ndarray): The array to scale + bits (int): The number of bits -def scale(data, bits): + Returns: + np.ndarray: The rescaled array + """ return (data / np.max(np.abs(data)) * 2**(bits - 1)) -def float2int(data, dtype): +def rescalefloat2int(data: np.ndarray, dtype: np.dtype) -> np.ndarray: + """Rescales a float array to an int array + + Args: + data (np.ndarray): The float array to rescale + dtype (np.dtype): The desired integer data type (e.g. np.int32) + + Returns: + np.ndarray: The converted integer array + """ bits = np.dtype(dtype).itemsize * 8 if np.max(data) > 2**(bits-1): data = scale(data, bits) return data.astype(dtype) -def complex2int2(data, dtype): +def complex2int2(data: np.ndarray, dtype: np.dtype) -> np.ndarray: + """Rescales a float array to an int array + + Args: + data (np.ndarray): The float array to rescale + dtype (np.dtype): The desired integer data type (e.g. np.int32) + + Returns: + np.ndarray: The converted integer array + """ bits = np.dtype(dtype).itemsize * 8 dtype = np.dtype([('real', dtype), ('imag', dtype)]) if np.max(data) > 2**(bits-1): data = scale(data, bits) return data.astype(dtype) -from pafsim.processor._processor import Processor -from pafsim.processor.beamformer import Beamformer -from pafsim.processor.calibrator import WeightGenerator -from pafsim.processor.channelizer import Channelizer -from pafsim.processor.correlator import Correlator -from pafsim.processor.waveform import WaveformGenerator -from pafsim.processor.generator import Generator __all__ = [ "Processor", diff --git a/pafsim/processor/_processor.py b/pafsim/processor/_processor.py index f3c86e3..1b90762 100644 --- a/pafsim/processor/_processor.py +++ b/pafsim/processor/_processor.py @@ -1,7 +1,7 @@ +from abc import ABC, abstractmethod +import os import pickle import numpy as np -import os -from abc import ABC, abstractmethod class Processor(ABC): @@ -16,7 +16,7 @@ class Processor(ABC): """Base constructor of the Processor. Can not be initiatied as this class is abstract Args: - name (str): The ID of the Processor + name (str): The unique name of theProcessor """ self.pp: list[Processor] = [None for __ in range(self.N_INPUT)] self.name: str = name @@ -66,15 +66,14 @@ class Processor(ABC): pass @abstractmethod - def plot(self, dir: str="", figsize: tuple=(8,4)): + def plot(self, path: str="", figsize: tuple=(8,4)): """Abstract method to plot the output array. Needs to be implemented by inherited classes Args: - dir (str, optional): If set stores the plot in the directory. Defaults to "". + path (str, optional): If set stores the plot in the directory. Defaults to "". figsize (tuple, optional): Te size of the figure to plot. Defaults to (8,4). """ - pass def dim(self, label: str) -> int: """Returns the size of the requested dimension @@ -154,4 +153,4 @@ def getDim(label: str, processor: Processor) -> int: idx = processor.O_FORMAT.index(label) except: raise ValueError(f"Label {label} does not exists in {processor.O_FORMAT}") - return processor.shape[idx] \ No newline at end of file + return processor.shape[idx] diff --git a/pafsim/processor/beamformer.py b/pafsim/processor/beamformer.py index 4b1d641..6d83270 100644 --- a/pafsim/processor/beamformer.py +++ b/pafsim/processor/beamformer.py @@ -21,7 +21,7 @@ class Beamformer(Processor): """Construct a Beamformer object Args: - name (str): The ID of the Beamformer + name (str): The unique name of the Beamformer """ super().__init__(name, **kwargs) @@ -76,11 +76,11 @@ class Beamformer(Processor): return np.matmul(i, w.conj()).transpose(0,3,1,2) return np.dot(self.pp[0].output, self.pp[1].output.T) - def plot(self, dir="", figsize=(8,4)): + def plot(self, path="", figsize=(8,4)): """Plotting function to plot the beamformed time series Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). """ sub = [] @@ -93,8 +93,8 @@ class Beamformer(Processor): sub[b*self.dim('P')+p].set_ylabel("Channel") plt.imshow(np.log(np.abs(self.output[:, b, p, :])), interpolation='nearest', aspect='auto') fig.tight_layout() - if dir: - fig.savefig(dir + self.name + '.png') + if path: + fig.savefig(path + self.name + '.png') plt.close() else: plt.show() diff --git a/pafsim/processor/calibrator.py b/pafsim/processor/calibrator.py index d832d5b..8a1914a 100644 --- a/pafsim/processor/calibrator.py +++ b/pafsim/processor/calibrator.py @@ -16,10 +16,11 @@ class WeightGenerator(Processor): """Construct a WeightGenerator object Args: - name (str): The ID of the WeightGenerator + name (str): The unique name of theWeightGenerator kwargs: - beams: The number of beams to produce. This parameter is implicitly determined + beams (int): The number of beams to produce. This parameter is implicitly determined when using the maxsnr processing + mode (str): maxsnr -> maximum signal-to-noise algorithm (default) """ super().__init__(name, **kwargs) self.beam = kwargs.get('beams', 2) @@ -103,11 +104,11 @@ class WeightGenerator(Processor): return data - def plot(self, dir="", figsize=(8,4)): + def plot(self, path="", figsize=(8,4)): """Plotting function to plot the amplitude of the beamweight Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). """ sub = [] @@ -120,8 +121,8 @@ class WeightGenerator(Processor): sub[b*self.dim('P')+p].set_ylabel("Channel") sub[b*self.dim('P')+p].imshow(np.log(np.abs(self.output[b, :, p])),interpolation='nearest', aspect='auto') fig.tight_layout() - if dir: - fig.savefig(dir + self.name + '.png') + if path: + fig.savefig(path + self.name + '.png') plt.close() else: - plt.show() \ No newline at end of file + plt.show() diff --git a/pafsim/processor/channelizer.py b/pafsim/processor/channelizer.py index fef9c37..4af9e00 100644 --- a/pafsim/processor/channelizer.py +++ b/pafsim/processor/channelizer.py @@ -17,7 +17,7 @@ class Channelizer(Processor): """Construct a Generator object Args: - name (str): The ID of the Generator + name (str): The unique name of theGenerator kwargs: taps (int): Number of taps used for the filter. Defaults to 2 channels (int): Number of channels to produce. Defaults to 32 @@ -35,6 +35,7 @@ class Channelizer(Processor): self.pd = kwargs.get('pd', 1) # self.stop_min = kwargs.get('stop_min', 0.00001) + self.coeff = np.array() self.os_factor = self.pn / self.pd self.tap_length = int(self.channels * 2) self.channel_bw = 1/self.channels @@ -97,8 +98,8 @@ class Channelizer(Processor): if end > x.size: break x_p = x[start:end].reshape(self.taps, self.tap_length).T - sum = (x_p * self.coeff).sum(axis=1) - y.append(sum) + summed = (x_p * self.coeff).sum(axis=1) + y.append(summed) offset+=1 return np.asarray(y) @@ -138,11 +139,11 @@ class Channelizer(Processor): reshaped = self.pp[0].output.reshape(self.dim('A'), self.dim('P'), -1, self.channels*2) return np.fft.rfft(reshaped * self.coeff, axis=-1).transpose(3,0,1,2)[1:] - def plot(self, dir="", figsize=(8,4)): + def plot(self, path="", figsize=(8,4)): """Plotting function to plot the channeles vs time Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). """ sub = [] @@ -163,8 +164,8 @@ class Channelizer(Processor): sub[self.dim('A') * self.dim('P')].plot(np.arange(0,len(self.coeff)), self.coeff.flatten()) fig.tight_layout() - if dir: - fig.savefig(dir + self.name + '.png') + if path: + fig.savefig(path + self.name + '.png') plt.close() else: plt.show() diff --git a/pafsim/processor/correlator.py b/pafsim/processor/correlator.py index 34f8a10..5f4b4de 100644 --- a/pafsim/processor/correlator.py +++ b/pafsim/processor/correlator.py @@ -16,7 +16,7 @@ class Correlator(Processor): """Construct a Correlator object Args: - name (str): The ID of the Correlator + name (str): The unique name of theCorrelator kwargs: acc (int): Number of ACMs to create. mode (str): 'acm' -> correlate and build covariance matrices (default) @@ -58,12 +58,12 @@ class Correlator(Processor): o[n] = np.matmul(x[..., n*nacc:(n+1)*nacc], np.conj(x[..., n*nacc:(n+1)*nacc]).transpose(0,1,3,2)) return o - def plot(self, dir="", figsize=(8,4)): + def plot(self, path="", figsize=(8,4)): """Plotting function to plot the amplitude of covariance matrices. Just a sub-set is randomly chosen Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). """ @@ -75,8 +75,8 @@ class Correlator(Processor): sub[i].set_title("Channel " + str(f)) sub[i].imshow(np.log(np.abs(self.output[0, f, 0])),interpolation='nearest', aspect='auto') fig.tight_layout() - if dir: - fig.savefig(dir + self.name + '.png') + if path: + fig.savefig(path + self.name + '.png') plt.close() else: - plt.show() \ No newline at end of file + plt.show() diff --git a/pafsim/processor/generator.py b/pafsim/processor/generator.py index b86bf61..819ef0c 100644 --- a/pafsim/processor/generator.py +++ b/pafsim/processor/generator.py @@ -1,11 +1,10 @@ -import numpy as np -from matplotlib import pyplot as plt -from pafsim.processor._processor import Processor import os +import typing as ty +from matplotlib import pyplot as plt import h5py as h5 import numpy as np -import typing as ty +from pafsim.processor._processor import Processor class FrontendSimFile(h5.File): """ The FrontendSimFile is class to read ACMs and properties from an HDF5 file produced @@ -95,9 +94,8 @@ class FrontendSimFile(h5.File): """ if self.exists(path): return self[path] - else: - print(f"Path {path} does not exists in dataset") - return None + print(f"Path {path} does not exists in dataset") + return None def datasets(self, base_path: str, names: set, suffix: str="acm") -> ty.List[np.ndarray]: """Returns multiple datasets with matching names and suffix within the base path. @@ -170,7 +168,7 @@ class Generator(Processor): """Construct a Generator object Args: - name (str): The ID of the Generator + name (str): The unique name of theGenerator kwargs: dataset (str): Path to an HDF5 file containing the output of the PAF Frontend simulator noise (str): Names of the noise datasets to use for the time series generation @@ -185,6 +183,7 @@ class Generator(Processor): self.rfi = self.conf.get("rfi", [0]) self.width = self.conf.get("width", 10000) self.fs = 0 + self.duration = 0 self.reader = FrontendSimFile(self.dataset) self.antennas = self.reader.nelements self.pol = self.reader.npol @@ -242,11 +241,12 @@ class Generator(Processor): out[:, 0, t*nchan:(t+1)*nchan] = np.fft.ifft(channelized_array[:,:,t], axis=0).T return out - def plot(self, dir="", figsize=(8,4)): +#pylint: disable=R0801 + def plot(self, path="", figsize=(8,4)): """Plotting function to plot the time series Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). """ sub = [] @@ -260,8 +260,8 @@ class Generator(Processor): sub[a*self.pol+p].set_xlabel("Time [s]") sub[a*self.pol+p].plot(taxis, self.output[a]) fig.tight_layout() - if dir: - fig.savefig(dir + self.name + '.png') + if path: + fig.savefig(path + self.name + '.png') plt.close() else: plt.show() diff --git a/pafsim/processor/waveform.py b/pafsim/processor/waveform.py index 45cbbfa..9578394 100644 --- a/pafsim/processor/waveform.py +++ b/pafsim/processor/waveform.py @@ -1,5 +1,5 @@ +from scipy import signal import numpy as np -import scipy.signal as signal import matplotlib.pyplot as plt @@ -15,7 +15,7 @@ class WaveformGenerator(Processor): def __init__(self, name:str, **kwargs): """Construct a WaveformGenerator object Args: - name (str): The ID of the Generator + name (str): The unique name of theGenerator kwargs: fs (float): The sampling rate at which to generate the time series [Hz]. Defaults to 1000 Hz duration (float): The duration of the signal [seconds]. Defaults to 1s @@ -83,7 +83,7 @@ class WaveformGenerator(Processor): o = np.zeros(self.shape) for a in range(self.antennas): for p in range(self.pol): - if type(self.freq) == list: + if isinstance(self.freq, list): for f in self.freq: o[a,p] += self.gain * np.sin(2 * np.pi * f * self.t + self.phase) + self.offset else: @@ -125,11 +125,11 @@ class WaveformGenerator(Processor): # def sawtooth(self): # return self.gain * signal.sawtooth(2 * np.pi * self.period * self.t) - def plot(self, dir="", figsize=(8,4)): + def plot(self, path="", figsize=(8,4)): """Plotting function to plot the time series Args: - dir (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". + path (str, optional): If not set to "" it stores the plot in the given directory. Defaults to "". figsize (tuple, optional): Size of the plotted figure. Defaults to (8,4). """ sub = [] @@ -142,8 +142,8 @@ class WaveformGenerator(Processor): sub[a*self.pol+p].set_xlabel("Time [s]") sub[a*self.pol+p].plot(self.t, self.output[a,p]) fig.tight_layout() - if dir: - fig.savefig(dir + self.name + '.png') + if path: + fig.savefig(path + self.name + '.png') plt.close() else: - plt.show() \ No newline at end of file + plt.show() diff --git a/pafsim/vector.py b/pafsim/vector.py index 326b941..ce308d6 100644 --- a/pafsim/vector.py +++ b/pafsim/vector.py @@ -45,8 +45,8 @@ class Vector(): Args: path (str): The location where to store the Vector """ - self.path = path + self.name + '.pk1' - with open(self.path, 'wb') as f: + path = path + self.name + '.pk1' + with open(path, 'wb') as f: pickle.dump(self, f, pickle.HIGHEST_PROTOCOL) def load(self, path: str): @@ -56,4 +56,6 @@ class Vector(): path (str): The location where the vector was stored """ with open(path, 'rb') as f: - self = pickle.load(f) \ No newline at end of file + loaded = pickle.load(f) + self.__dict__.update(loaded.__dict__) + -- GitLab