pyduino.spectra

  1from pyduino.optimization.nelder_mead import NelderMeadBounded
  2from pyduino.pyduino2 import ReactorManager, chunks, PATHS
  3import numpy as np
  4import json
  5import os
  6import pandas as pd
  7import numpy as np
  8import time
  9from tensorboardX import SummaryWriter
 10from datetime import date, datetime
 11from pyduino.data_parser import RangeParser
 12from collections import OrderedDict
 13from pyduino.utils import bcolors, get_param, partition
 14from pyduino.paths import PATHS
 15from pyduino.log import datetime_to_str, y_to_table, to_markdown_table
 16import traceback
 17import warnings
 18import threading
 19from datetime import datetime
 20import logging
 21
 22__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
 23
 24#Path to spectrum.json
 25SPECTRUM_PATH = os.path.join(__location__,"spectrum.json")
 26#Path to irradiance values
 27IRRADIANCE_PATH = os.path.join(__location__,"irradiance.yaml")
 28
 29logging.basicConfig(
 30    filename='pyduino.log',
 31    filemode='w',
 32    level=PATHS.SYSTEM_PARAMETERS.get('log_level', logging.INFO),
 33)
 34
 35
 36def update_dict(D,A,key):
 37    """
 38    Updates dictionary `D` with values in `A` with key name `key`.
 39    
 40    Args:
 41        D (:obj:dict of :obj:dict): Input dictionary.
 42        A (dict): Dict of values to be added with the same keys as in `D`.
 43        key (str): Key name to assign to values of `A`.
 44    """
 45    for k,v in A.items():
 46        D[k].update({key:v})
 47
 48
 49def dict_subset(D,keys):
 50    """
 51    Extracts a subdictionary from a given dictionary.
 52
 53    Args:
 54        D (dict): Input dictionary.
 55        keys (:obj:list of :obj:str): A list of keys pertaining to the dictionary.
 56    """
 57    return {k:D[k] for k in keys if k in D}
 58
 59def row_subset(rows,keys):
 60    """
 61    Extracts a subset from a `rows` table.
 62
 63    Args: 
 64        rows (:obj:`dict` of :obj:`OrderedDict`): Data obtained from `ReactorManager.log_dados`.
 65        keys (:obj:list of :obj:str): A list of keys.
 66    """
 67    return pd.DataFrame(rows).T.loc[:,keys].T.astype(float).astype(str).to_dict()
 68
 69def seval(x):
 70    try:
 71        return eval(x)
 72    except:
 73        return x
 74
 75def parse_dados(X,param):
 76    """
 77    Parses data from `ReactorManager.log_dados` into an array while attempting to convert the input string into an appropriate data type.
 78
 79    Args:
 80        X (:obj:`dict` of :obj:`OrderedDict`): Data obtained from `ReactorManager.log_dados`.
 81        param (str): Parameter name to be extracted from `X`.
 82    """
 83    return np.array(list(map(seval,map(lambda x: x[1].get(param,0),sorted(X.items(),key=lambda x: x[0])))))
 84
 85class Spectra(RangeParser,ReactorManager,NelderMeadBounded):
 86    def __init__(self,
 87        ranges,
 88        density_param,
 89        brilho_param=None,
 90        maximize=True,
 91        log_name=None,
 92        reset_density=False,
 93        summary_writer=None,
 94        **kwargs
 95        ):
 96        """
 97        Initializes the Spectra class.
 98
 99        Args:
100            ranges (dict): A dictionary of parameters with a two-element list containing the minimum and maximum attainable values for each parameter.
101            density_param (str): The name of the parameter to be used as the density count.
102            brilho_param (float, optional): When nonzero, it will be used to turn optimization on or off. Defaults to None.
103            maximize (bool, optional): Whether to maximize the fitness function. Defaults to True.
104            log_name (str, optional): The name of the log. Defaults to None.
105            reset_density (bool, optional): Whether to reset density values on the reactors at each iteration. Defaults to False.
106            **kwargs: Additional keyword arguments.
107
108        Attributes:
109            spectrum (dict): A dictionary containing the spectrum data.
110            parameters (list): A list of relevant system parameters.
111            titled_parameters (list): A list of relevant system parameters with their titles capitalized.
112            irradiance (str): The irradiance value.
113            ids (list): A list of reactor IDs.
114            sorted_ids (list): A sorted list of reactor IDs.
115            tensorboard_path (str): The path to the tensorboard log.
116            writer (SummaryWriter): The tensorboard summary writer.
117            payload (dict): The payload data.
118            data (None): Placeholder for data.
119            do_gotod (bool): Whether to reset density values.
120            dt (float): The time step value.
121
122        Raises:
123            AssertionError: If the spectrum file does not exist.
124
125        """
126        assert os.path.exists(SPECTRUM_PATH)
127        with open(SPECTRUM_PATH) as jfile:
128            self.spectrum = json.loads(jfile.read())
129        
130        self.parameters = PATHS.SYSTEM_PARAMETERS['relevant_parameters']
131        self.titled_parameters = list(map(lambda x: x.title(),self.parameters))
132        RangeParser.__init__(self,ranges,self.parameters)
133
134        self.irradiance = PATHS.SYSTEM_PARAMETERS['irradiance']
135
136        ReactorManager.__init__(self, kwargs.get('include', None))
137        NelderMeadBounded.__init__(
138            self,
139            population_size=len(self.parameters),
140            ranges=self.ranges_as_list(),
141            rng_seed=kwargs.get('rng_seed',0)
142        )
143        self.sorted_ids = sorted(self.ids)
144        self.log_init(name=log_name)
145        if summary_writer is None:
146            self.tensorboard_path = os.path.join(self.log.prefix, "runs", datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
147            self.writer = SummaryWriter(self.tensorboard_path)
148            logging.info(f"{bcolors.OKGREEN}Created tensorboard log at {self.tensorboard_path}{bcolors.ENDC}")
149        else:
150            self.writer = summary_writer
151            self.tensorboard_path = summary_writer.logdir
152        self.payload = self.population_as_dict if self.payload is None else self.payload
153        self.data = None
154        self.do_gotod = reset_density
155        self.density_param = density_param
156        self.brilho_param = brilho_param
157        self.maximize = maximize
158        self.dt = np.nan
159    def assign_to_reactors(self, x):
160        """
161        Assigns a list of parameters to the reactors.
162
163        Parameters:
164        x (list): The input list to be converted.
165
166        Returns:
167        OrderedDict: An ordered dictionary where the keys are the IDs and the values are the ranges.
168
169        """
170        ids = self.ids[:len(x)]
171        return OrderedDict(
172            zip(
173                ids,
174                map(
175                    lambda u: self.ranges_as_keyed(u),
176                    list(np.round(self.view(x),2))
177                )
178            )
179        )
180    @property
181    def population_as_dict(self):
182        """
183        Converts genome matrix into an appropriate format to send to the reactors.
184        """
185        return self.assign_to_reactors(self.population)
186    
187    @property
188    def power(self):
189        return {reactor_ids: sum(vals[key]*self.irradiance[key] for key in self.irradiance.keys()) for reactor_ids, vals in self.payload.items()}
190
191    def payload_to_matrix(self):
192        return np.nan_to_num(
193            np.array(
194                [[self.payload[i].get(u,np.nan) for u in self.keyed_ranges.keys()] for i in self.ids]
195                ).astype(float)
196            )
197    def data_dict_to_matrix(self,D):
198        return np.nan_to_num(
199            np.array(
200                [[self.D[i].get(u,np.nan) for u in self.parameters] for i in self.ids]
201                ).astype(float)
202            )
203    def F_get(self):
204        """
205        Extracts relevant data from Arduinos.
206        """
207        return self.dados()
208    def F_set(self,x):
209        """
210        Sets parameters to Arduinos.
211        Args:
212            x (:obj:`dict` of :obj:`dict`): Dictionary having reactor id as keys
213            and a dictionary of parameters and their values as values.
214        """
215
216        for _id,params in x.items():
217            for chk in chunks(list(params.items()),3):
218                self.reactors[_id].set(dict(chk))
219                time.sleep(1)
220    def init(self):
221        """
222        Sets payload to the reactors.
223        """
224        self.F_set(self.payload)
225
226    def set_spectrum(self,preset):
227        """
228        Sets all reactors with a preset spectrum contained in `SPECTRUM_PATH`.
229        """
230        command = f"set({','.join(map(lambda x: f'{x[0]},{x[1]}',self.spectrum[preset].items()))})"
231        self.send(command,await_response=False)
232
233    def set_preset_state_spectra(self,*args,**kwargs):
234        self.set_preset_state(*args,**kwargs)
235        self.population = self.inverse_view(self.payload_to_matrix()).astype(int)   
236    
237    def GET(self,tag):
238        """
239        Collects data from Arduinos and logs it to disk.
240        """
241        print("[INFO]","GET",datetime.now().strftime("%c"))
242        self.past_data = self.data.copy() if self.data is not None else pd.DataFrame(self.payload)
243        self.data = self.F_get()
244        self.log.log_many_rows(pd.DataFrame(self.data),tags={'growth_state':tag})
245
246    def log_data(self, i, tags={}):
247        """
248        Logs the tensor values and fitness scores.
249
250        This method iterates over the tensor values and fitness scores and logs them using the writer object.
251        """
252        logging.info(f"LOGGING {datetime.now().strftime('%c')}")
253        data = self.F_get()
254        logging.debug(f"DATA\n{pd.DataFrame(data)}")
255        y = get_param(data, self.density_param, self.reactors)
256        y = {k:float(v) for k,v in y.items()}
257        additional_parameters = {}
258        if PATHS.TENSORBOARD is not None and "additional_parameters" in PATHS.TENSORBOARD:
259            additional_parameters_source = PATHS.TENSORBOARD["additional_parameters"]
260            for param in additional_parameters_source:
261                additional_parameters[param] = get_param(data, param, self.reactors.keys())
262
263        logging.debug(f"ADDITIONAL PARAMETERS {additional_parameters}")
264        
265        P = self.view_g()
266        #Log main parameters
267        for k,(rid, ry) in enumerate(y.items()):
268            self.writer.add_scalar(f'reactor_{rid}/y', float(ry), i)
269            for r_param_id, rparam in enumerate(self.parameters):
270                self.writer.add_scalar(f'reactor_{rid}/{rparam}', float(P[k][r_param_id]), i)
271            for param, value in additional_parameters.items():
272                self.writer.add_scalar(f'reactor_{rid}/{param}', float(value[rid]), i)
273        if self.maximize:
274            self.writer.add_scalar('optima', max(y.values()), i)
275        else:
276            self.writer.add_scalar('optima', min(y.values()), i)
277
278        # Log the DataFrame as a table in text format
279        self.writer.add_text("reactor_state", text_string=to_markdown_table(data), global_step=i)
280        self.log.log_many_rows(data,tags=tags)
281
282    def gotod(self):
283        self.t_gotod_1 = datetime.now()
284        self.send("gotod",await_response=False)
285        logging.debug(f"gotod sent")
286        time.sleep(self.deltaTgotod)
287        self.dt = (datetime.now()-self.t_gotod_1).total_seconds()
288        logging.debug(f"gotod DT {self.dt}")
289
290    # === Optimizer methods ===
291    def ask_oracle(self, X) -> np.ndarray:
292        """
293        Asks the oracle for the fitness of the given input.
294
295        Parameters:
296        X (np.ndarray): The input for which the fitness is to be calculated. Must be already mapped to codomain.
297
298        Returns:
299        np.ndarray: The fitness value calculated by the oracle.
300        """
301
302        assert X.shape[1] == len(self.parameters)
303        assert X.ndim == 2, "X must be a 2D array."
304        partitions = partition(X, len(self.reactors))
305
306        y = pd.Series([])
307
308        for partition in partitions:
309            self.payload = self.assign_to_reactors(partition)
310            reactors = self.payload.keys()
311
312            if self.deltaTgotod is not None and isinstance(self.deltaTgotod, int):
313                self.gotod()
314            data0 = self.F_get()
315            f0 = pd.Series(get_param(data0, self.density_param, reactors))
316
317            self.F_set(self.payload)
318            time.sleep(self.deltaT)
319            data = self.F_get()
320            f = pd.Series(get_param(data, self.density_param, reactors))
321
322            #yield_rate = np.array([(float(f[id])/float(f[id]) - 1)/self.deltaT/self.power[id] for id in reactors]).astype(float)
323            
324            fitness = pd.Series(dict([(id, float(self.power[id])) for id in reactors]))
325
326            partial_y = np.array(((-1)**(self.maximize))*(fitness))
327            y = y.append(partial_y)
328        
329        assert len(y) == len(self.parameters), f"Got different shapes for y: {len(y)} and n params: {len(self.parameters)}"
330        
331        return np.array([y[id] for id in reactors]) 
332    # === * ===
333
334    def run(
335        self,
336        deltaT: float,
337        mode: str = 'optimize',
338        deltaTgotod: int = None
339    ):
340        """
341        Run the bioreactor simulation.
342
343        Args:
344            deltaT (float): The time step for the simulation.
345            mode (str, optional): The mode of operation. Defaults to 'optimize'.
346            deltaTgotod (int, optional): The time interval for performing optimization. Defaults to None.
347
348        Notes:
349            - If mode is 'optimize' and deltaTgotod is less than or equal to 300, a warning will be raised.
350            - If mode is 'free', the number of rows in X must be equal to the number of reactors.
351
352        """
353        if hasattr(self, 'thread') and self.thread.is_alive():
354            self.thread.kill()
355        self.thread = threading.Thread(target=self._run, args=(deltaT, mode, deltaTgotod))
356        self.thread.start()
357    
358    def _run(self, deltaT: float, mode: str = 'optimize', deltaTgotod: int = None):
359        # Checking if gotod time is at least five minutes
360        if mode == "optimize" and isinstance(deltaTgotod, int) and deltaTgotod <= 300:
361            warnings.warn("deltaTgotod should be at least 5 minutes.")
362
363        if mode == "free":
364            assert self.population.shape[0] == len(self.reactors), "X must have the same number of rows as reactors in free mode."
365
366        self.deltaT = deltaT
367        self.deltaTgotod = deltaTgotod
368        self.iteration_counter = 1
369
370        with open("error_traceback.log", "w") as log_file:
371            log_file.write(datetime_to_str(self.log.timestamp) + '\n')
372            try:
373                logging.debug("START")
374                while True:
375                    # growing
376                    self.t_grow_1 = datetime.now()
377                    time.sleep(max(2, deltaT))
378                    self.dt = (datetime.now() - self.t_grow_1).total_seconds()
379                    logging.debug(f"DT {self.dt}")
380                    # Optimizer
381                    if mode == "optimize":
382                        if self.brilho_param is None:
383                            self.step()
384                        else:
385                            brilhos = np.array(list(self.brilho.values()))
386                            if np.all(brilhos > 0):
387                                self.step()
388                            else:
389                                logging.info(f"{self.brilho_param} is off. No optimization steps are being performed.")
390                        if self.deltaTgotod is not None and isinstance(self.deltaTgotod, int):
391                            self.gotod()
392                    self.log_data(self.iteration_counter)
393                    self.iteration_counter += 1
394            except Exception as e:
395                traceback.print_exc(file=log_file)
396                raise(e)
397
398class SpectraManager():
399    """
400    A class that manages multiple Spectra instances.
401
402    Attributes:
403        g (dict): A dictionary containing Spectra instances.
404
405    Methods:
406        call_method: Calls a method on all Spectra instances.
407        get_attr: Retrieves an attribute from all Spectra instances.
408        run: Runs the Spectra instances with specified parameters.
409        reactors: Returns a dictionary of all reactors from Spectra instances.
410    """
411
412    def __init__(self, g:dict):
413        self.g = g
414
415    def __getitem__(self, key):
416        """
417        Allows the SpectraManager class to be indexable.
418
419        Args:
420            key: The key to access the Spectra instance.
421
422        Returns:
423            Spectra: The Spectra instance corresponding to the key.
424        """
425        return self.g[key]
426
427    def call_method(self, method, *args, **kwargs):
428        """
429        Calls a method on all Spectra instances.
430
431        Args:
432            method (str): The name of the method to call.
433            *args: Variable length argument list for the method.
434            **kwargs: Arbitrary keyword arguments for the method.
435        """
436        returns = {}
437        for s in self.g.values():
438            r = getattr(s, method)(*args, **kwargs)
439            returns.update({s:r})
440        return returns
441    
442    def get_attr(self, attr):
443        """
444        Retrieves an attribute from all Spectra instances.
445
446        Args:
447            attr (str): The name of the attribute to retrieve.
448
449        Returns:
450            dict: A dictionary containing the attribute values for each Spectra instance.
451        """
452        return {k:getattr(v, attr) for k,v in self.g.items()}
453    
454    def run(self, deltaT, mode, deltaTgotod):
455        """
456        Runs the Spectra instances with specified parameters.
457
458        Args:
459            deltaT (float): The time step for the simulation.
460            mode (str): The mode of operation for the Spectra instances.
461            deltaTgotod (float): The time step for the go-to-deltaT operation.
462        """
463        for s in self.g.values():
464            s.run(deltaT, mode, deltaTgotod)
465    
466    @property
467    def reactors(self):
468        """
469        Returns a dictionary of all reactors from Spectra instances.
470
471        Returns:
472            dict: A dictionary containing all reactors from Spectra instances.
473        """
474        reactors = {}
475        for s in self.g.values():
476            reactors.update(s.reactors)
477        return reactors
478
479    def __repr__(self) -> str:
480        rstr = bcolors.BOLD + bcolors.OKGREEN + "Main Manager\n" + bcolors.ENDC
481        for v in self.g.values():
482            rstr += f"├── {repr(v)}" + "\n"
483        return rstr
484
485
486if __name__ == "__main__":
487    g = Spectra(**PATHS.HYPERPARAMETERS)
SPECTRUM_PATH = '/home/runner/work/pyduino-parallel/pyduino-parallel/pyduino/spectrum.json'
IRRADIANCE_PATH = '/home/runner/work/pyduino-parallel/pyduino-parallel/pyduino/irradiance.yaml'
def update_dict(D, A, key):
37def update_dict(D,A,key):
38    """
39    Updates dictionary `D` with values in `A` with key name `key`.
40    
41    Args:
42        D (:obj:dict of :obj:dict): Input dictionary.
43        A (dict): Dict of values to be added with the same keys as in `D`.
44        key (str): Key name to assign to values of `A`.
45    """
46    for k,v in A.items():
47        D[k].update({key:v})

Updates dictionary D with values in A with key name key.

Arguments:
  • D (: obj:dict of :obj:dict): Input dictionary.
  • A (dict): Dict of values to be added with the same keys as in D.
  • key (str): Key name to assign to values of A.
def dict_subset(D, keys):
50def dict_subset(D,keys):
51    """
52    Extracts a subdictionary from a given dictionary.
53
54    Args:
55        D (dict): Input dictionary.
56        keys (:obj:list of :obj:str): A list of keys pertaining to the dictionary.
57    """
58    return {k:D[k] for k in keys if k in D}

Extracts a subdictionary from a given dictionary.

Arguments:
  • D (dict): Input dictionary.
  • keys (: obj:list of :obj:str): A list of keys pertaining to the dictionary.
def row_subset(rows, keys):
60def row_subset(rows,keys):
61    """
62    Extracts a subset from a `rows` table.
63
64    Args: 
65        rows (:obj:`dict` of :obj:`OrderedDict`): Data obtained from `ReactorManager.log_dados`.
66        keys (:obj:list of :obj:str): A list of keys.
67    """
68    return pd.DataFrame(rows).T.loc[:,keys].T.astype(float).astype(str).to_dict()

Extracts a subset from a rows table.

Args: rows (dict of OrderedDict): Data obtained from ReactorManager.log_dados. keys (:obj:list of :obj:str): A list of keys.

def seval(x):
70def seval(x):
71    try:
72        return eval(x)
73    except:
74        return x
def parse_dados(X, param):
76def parse_dados(X,param):
77    """
78    Parses data from `ReactorManager.log_dados` into an array while attempting to convert the input string into an appropriate data type.
79
80    Args:
81        X (:obj:`dict` of :obj:`OrderedDict`): Data obtained from `ReactorManager.log_dados`.
82        param (str): Parameter name to be extracted from `X`.
83    """
84    return np.array(list(map(seval,map(lambda x: x[1].get(param,0),sorted(X.items(),key=lambda x: x[0])))))

Parses data from ReactorManager.log_dados into an array while attempting to convert the input string into an appropriate data type.

Arguments:
  • X (dict of OrderedDict): Data obtained from ReactorManager.log_dados.
  • param (str): Parameter name to be extracted from X.
 86class Spectra(RangeParser,ReactorManager,NelderMeadBounded):
 87    def __init__(self,
 88        ranges,
 89        density_param,
 90        brilho_param=None,
 91        maximize=True,
 92        log_name=None,
 93        reset_density=False,
 94        summary_writer=None,
 95        **kwargs
 96        ):
 97        """
 98        Initializes the Spectra class.
 99
100        Args:
101            ranges (dict): A dictionary of parameters with a two-element list containing the minimum and maximum attainable values for each parameter.
102            density_param (str): The name of the parameter to be used as the density count.
103            brilho_param (float, optional): When nonzero, it will be used to turn optimization on or off. Defaults to None.
104            maximize (bool, optional): Whether to maximize the fitness function. Defaults to True.
105            log_name (str, optional): The name of the log. Defaults to None.
106            reset_density (bool, optional): Whether to reset density values on the reactors at each iteration. Defaults to False.
107            **kwargs: Additional keyword arguments.
108
109        Attributes:
110            spectrum (dict): A dictionary containing the spectrum data.
111            parameters (list): A list of relevant system parameters.
112            titled_parameters (list): A list of relevant system parameters with their titles capitalized.
113            irradiance (str): The irradiance value.
114            ids (list): A list of reactor IDs.
115            sorted_ids (list): A sorted list of reactor IDs.
116            tensorboard_path (str): The path to the tensorboard log.
117            writer (SummaryWriter): The tensorboard summary writer.
118            payload (dict): The payload data.
119            data (None): Placeholder for data.
120            do_gotod (bool): Whether to reset density values.
121            dt (float): The time step value.
122
123        Raises:
124            AssertionError: If the spectrum file does not exist.
125
126        """
127        assert os.path.exists(SPECTRUM_PATH)
128        with open(SPECTRUM_PATH) as jfile:
129            self.spectrum = json.loads(jfile.read())
130        
131        self.parameters = PATHS.SYSTEM_PARAMETERS['relevant_parameters']
132        self.titled_parameters = list(map(lambda x: x.title(),self.parameters))
133        RangeParser.__init__(self,ranges,self.parameters)
134
135        self.irradiance = PATHS.SYSTEM_PARAMETERS['irradiance']
136
137        ReactorManager.__init__(self, kwargs.get('include', None))
138        NelderMeadBounded.__init__(
139            self,
140            population_size=len(self.parameters),
141            ranges=self.ranges_as_list(),
142            rng_seed=kwargs.get('rng_seed',0)
143        )
144        self.sorted_ids = sorted(self.ids)
145        self.log_init(name=log_name)
146        if summary_writer is None:
147            self.tensorboard_path = os.path.join(self.log.prefix, "runs", datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
148            self.writer = SummaryWriter(self.tensorboard_path)
149            logging.info(f"{bcolors.OKGREEN}Created tensorboard log at {self.tensorboard_path}{bcolors.ENDC}")
150        else:
151            self.writer = summary_writer
152            self.tensorboard_path = summary_writer.logdir
153        self.payload = self.population_as_dict if self.payload is None else self.payload
154        self.data = None
155        self.do_gotod = reset_density
156        self.density_param = density_param
157        self.brilho_param = brilho_param
158        self.maximize = maximize
159        self.dt = np.nan
160    def assign_to_reactors(self, x):
161        """
162        Assigns a list of parameters to the reactors.
163
164        Parameters:
165        x (list): The input list to be converted.
166
167        Returns:
168        OrderedDict: An ordered dictionary where the keys are the IDs and the values are the ranges.
169
170        """
171        ids = self.ids[:len(x)]
172        return OrderedDict(
173            zip(
174                ids,
175                map(
176                    lambda u: self.ranges_as_keyed(u),
177                    list(np.round(self.view(x),2))
178                )
179            )
180        )
181    @property
182    def population_as_dict(self):
183        """
184        Converts genome matrix into an appropriate format to send to the reactors.
185        """
186        return self.assign_to_reactors(self.population)
187    
188    @property
189    def power(self):
190        return {reactor_ids: sum(vals[key]*self.irradiance[key] for key in self.irradiance.keys()) for reactor_ids, vals in self.payload.items()}
191
192    def payload_to_matrix(self):
193        return np.nan_to_num(
194            np.array(
195                [[self.payload[i].get(u,np.nan) for u in self.keyed_ranges.keys()] for i in self.ids]
196                ).astype(float)
197            )
198    def data_dict_to_matrix(self,D):
199        return np.nan_to_num(
200            np.array(
201                [[self.D[i].get(u,np.nan) for u in self.parameters] for i in self.ids]
202                ).astype(float)
203            )
204    def F_get(self):
205        """
206        Extracts relevant data from Arduinos.
207        """
208        return self.dados()
209    def F_set(self,x):
210        """
211        Sets parameters to Arduinos.
212        Args:
213            x (:obj:`dict` of :obj:`dict`): Dictionary having reactor id as keys
214            and a dictionary of parameters and their values as values.
215        """
216
217        for _id,params in x.items():
218            for chk in chunks(list(params.items()),3):
219                self.reactors[_id].set(dict(chk))
220                time.sleep(1)
221    def init(self):
222        """
223        Sets payload to the reactors.
224        """
225        self.F_set(self.payload)
226
227    def set_spectrum(self,preset):
228        """
229        Sets all reactors with a preset spectrum contained in `SPECTRUM_PATH`.
230        """
231        command = f"set({','.join(map(lambda x: f'{x[0]},{x[1]}',self.spectrum[preset].items()))})"
232        self.send(command,await_response=False)
233
234    def set_preset_state_spectra(self,*args,**kwargs):
235        self.set_preset_state(*args,**kwargs)
236        self.population = self.inverse_view(self.payload_to_matrix()).astype(int)   
237    
238    def GET(self,tag):
239        """
240        Collects data from Arduinos and logs it to disk.
241        """
242        print("[INFO]","GET",datetime.now().strftime("%c"))
243        self.past_data = self.data.copy() if self.data is not None else pd.DataFrame(self.payload)
244        self.data = self.F_get()
245        self.log.log_many_rows(pd.DataFrame(self.data),tags={'growth_state':tag})
246
247    def log_data(self, i, tags={}):
248        """
249        Logs the tensor values and fitness scores.
250
251        This method iterates over the tensor values and fitness scores and logs them using the writer object.
252        """
253        logging.info(f"LOGGING {datetime.now().strftime('%c')}")
254        data = self.F_get()
255        logging.debug(f"DATA\n{pd.DataFrame(data)}")
256        y = get_param(data, self.density_param, self.reactors)
257        y = {k:float(v) for k,v in y.items()}
258        additional_parameters = {}
259        if PATHS.TENSORBOARD is not None and "additional_parameters" in PATHS.TENSORBOARD:
260            additional_parameters_source = PATHS.TENSORBOARD["additional_parameters"]
261            for param in additional_parameters_source:
262                additional_parameters[param] = get_param(data, param, self.reactors.keys())
263
264        logging.debug(f"ADDITIONAL PARAMETERS {additional_parameters}")
265        
266        P = self.view_g()
267        #Log main parameters
268        for k,(rid, ry) in enumerate(y.items()):
269            self.writer.add_scalar(f'reactor_{rid}/y', float(ry), i)
270            for r_param_id, rparam in enumerate(self.parameters):
271                self.writer.add_scalar(f'reactor_{rid}/{rparam}', float(P[k][r_param_id]), i)
272            for param, value in additional_parameters.items():
273                self.writer.add_scalar(f'reactor_{rid}/{param}', float(value[rid]), i)
274        if self.maximize:
275            self.writer.add_scalar('optima', max(y.values()), i)
276        else:
277            self.writer.add_scalar('optima', min(y.values()), i)
278
279        # Log the DataFrame as a table in text format
280        self.writer.add_text("reactor_state", text_string=to_markdown_table(data), global_step=i)
281        self.log.log_many_rows(data,tags=tags)
282
283    def gotod(self):
284        self.t_gotod_1 = datetime.now()
285        self.send("gotod",await_response=False)
286        logging.debug(f"gotod sent")
287        time.sleep(self.deltaTgotod)
288        self.dt = (datetime.now()-self.t_gotod_1).total_seconds()
289        logging.debug(f"gotod DT {self.dt}")
290
291    # === Optimizer methods ===
292    def ask_oracle(self, X) -> np.ndarray:
293        """
294        Asks the oracle for the fitness of the given input.
295
296        Parameters:
297        X (np.ndarray): The input for which the fitness is to be calculated. Must be already mapped to codomain.
298
299        Returns:
300        np.ndarray: The fitness value calculated by the oracle.
301        """
302
303        assert X.shape[1] == len(self.parameters)
304        assert X.ndim == 2, "X must be a 2D array."
305        partitions = partition(X, len(self.reactors))
306
307        y = pd.Series([])
308
309        for partition in partitions:
310            self.payload = self.assign_to_reactors(partition)
311            reactors = self.payload.keys()
312
313            if self.deltaTgotod is not None and isinstance(self.deltaTgotod, int):
314                self.gotod()
315            data0 = self.F_get()
316            f0 = pd.Series(get_param(data0, self.density_param, reactors))
317
318            self.F_set(self.payload)
319            time.sleep(self.deltaT)
320            data = self.F_get()
321            f = pd.Series(get_param(data, self.density_param, reactors))
322
323            #yield_rate = np.array([(float(f[id])/float(f[id]) - 1)/self.deltaT/self.power[id] for id in reactors]).astype(float)
324            
325            fitness = pd.Series(dict([(id, float(self.power[id])) for id in reactors]))
326
327            partial_y = np.array(((-1)**(self.maximize))*(fitness))
328            y = y.append(partial_y)
329        
330        assert len(y) == len(self.parameters), f"Got different shapes for y: {len(y)} and n params: {len(self.parameters)}"
331        
332        return np.array([y[id] for id in reactors]) 
333    # === * ===
334
335    def run(
336        self,
337        deltaT: float,
338        mode: str = 'optimize',
339        deltaTgotod: int = None
340    ):
341        """
342        Run the bioreactor simulation.
343
344        Args:
345            deltaT (float): The time step for the simulation.
346            mode (str, optional): The mode of operation. Defaults to 'optimize'.
347            deltaTgotod (int, optional): The time interval for performing optimization. Defaults to None.
348
349        Notes:
350            - If mode is 'optimize' and deltaTgotod is less than or equal to 300, a warning will be raised.
351            - If mode is 'free', the number of rows in X must be equal to the number of reactors.
352
353        """
354        if hasattr(self, 'thread') and self.thread.is_alive():
355            self.thread.kill()
356        self.thread = threading.Thread(target=self._run, args=(deltaT, mode, deltaTgotod))
357        self.thread.start()
358    
359    def _run(self, deltaT: float, mode: str = 'optimize', deltaTgotod: int = None):
360        # Checking if gotod time is at least five minutes
361        if mode == "optimize" and isinstance(deltaTgotod, int) and deltaTgotod <= 300:
362            warnings.warn("deltaTgotod should be at least 5 minutes.")
363
364        if mode == "free":
365            assert self.population.shape[0] == len(self.reactors), "X must have the same number of rows as reactors in free mode."
366
367        self.deltaT = deltaT
368        self.deltaTgotod = deltaTgotod
369        self.iteration_counter = 1
370
371        with open("error_traceback.log", "w") as log_file:
372            log_file.write(datetime_to_str(self.log.timestamp) + '\n')
373            try:
374                logging.debug("START")
375                while True:
376                    # growing
377                    self.t_grow_1 = datetime.now()
378                    time.sleep(max(2, deltaT))
379                    self.dt = (datetime.now() - self.t_grow_1).total_seconds()
380                    logging.debug(f"DT {self.dt}")
381                    # Optimizer
382                    if mode == "optimize":
383                        if self.brilho_param is None:
384                            self.step()
385                        else:
386                            brilhos = np.array(list(self.brilho.values()))
387                            if np.all(brilhos > 0):
388                                self.step()
389                            else:
390                                logging.info(f"{self.brilho_param} is off. No optimization steps are being performed.")
391                        if self.deltaTgotod is not None and isinstance(self.deltaTgotod, int):
392                            self.gotod()
393                    self.log_data(self.iteration_counter)
394                    self.iteration_counter += 1
395            except Exception as e:
396                traceback.print_exc(file=log_file)
397                raise(e)

A class that manages multiple reactors.

Attributes:
  • pinged (bool): Indicates if the reactors have been pinged.
  • network (str): The network address of the reactors.
  • port (int): The port number of the reactors.
  • exclude (list): A list of reactors to exclude.
  • reactors (dict): A dictionary of reactor objects.
  • servers (dict): A dictionary of server addresses.
  • header (list): A list of header values.
  • payload (dict): A dictionary of payload values.
  • connected (bool): Indicates if the reactors are connected.
  • log (log): A log object for logging data.
Methods:

__init__(self, include: dict = None): Initializes the ReactorManager object. ids(self) -> list: Returns a list of reactor IDs. send(self, command, await_response=True, *kwargs): Sends a command to the reactors. send_parallel(self, command, delay, await_response=True): Sends a command to the reactors in parallel. set(self, data=None, *kwargs): Sets data on the reactors. get(self, key=None): Gets data from the reactors. connect(self): Connects to the reactors. reset(self): Resets the reactors. reboot(self): Reboots the reactors. horacerta(self): Updates Arduino clocks with the clock of the current system. log_init(self, *kwargs): Creates log directories for each Arduino. dados(self, save_cache=True): Gets data from the Arduinos. log_dados(self, save_cache=True): Logs output of dados in CSV format. set_preset_state(self, path="preset_state.csv", sep=" ", chunksize=4, params=PATHS.REACTOR_PARAMETERS, *kwargs): Prepares Arduinos with preset parameters from a CSV file. calibrate(self, deltaT=120, dir="calibrate"): Runs curva and dumps the result into txts.

Spectra( ranges, density_param, brilho_param=None, maximize=True, log_name=None, reset_density=False, summary_writer=None, **kwargs)
 87    def __init__(self,
 88        ranges,
 89        density_param,
 90        brilho_param=None,
 91        maximize=True,
 92        log_name=None,
 93        reset_density=False,
 94        summary_writer=None,
 95        **kwargs
 96        ):
 97        """
 98        Initializes the Spectra class.
 99
100        Args:
101            ranges (dict): A dictionary of parameters with a two-element list containing the minimum and maximum attainable values for each parameter.
102            density_param (str): The name of the parameter to be used as the density count.
103            brilho_param (float, optional): When nonzero, it will be used to turn optimization on or off. Defaults to None.
104            maximize (bool, optional): Whether to maximize the fitness function. Defaults to True.
105            log_name (str, optional): The name of the log. Defaults to None.
106            reset_density (bool, optional): Whether to reset density values on the reactors at each iteration. Defaults to False.
107            **kwargs: Additional keyword arguments.
108
109        Attributes:
110            spectrum (dict): A dictionary containing the spectrum data.
111            parameters (list): A list of relevant system parameters.
112            titled_parameters (list): A list of relevant system parameters with their titles capitalized.
113            irradiance (str): The irradiance value.
114            ids (list): A list of reactor IDs.
115            sorted_ids (list): A sorted list of reactor IDs.
116            tensorboard_path (str): The path to the tensorboard log.
117            writer (SummaryWriter): The tensorboard summary writer.
118            payload (dict): The payload data.
119            data (None): Placeholder for data.
120            do_gotod (bool): Whether to reset density values.
121            dt (float): The time step value.
122
123        Raises:
124            AssertionError: If the spectrum file does not exist.
125
126        """
127        assert os.path.exists(SPECTRUM_PATH)
128        with open(SPECTRUM_PATH) as jfile:
129            self.spectrum = json.loads(jfile.read())
130        
131        self.parameters = PATHS.SYSTEM_PARAMETERS['relevant_parameters']
132        self.titled_parameters = list(map(lambda x: x.title(),self.parameters))
133        RangeParser.__init__(self,ranges,self.parameters)
134
135        self.irradiance = PATHS.SYSTEM_PARAMETERS['irradiance']
136
137        ReactorManager.__init__(self, kwargs.get('include', None))
138        NelderMeadBounded.__init__(
139            self,
140            population_size=len(self.parameters),
141            ranges=self.ranges_as_list(),
142            rng_seed=kwargs.get('rng_seed',0)
143        )
144        self.sorted_ids = sorted(self.ids)
145        self.log_init(name=log_name)
146        if summary_writer is None:
147            self.tensorboard_path = os.path.join(self.log.prefix, "runs", datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
148            self.writer = SummaryWriter(self.tensorboard_path)
149            logging.info(f"{bcolors.OKGREEN}Created tensorboard log at {self.tensorboard_path}{bcolors.ENDC}")
150        else:
151            self.writer = summary_writer
152            self.tensorboard_path = summary_writer.logdir
153        self.payload = self.population_as_dict if self.payload is None else self.payload
154        self.data = None
155        self.do_gotod = reset_density
156        self.density_param = density_param
157        self.brilho_param = brilho_param
158        self.maximize = maximize
159        self.dt = np.nan

Initializes the Spectra class.

Arguments:
  • ranges (dict): A dictionary of parameters with a two-element list containing the minimum and maximum attainable values for each parameter.
  • density_param (str): The name of the parameter to be used as the density count.
  • brilho_param (float, optional): When nonzero, it will be used to turn optimization on or off. Defaults to None.
  • maximize (bool, optional): Whether to maximize the fitness function. Defaults to True.
  • log_name (str, optional): The name of the log. Defaults to None.
  • reset_density (bool, optional): Whether to reset density values on the reactors at each iteration. Defaults to False.
  • **kwargs: Additional keyword arguments.
Attributes:
  • spectrum (dict): A dictionary containing the spectrum data.
  • parameters (list): A list of relevant system parameters.
  • titled_parameters (list): A list of relevant system parameters with their titles capitalized.
  • irradiance (str): The irradiance value.
  • ids (list): A list of reactor IDs.
  • sorted_ids (list): A sorted list of reactor IDs.
  • tensorboard_path (str): The path to the tensorboard log.
  • writer (SummaryWriter): The tensorboard summary writer.
  • payload (dict): The payload data.
  • data (None): Placeholder for data.
  • do_gotod (bool): Whether to reset density values.
  • dt (float): The time step value.
Raises:
  • AssertionError: If the spectrum file does not exist.
parameters
titled_parameters
irradiance
sorted_ids
payload
data
do_gotod
density_param
brilho_param
maximize
dt
def assign_to_reactors(self, x):
160    def assign_to_reactors(self, x):
161        """
162        Assigns a list of parameters to the reactors.
163
164        Parameters:
165        x (list): The input list to be converted.
166
167        Returns:
168        OrderedDict: An ordered dictionary where the keys are the IDs and the values are the ranges.
169
170        """
171        ids = self.ids[:len(x)]
172        return OrderedDict(
173            zip(
174                ids,
175                map(
176                    lambda u: self.ranges_as_keyed(u),
177                    list(np.round(self.view(x),2))
178                )
179            )
180        )

Assigns a list of parameters to the reactors.

Parameters: x (list): The input list to be converted.

Returns: OrderedDict: An ordered dictionary where the keys are the IDs and the values are the ranges.

population_as_dict
181    @property
182    def population_as_dict(self):
183        """
184        Converts genome matrix into an appropriate format to send to the reactors.
185        """
186        return self.assign_to_reactors(self.population)

Converts genome matrix into an appropriate format to send to the reactors.

power
188    @property
189    def power(self):
190        return {reactor_ids: sum(vals[key]*self.irradiance[key] for key in self.irradiance.keys()) for reactor_ids, vals in self.payload.items()}
def payload_to_matrix(self):
192    def payload_to_matrix(self):
193        return np.nan_to_num(
194            np.array(
195                [[self.payload[i].get(u,np.nan) for u in self.keyed_ranges.keys()] for i in self.ids]
196                ).astype(float)
197            )
def data_dict_to_matrix(self, D):
198    def data_dict_to_matrix(self,D):
199        return np.nan_to_num(
200            np.array(
201                [[self.D[i].get(u,np.nan) for u in self.parameters] for i in self.ids]
202                ).astype(float)
203            )
def F_get(self):
204    def F_get(self):
205        """
206        Extracts relevant data from Arduinos.
207        """
208        return self.dados()

Extracts relevant data from Arduinos.

def F_set(self, x):
209    def F_set(self,x):
210        """
211        Sets parameters to Arduinos.
212        Args:
213            x (:obj:`dict` of :obj:`dict`): Dictionary having reactor id as keys
214            and a dictionary of parameters and their values as values.
215        """
216
217        for _id,params in x.items():
218            for chk in chunks(list(params.items()),3):
219                self.reactors[_id].set(dict(chk))
220                time.sleep(1)

Sets parameters to Arduinos.

Arguments:
  • x (dict of dict): Dictionary having reactor id as keys
  • and a dictionary of parameters and their values as values.
def init(self):
221    def init(self):
222        """
223        Sets payload to the reactors.
224        """
225        self.F_set(self.payload)

Sets payload to the reactors.

def set_spectrum(self, preset):
227    def set_spectrum(self,preset):
228        """
229        Sets all reactors with a preset spectrum contained in `SPECTRUM_PATH`.
230        """
231        command = f"set({','.join(map(lambda x: f'{x[0]},{x[1]}',self.spectrum[preset].items()))})"
232        self.send(command,await_response=False)

Sets all reactors with a preset spectrum contained in SPECTRUM_PATH.

def set_preset_state_spectra(self, *args, **kwargs):
234    def set_preset_state_spectra(self,*args,**kwargs):
235        self.set_preset_state(*args,**kwargs)
236        self.population = self.inverse_view(self.payload_to_matrix()).astype(int)   
def GET(self, tag):
238    def GET(self,tag):
239        """
240        Collects data from Arduinos and logs it to disk.
241        """
242        print("[INFO]","GET",datetime.now().strftime("%c"))
243        self.past_data = self.data.copy() if self.data is not None else pd.DataFrame(self.payload)
244        self.data = self.F_get()
245        self.log.log_many_rows(pd.DataFrame(self.data),tags={'growth_state':tag})

Collects data from Arduinos and logs it to disk.

def log_data(self, i, tags={}):
247    def log_data(self, i, tags={}):
248        """
249        Logs the tensor values and fitness scores.
250
251        This method iterates over the tensor values and fitness scores and logs them using the writer object.
252        """
253        logging.info(f"LOGGING {datetime.now().strftime('%c')}")
254        data = self.F_get()
255        logging.debug(f"DATA\n{pd.DataFrame(data)}")
256        y = get_param(data, self.density_param, self.reactors)
257        y = {k:float(v) for k,v in y.items()}
258        additional_parameters = {}
259        if PATHS.TENSORBOARD is not None and "additional_parameters" in PATHS.TENSORBOARD:
260            additional_parameters_source = PATHS.TENSORBOARD["additional_parameters"]
261            for param in additional_parameters_source:
262                additional_parameters[param] = get_param(data, param, self.reactors.keys())
263
264        logging.debug(f"ADDITIONAL PARAMETERS {additional_parameters}")
265        
266        P = self.view_g()
267        #Log main parameters
268        for k,(rid, ry) in enumerate(y.items()):
269            self.writer.add_scalar(f'reactor_{rid}/y', float(ry), i)
270            for r_param_id, rparam in enumerate(self.parameters):
271                self.writer.add_scalar(f'reactor_{rid}/{rparam}', float(P[k][r_param_id]), i)
272            for param, value in additional_parameters.items():
273                self.writer.add_scalar(f'reactor_{rid}/{param}', float(value[rid]), i)
274        if self.maximize:
275            self.writer.add_scalar('optima', max(y.values()), i)
276        else:
277            self.writer.add_scalar('optima', min(y.values()), i)
278
279        # Log the DataFrame as a table in text format
280        self.writer.add_text("reactor_state", text_string=to_markdown_table(data), global_step=i)
281        self.log.log_many_rows(data,tags=tags)

Logs the tensor values and fitness scores.

This method iterates over the tensor values and fitness scores and logs them using the writer object.

def gotod(self):
283    def gotod(self):
284        self.t_gotod_1 = datetime.now()
285        self.send("gotod",await_response=False)
286        logging.debug(f"gotod sent")
287        time.sleep(self.deltaTgotod)
288        self.dt = (datetime.now()-self.t_gotod_1).total_seconds()
289        logging.debug(f"gotod DT {self.dt}")
def ask_oracle(self, X) -> numpy.ndarray:
292    def ask_oracle(self, X) -> np.ndarray:
293        """
294        Asks the oracle for the fitness of the given input.
295
296        Parameters:
297        X (np.ndarray): The input for which the fitness is to be calculated. Must be already mapped to codomain.
298
299        Returns:
300        np.ndarray: The fitness value calculated by the oracle.
301        """
302
303        assert X.shape[1] == len(self.parameters)
304        assert X.ndim == 2, "X must be a 2D array."
305        partitions = partition(X, len(self.reactors))
306
307        y = pd.Series([])
308
309        for partition in partitions:
310            self.payload = self.assign_to_reactors(partition)
311            reactors = self.payload.keys()
312
313            if self.deltaTgotod is not None and isinstance(self.deltaTgotod, int):
314                self.gotod()
315            data0 = self.F_get()
316            f0 = pd.Series(get_param(data0, self.density_param, reactors))
317
318            self.F_set(self.payload)
319            time.sleep(self.deltaT)
320            data = self.F_get()
321            f = pd.Series(get_param(data, self.density_param, reactors))
322
323            #yield_rate = np.array([(float(f[id])/float(f[id]) - 1)/self.deltaT/self.power[id] for id in reactors]).astype(float)
324            
325            fitness = pd.Series(dict([(id, float(self.power[id])) for id in reactors]))
326
327            partial_y = np.array(((-1)**(self.maximize))*(fitness))
328            y = y.append(partial_y)
329        
330        assert len(y) == len(self.parameters), f"Got different shapes for y: {len(y)} and n params: {len(self.parameters)}"
331        
332        return np.array([y[id] for id in reactors]) 

Asks the oracle for the fitness of the given input.

Parameters: X (np.ndarray): The input for which the fitness is to be calculated. Must be already mapped to codomain.

Returns: np.ndarray: The fitness value calculated by the oracle.

def run(self, deltaT: float, mode: str = 'optimize', deltaTgotod: int = None):
335    def run(
336        self,
337        deltaT: float,
338        mode: str = 'optimize',
339        deltaTgotod: int = None
340    ):
341        """
342        Run the bioreactor simulation.
343
344        Args:
345            deltaT (float): The time step for the simulation.
346            mode (str, optional): The mode of operation. Defaults to 'optimize'.
347            deltaTgotod (int, optional): The time interval for performing optimization. Defaults to None.
348
349        Notes:
350            - If mode is 'optimize' and deltaTgotod is less than or equal to 300, a warning will be raised.
351            - If mode is 'free', the number of rows in X must be equal to the number of reactors.
352
353        """
354        if hasattr(self, 'thread') and self.thread.is_alive():
355            self.thread.kill()
356        self.thread = threading.Thread(target=self._run, args=(deltaT, mode, deltaTgotod))
357        self.thread.start()

Run the bioreactor simulation.

Arguments:
  • deltaT (float): The time step for the simulation.
  • mode (str, optional): The mode of operation. Defaults to 'optimize'.
  • deltaTgotod (int, optional): The time interval for performing optimization. Defaults to None.
Notes:
  • If mode is 'optimize' and deltaTgotod is less than or equal to 300, a warning will be raised.
  • If mode is 'free', the number of rows in X must be equal to the number of reactors.
class SpectraManager:
399class SpectraManager():
400    """
401    A class that manages multiple Spectra instances.
402
403    Attributes:
404        g (dict): A dictionary containing Spectra instances.
405
406    Methods:
407        call_method: Calls a method on all Spectra instances.
408        get_attr: Retrieves an attribute from all Spectra instances.
409        run: Runs the Spectra instances with specified parameters.
410        reactors: Returns a dictionary of all reactors from Spectra instances.
411    """
412
413    def __init__(self, g:dict):
414        self.g = g
415
416    def __getitem__(self, key):
417        """
418        Allows the SpectraManager class to be indexable.
419
420        Args:
421            key: The key to access the Spectra instance.
422
423        Returns:
424            Spectra: The Spectra instance corresponding to the key.
425        """
426        return self.g[key]
427
428    def call_method(self, method, *args, **kwargs):
429        """
430        Calls a method on all Spectra instances.
431
432        Args:
433            method (str): The name of the method to call.
434            *args: Variable length argument list for the method.
435            **kwargs: Arbitrary keyword arguments for the method.
436        """
437        returns = {}
438        for s in self.g.values():
439            r = getattr(s, method)(*args, **kwargs)
440            returns.update({s:r})
441        return returns
442    
443    def get_attr(self, attr):
444        """
445        Retrieves an attribute from all Spectra instances.
446
447        Args:
448            attr (str): The name of the attribute to retrieve.
449
450        Returns:
451            dict: A dictionary containing the attribute values for each Spectra instance.
452        """
453        return {k:getattr(v, attr) for k,v in self.g.items()}
454    
455    def run(self, deltaT, mode, deltaTgotod):
456        """
457        Runs the Spectra instances with specified parameters.
458
459        Args:
460            deltaT (float): The time step for the simulation.
461            mode (str): The mode of operation for the Spectra instances.
462            deltaTgotod (float): The time step for the go-to-deltaT operation.
463        """
464        for s in self.g.values():
465            s.run(deltaT, mode, deltaTgotod)
466    
467    @property
468    def reactors(self):
469        """
470        Returns a dictionary of all reactors from Spectra instances.
471
472        Returns:
473            dict: A dictionary containing all reactors from Spectra instances.
474        """
475        reactors = {}
476        for s in self.g.values():
477            reactors.update(s.reactors)
478        return reactors
479
480    def __repr__(self) -> str:
481        rstr = bcolors.BOLD + bcolors.OKGREEN + "Main Manager\n" + bcolors.ENDC
482        for v in self.g.values():
483            rstr += f"├── {repr(v)}" + "\n"
484        return rstr

A class that manages multiple Spectra instances.

Attributes:
  • g (dict): A dictionary containing Spectra instances.
Methods:

call_method: Calls a method on all Spectra instances. get_attr: Retrieves an attribute from all Spectra instances. run: Runs the Spectra instances with specified parameters. reactors: Returns a dictionary of all reactors from Spectra instances.

SpectraManager(g: dict)
413    def __init__(self, g:dict):
414        self.g = g
g
def call_method(self, method, *args, **kwargs):
428    def call_method(self, method, *args, **kwargs):
429        """
430        Calls a method on all Spectra instances.
431
432        Args:
433            method (str): The name of the method to call.
434            *args: Variable length argument list for the method.
435            **kwargs: Arbitrary keyword arguments for the method.
436        """
437        returns = {}
438        for s in self.g.values():
439            r = getattr(s, method)(*args, **kwargs)
440            returns.update({s:r})
441        return returns

Calls a method on all Spectra instances.

Arguments:
  • method (str): The name of the method to call.
  • *args: Variable length argument list for the method.
  • **kwargs: Arbitrary keyword arguments for the method.
def get_attr(self, attr):
443    def get_attr(self, attr):
444        """
445        Retrieves an attribute from all Spectra instances.
446
447        Args:
448            attr (str): The name of the attribute to retrieve.
449
450        Returns:
451            dict: A dictionary containing the attribute values for each Spectra instance.
452        """
453        return {k:getattr(v, attr) for k,v in self.g.items()}

Retrieves an attribute from all Spectra instances.

Arguments:
  • attr (str): The name of the attribute to retrieve.
Returns:

dict: A dictionary containing the attribute values for each Spectra instance.

def run(self, deltaT, mode, deltaTgotod):
455    def run(self, deltaT, mode, deltaTgotod):
456        """
457        Runs the Spectra instances with specified parameters.
458
459        Args:
460            deltaT (float): The time step for the simulation.
461            mode (str): The mode of operation for the Spectra instances.
462            deltaTgotod (float): The time step for the go-to-deltaT operation.
463        """
464        for s in self.g.values():
465            s.run(deltaT, mode, deltaTgotod)

Runs the Spectra instances with specified parameters.

Arguments:
  • deltaT (float): The time step for the simulation.
  • mode (str): The mode of operation for the Spectra instances.
  • deltaTgotod (float): The time step for the go-to-deltaT operation.
reactors
467    @property
468    def reactors(self):
469        """
470        Returns a dictionary of all reactors from Spectra instances.
471
472        Returns:
473            dict: A dictionary containing all reactors from Spectra instances.
474        """
475        reactors = {}
476        for s in self.g.values():
477            reactors.update(s.reactors)
478        return reactors

Returns a dictionary of all reactors from Spectra instances.

Returns:

dict: A dictionary containing all reactors from Spectra instances.