From eb9e5b816d8f95a9f6116b86ef15f5c4cb9bd1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 9 Mar 2024 23:59:10 -0300 Subject: [PATCH 01/54] Implemented Nelder Mead and Gradient Descent --- .gitignore | 162 +++++++++++++++++++++++ pyduino/__init__.py | 3 +- pyduino/optimization/__init__.py | 62 +++++++++ pyduino/optimization/gradient_descent.py | 71 ++++++++++ pyduino/optimization/nelder_mead.py | 112 ++++++++++++++++ tests/test_linmap.py | 13 ++ tests/test_optim.py | 28 ++++ 7 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 pyduino/optimization/__init__.py create mode 100644 pyduino/optimization/gradient_descent.py create mode 100644 pyduino/optimization/nelder_mead.py create mode 100644 tests/test_linmap.py create mode 100644 tests/test_optim.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..704ae3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +logs/ + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/pyduino/__init__.py b/pyduino/__init__.py index 0807791..355c638 100644 --- a/pyduino/__init__.py +++ b/pyduino/__init__.py @@ -1,9 +1,10 @@ import logging +import os logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s', handlers=[ - logging.FileHandler('/var/log/pyduino/slave.log'), + logging.FileHandler(os.environ.get('PYDUINO_SLAVE_LOG','/var/log/pyduino/slave.log')), logging.StreamHandler() ] ) \ No newline at end of file diff --git a/pyduino/optimization/__init__.py b/pyduino/optimization/__init__.py new file mode 100644 index 0000000..ecc5f6f --- /dev/null +++ b/pyduino/optimization/__init__.py @@ -0,0 +1,62 @@ +from typing import Callable +from abc import ABC, abstractmethod +import warnings +import numpy as np + +def linmap(domain: list[tuple[float, float]], codomain: list[tuple[float, float]]) -> Callable[[np.ndarray], np.ndarray]: + """ + Linear mapping from domain to codomain. + domain list of pairs: + Maxima and minima that each parameter can cover. + Example: [(0, 10), (0, 10)] for two parameters ranging from 0 to 10. + codomain list of pairs: + Maxima and minima that each parameter can cover. + Example: [(0, 10), (0, 10)] for two parameters ranging from 0 to 10. + """ + domain = np.array(domain) + codomain = np.array(codomain) + + M = (codomain[:, 1] - codomain[:, 0])/ (domain[:, 1] - domain[:, 0]) + + def f(x): + assert x.shape[1] == domain.shape[0], f"x must have the same number of features as the domain. Expected {domain.shape[0]}, got {x.shape[1]}." + return codomain[:, 0] + M * (x - domain[:, 0]) + + return f + +class Optimizer(ABC): + """ + Abstract class for optimization algorithms + """ + @abstractmethod + def __init__(self, growth_rate: float, population_size, ranges, iterations=None, rng_seed=0): + pass + + @abstractmethod + def view(self, x, linmap): + pass + + @abstractmethod + def view_g(self): + pass + + @abstractmethod + def inverse_view(self, x, linmap=None): + pass + + @abstractmethod + def ask_oracle(self) -> np.ndarray: + raise NotImplementedError("The oracle getter must be implemented.") + + @abstractmethod + def set_oracle(self, X: np.ndarray): + raise NotImplementedError("The oracle setter must be implemented.") + + @abstractmethod + def init_oracle(self): + warnings.warn("The oracle initialization is not implemented.") + pass + + @abstractmethod + def step(self): + pass \ No newline at end of file diff --git a/pyduino/optimization/gradient_descent.py b/pyduino/optimization/gradient_descent.py new file mode 100644 index 0000000..8bbe0e1 --- /dev/null +++ b/pyduino/optimization/gradient_descent.py @@ -0,0 +1,71 @@ +import numpy as np +from . import Optimizer, linmap + +class GradientDescent(Optimizer): + def __init__(self, population_size: int, ranges: list[float], damping: float = 0.01, rng_seed: int = 0): + """ + This gradient descent algorithm assumes that the optimization function 'f' is to be minimized, differentiable, and time independent. + + $$\frac{\mathrm{d} f}{\mathrm{d} t} = \frac{\partial f}{\partial \vec{x}}\frac{\mathrm{d} \vec{x}}{\mathrm{d} t}$$ + + Where $\frac{\partial f}{\partial t}$ is assumed to be zero. + + ranges list of pairs: + Maxima and minima that each parameter can cover. + Example: [(0, 10), (0, 10)] for two parameters ranging from 0 to 10. + population_size int: + Number of individuals in the population. + damping float: + Damping factor to avoid oscillations. Default is 0.01. + rng_seed int: + Seed for the random number generator. Default is 0. + """ + self.population_size = population_size + self.ranges = np.array(ranges) + self.damping = damping + self.rng_seed = np.random.default_rng(rng_seed) + + # Derived attributes + self.invlinmap = linmap(self.ranges, np.array([[0, 1]] * len(self.ranges))) + self.linmap = linmap(np.array([[0, 1]] * len(self.ranges)), self.ranges) + + # Initialize the population (random position and initial momentum) + self.population = self.rng_seed.random((self.population_size, len(self.ranges))) + self.momenta = self.rng_seed.random((self.population_size, len(self.ranges))) + self.oracle_past = self.rng_seed.random((self.population_size, 1)) + + def view(self, x): + """ + Maps the input from the domain to the codomain. + """ + return self.linmap(x) + + def view_g(self): + """ + Maps the input from the domain to the codomain. + """ + return self.linmap(self.population) + + def inverse_view(self, x): + """ + Maps the input from the codomain to the domain. + """ + return self.invlinmap(x) + + def ask_oracle(self) -> np.ndarray: + return super().ask_oracle() + + def set_oracle(self, X: np.ndarray): + return super().set_oracle(X) + + def init_oracle(self): + return self.set_oracle(self.view(self.population)) + + def step(self, dt: float): + """ + Moves the population in the direction of the gradient. + + dt float: + Time taken from the last observation. + """ + pass diff --git a/pyduino/optimization/nelder_mead.py b/pyduino/optimization/nelder_mead.py new file mode 100644 index 0000000..f99e00c --- /dev/null +++ b/pyduino/optimization/nelder_mead.py @@ -0,0 +1,112 @@ +import numpy as np +from typing import Callable +from . import Optimizer, linmap + +class NelderMead(Optimizer): + def __init__(self, population_size: int, ranges: list[float], rng_seed: int = 0): + """ + This Nelder-Mead algorithm assumes that the optimization function 'f' is to be minimized and time independent. + + ranges list of pairs: + Maxima and minima that each parameter can cover. + Example: [(0, 10), (0, 10)] for two parameters ranging from 0 to 10. + population_size int: + Number of individuals in the population. + rng_seed int: + Seed for the random number generator. Default is 0. + """ + self.population_size = population_size + self.ranges = np.array(ranges) + self.rng_seed = np.random.default_rng(rng_seed) + + # Derived attributes + self.invlinmap = linmap(self.ranges, np.array([[0, 1]] * len(self.ranges))) + self.linmap = linmap(np.array([[0, 1]] * len(self.ranges)), self.ranges) + + # Initialize the population (random position and initial momentum) + self.population = self.rng_seed.random((self.population_size, len(self.ranges))) + + def view(self, x): + """ + Maps the input from the domain to the codomain. + """ + return self.linmap(x) + + def view_g(self): + """ + Maps the input from the domain to the codomain. + """ + return self.linmap(self.population) + + def inverse_view(self, x): + """ + Maps the input from the codomain to the domain. + """ + return self.invlinmap(x) + + def ask_oracle(self) -> callable: + return super().ask_oracle() + + def set_oracle(self, X: np.ndarray): + return super().set_oracle(X) + + def init_oracle(self): + return self.set_oracle(self.view(self.population)) + + def step(self): + """ + This function performs a single step of the Nelder-Mead algorithm. + """ + + # Sort the population by the value of the oracle + self.set_oracle(self.view(self.population)) + y = self.ask_oracle() + idx = np.argsort(y) + self.population = self.population[idx] + + # Compute the centroid of the population + centroid = self.population[:-1].mean(axis=0) + + # Reflect the worst point through the centroid + reflected = centroid + (centroid - self.population[-1]) + + # Evaluate the reflected point + + self.set_oracle(self.view(reflected.reshape(1,-1))) + y_reflected = self.ask_oracle() + + # If the reflected point is better than the second worst, but not better than the best, then expand + + if y_reflected < y[-2] and y_reflected > y[0]: + expanded = centroid + (reflected - centroid) + self.set_oracle(self.view(expanded.reshape(1,-1))) + y_expanded = self.ask_oracle() + if y_expanded < y_reflected: + self.population[-1] = expanded + else: + self.population[-1] = reflected + # If the reflected point is better than the best, then expand + elif y_reflected < y[0]: + expanded = centroid + 2 * (reflected - centroid) + self.set_oracle(self.view(expanded.reshape(1,-1))) + y_expanded = self.ask_oracle() + if y_expanded < y_reflected: + self.population[-1] = expanded + else: + self.population[-1] = reflected + # If the reflected point is worse than the second worst, then contract + elif y_reflected > y[-2]: + contracted = centroid + 0.5 * (self.population[-1] - centroid) + self.set_oracle(self.view(contracted.reshape(1,-1))) + y_contracted = self.ask_oracle() + if y_contracted < y[-1]: + self.population[-1] = contracted + else: + for i in range(1, len(self.population)): + self.population[i] = 0.5 * (self.population[i] + self.population[0]) + # If the reflected point is worse than the worst, then shrink + elif y_reflected > y[-1]: + for i in range(1, len(self.population)): + self.population[i] = 0.5 * (self.population[i] + self.population[0]) + + return self.view(self.population) \ No newline at end of file diff --git a/tests/test_linmap.py b/tests/test_linmap.py new file mode 100644 index 0000000..204dc74 --- /dev/null +++ b/tests/test_linmap.py @@ -0,0 +1,13 @@ +# Tests the linear map. +import sys +import os +import numpy as np + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from pyduino.optimization import linmap + +def test_linmap(): + domain = [(0,1), (0,1), (0,1)] + codomain = [(-1,1), (-10,10), (0,8)] + f = linmap(domain, codomain) + assert f(np.random.random((10,3))).shape == (10,3) \ No newline at end of file diff --git a/tests/test_optim.py b/tests/test_optim.py new file mode 100644 index 0000000..5d78d1e --- /dev/null +++ b/tests/test_optim.py @@ -0,0 +1,28 @@ +# Tests the gradient descent against a paraboloid of 4 dimensions. +import sys +import os +import numpy as np + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from pyduino.optimization.nelder_mead import NelderMead + +def test_gradient_descent(): + optimizer = NelderMead(population_size=10, ranges=[(-10, 10)] * 4) + class Oracle: + def set(self, X): + self.X = X + def ask(self): + return (self.X**2).sum(axis=1) + oracle = Oracle() + optimizer.ask_oracle = oracle.ask + optimizer.set_oracle = lambda X: oracle.set(X) + + # Set initial population to oracle + optimizer.init_oracle() + + for i in range(100): + optimizer.step() + assert optimizer.view(optimizer.population).shape == (10, 4) + assert optimizer.view_g().shape == (10, 4) + assert optimizer.inverse_view(optimizer.view(optimizer.population)).shape == (10, 4) + assert np.isclose(optimizer.ask_oracle().min(), 0, atol=1e-4), f"Oracle: {optimizer.ask_oracle().min()}" \ No newline at end of file From 7759e9fe65095c0b03822e71d2e36c47f0941908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 10 Mar 2024 00:01:16 -0300 Subject: [PATCH 02/54] Added test against rosenbrock for nelder mead --- tests/test_optim.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/test_optim.py b/tests/test_optim.py index 5d78d1e..0410db4 100644 --- a/tests/test_optim.py +++ b/tests/test_optim.py @@ -1,4 +1,3 @@ -# Tests the gradient descent against a paraboloid of 4 dimensions. import sys import os import numpy as np @@ -6,7 +5,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from pyduino.optimization.nelder_mead import NelderMead -def test_gradient_descent(): +def test_nelder_mead_parabola(): optimizer = NelderMead(population_size=10, ranges=[(-10, 10)] * 4) class Oracle: def set(self, X): @@ -25,4 +24,25 @@ def ask(self): assert optimizer.view(optimizer.population).shape == (10, 4) assert optimizer.view_g().shape == (10, 4) assert optimizer.inverse_view(optimizer.view(optimizer.population)).shape == (10, 4) + assert np.isclose(optimizer.ask_oracle().min(), 0, atol=1e-4), f"Oracle: {optimizer.ask_oracle().min()}" + +def test_nelder_mead_rosenbrock(): + optimizer = NelderMead(population_size=10, ranges=[(-10, 10)] * 4) + class Oracle: + def set(self, X): + self.X = X + def ask(self): + return ((1 - self.X[:, :-1])**2).sum(axis=1) + 100 * ((self.X[:, 1:] - self.X[:, :-1]**2)**2).sum(axis=1) + oracle = Oracle() + optimizer.ask_oracle = oracle.ask + optimizer.set_oracle = lambda X: oracle.set(X) + + # Set initial population to oracle + optimizer.init_oracle() + + for i in range(1000): + optimizer.step() + assert optimizer.view(optimizer.population).shape == (10, 4) + assert optimizer.view_g().shape == (10, 4) + assert optimizer.inverse_view(optimizer.view(optimizer.population)).shape == (10, 4) assert np.isclose(optimizer.ask_oracle().min(), 0, atol=1e-4), f"Oracle: {optimizer.ask_oracle().min()}" \ No newline at end of file From 6b758a725963ed5d32dffdb94a85e2f725dfc76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 16 Mar 2024 22:24:44 -0300 Subject: [PATCH 03/54] Started implementing nelder mead on spectra --- pyduino/optimization/__init__.py | 6 +- pyduino/optimization/nelder_mead.py | 36 +++---- pyduino/spectra.py | 146 +++++++++++++++------------- tests/test_optim.py | 18 ++-- 4 files changed, 100 insertions(+), 106 deletions(-) diff --git a/pyduino/optimization/__init__.py b/pyduino/optimization/__init__.py index ecc5f6f..ea0a857 100644 --- a/pyduino/optimization/__init__.py +++ b/pyduino/optimization/__init__.py @@ -46,11 +46,7 @@ def inverse_view(self, x, linmap=None): @abstractmethod def ask_oracle(self) -> np.ndarray: - raise NotImplementedError("The oracle getter must be implemented.") - - @abstractmethod - def set_oracle(self, X: np.ndarray): - raise NotImplementedError("The oracle setter must be implemented.") + raise NotImplementedError("The oracle must be implemented.") @abstractmethod def init_oracle(self): diff --git a/pyduino/optimization/nelder_mead.py b/pyduino/optimization/nelder_mead.py index f99e00c..6dde03b 100644 --- a/pyduino/optimization/nelder_mead.py +++ b/pyduino/optimization/nelder_mead.py @@ -44,14 +44,11 @@ def inverse_view(self, x): """ return self.invlinmap(x) - def ask_oracle(self) -> callable: + def ask_oracle(self, X: np.ndarray) -> np.ndarray: return super().ask_oracle() - - def set_oracle(self, X: np.ndarray): - return super().set_oracle(X) def init_oracle(self): - return self.set_oracle(self.view(self.population)) + return self.ask_oracle(self.view_g()) def step(self): """ @@ -59,9 +56,8 @@ def step(self): """ # Sort the population by the value of the oracle - self.set_oracle(self.view(self.population)) - y = self.ask_oracle() - idx = np.argsort(y) + self.y = self.ask_oracle(self.view_g()) + idx = np.argsort(self.y) self.population = self.population[idx] # Compute the centroid of the population @@ -72,41 +68,37 @@ def step(self): # Evaluate the reflected point - self.set_oracle(self.view(reflected.reshape(1,-1))) - y_reflected = self.ask_oracle() + y_reflected = self.ask_oracle(self.view(reflected.reshape(1,-1))) # If the reflected point is better than the second worst, but not better than the best, then expand - if y_reflected < y[-2] and y_reflected > y[0]: + if y_reflected < self.y[-2] and y_reflected > self.y[0]: expanded = centroid + (reflected - centroid) - self.set_oracle(self.view(expanded.reshape(1,-1))) - y_expanded = self.ask_oracle() + y_expanded = self.ask_oracle(self.view(expanded.reshape(1,-1))) if y_expanded < y_reflected: self.population[-1] = expanded else: self.population[-1] = reflected # If the reflected point is better than the best, then expand - elif y_reflected < y[0]: + elif y_reflected < self.y[0]: expanded = centroid + 2 * (reflected - centroid) - self.set_oracle(self.view(expanded.reshape(1,-1))) - y_expanded = self.ask_oracle() + y_expanded = self.ask_oracle(self.view(expanded.reshape(1,-1))) if y_expanded < y_reflected: self.population[-1] = expanded else: self.population[-1] = reflected # If the reflected point is worse than the second worst, then contract - elif y_reflected > y[-2]: + elif y_reflected > self.y[-2]: contracted = centroid + 0.5 * (self.population[-1] - centroid) - self.set_oracle(self.view(contracted.reshape(1,-1))) - y_contracted = self.ask_oracle() - if y_contracted < y[-1]: + y_contracted = self.ask_oracle(self.view(contracted.reshape(1,-1))) + if y_contracted < self.y[-1]: self.population[-1] = contracted else: for i in range(1, len(self.population)): self.population[i] = 0.5 * (self.population[i] + self.population[0]) # If the reflected point is worse than the worst, then shrink - elif y_reflected > y[-1]: + elif y_reflected > self.y[-1]: for i in range(1, len(self.population)): self.population[i] = 0.5 * (self.population[i] + self.population[0]) - return self.view(self.population) \ No newline at end of file + return self.view_g() \ No newline at end of file diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 010aa80..0678986 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -1,4 +1,4 @@ -from gapy.gapy2 import GA +from pyduino.optimization.nelder_mead import NelderMead from pyduino.pyduino2 import ReactorManager, chunks, PATHS import numpy as np from functools import partial @@ -78,7 +78,7 @@ def parse_dados(X,param): """ return np.array(list(map(seval,map(lambda x: x[1].get(param,0),sorted(X.items(),key=lambda x: x[0]))))) -class Spectra(RangeParser,ReactorManager,GA): +class Spectra(RangeParser,ReactorManager,NelderMead): def __init__(self,elitism,f_param,ranges,density_param,maximize=True,log_name=None,reset_density=False,**kwargs): """ Args: @@ -100,23 +100,20 @@ def __init__(self,elitism,f_param,ranges,density_param,maximize=True,log_name=No RangeParser.__init__(self,ranges,self.parameters) - #assert os.path.exists(IRRADIANCE_PATH) self.irradiance = PATHS.SYSTEM_PARAMETERS['irradiance']#yaml_get(IRRADIANCE_PATH) - #self.irradiance = np.array([self.irradiance[u] for u in self.keyed_ranges.keys()]) self.irradiance = pd.Series(self.irradiance) ReactorManager.__init__(self) - GA.__init__( + NelderMead.__init__( self, population_size=len(self.reactors), ranges=self.ranges_as_list(), - generations=0, - **kwargs - ) + rng_seed=kwargs.get('rng_seed',0) + ) self.ids = list(self.reactors.keys()) self.sorted_ids = sorted(self.ids) self.log_init(name=log_name) - self.payload = self.G_as_keyed() if self.payload is None else self.payload + self.payload = self.population_as_dict if self.payload is None else self.payload self.data = None self.do_gotod = reset_density self.fparam = f_param @@ -125,38 +122,33 @@ def __init__(self,elitism,f_param,ranges,density_param,maximize=True,log_name=No self.maximize = maximize self.dt = np.nan self.elitism = elitism - def G_as_keyed(self): + def dictfy(self, x): """ - Converts genome matrix into an appropriate format to send to the reactors. + Converts the given input into an ordered dictionary. + + 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. + """ + ids = self.ids[:len(x)] return OrderedDict( zip( - self.ids, + ids, map( lambda u: self.ranges_as_keyed(u), - list(np.round(self.view(self.G,self.linmap),2)) + list(np.round(self.view(x),2)) ) ) ) - def f_map(self,x_1,x_0): + @property.getter + def population_as_dict(self): """ - Computation for the fitness function. + Converts genome matrix into an appropriate format to send to the reactors. """ - f_1 = x_1.loc[self.density_param].astype(float) - self.power = ((pd.DataFrame(self.G_as_keyed()).T*self.irradiance).sum(axis=1))/100 - if (self.dt is not np.nan) and (self.iteration_counter>0): - f_0 = x_0.loc[self.density_param].astype(float) - #self.growth_rate = (f_1-f_0)/self.dt - self.growth_rate = (f_1/f_0-1)/self.dt - #self.efficiency = self.growth_rate/(self.power+1) - #self.efficiency = 1000000*self.growth_rate*np.exp(-self.power/5) - self.efficiency = 1000000*self.growth_rate*np.exp(-(self.power-7)*(self.power-7)/(2*1.5*1.5)) - else: - self.growth_rate = self.power*np.nan - self.efficiency = self.power*np.nan - x_1.loc['power',:] = self.power.copy() - x_1.loc['efficiency',:] = self.efficiency.copy() - x_1.loc['growth_rate',:] = self.growth_rate.copy() + return self.dictfy(self.population) def payload_to_matrix(self): return np.nan_to_num( np.array( @@ -188,6 +180,7 @@ def F_set(self,x): x (:obj:`dict` of :obj:`dict`): Dictionary having reactor id as keys and a dictionary of parameters and their values as values. """ + for _id,params in x.items(): for chk in chunks(list(params.items()),3): self.reactors[_id].set(dict(chk)) @@ -201,11 +194,11 @@ def set_spectrum(self,preset): def set_preset_state_spectra(self,*args,**kwargs): self.set_preset_state(*args,**kwargs) - self.G = self.inverse_view(self.payload_to_matrix()).astype(int) + self.population = self.inverse_view(self.payload_to_matrix()).astype(int) def update_fitness(self,X): #Get and return parameter chosen for fitness - self.fitness = ((-1)**(1+self.maximize))*X.loc[self.fparam].astype(float).to_numpy() + self.fitness = ((-1)**(self.maximize))*X.loc[self.fparam].astype(float).to_numpy() return self.fitness def GET(self,tag): @@ -215,47 +208,82 @@ def GET(self,tag): print("[INFO]","GET",datetime.now().strftime("%c")) self.past_data = self.data.copy() if self.data is not None else pd.DataFrame(self.payload) self.data = pd.DataFrame(self.F_get()) - self.f_map(self.data,self.past_data) self.log.log_many_rows(self.data,tags={'growth_state':tag}) self.log.log_optimal(column=self.fparam,maximum=self.maximize,tags={'growth_state':tag}) self.log.log_average(tags={'growth_state':tag}) - def gotod(self,deltaTgotod): + def gotod(self): self.t_gotod_1 = datetime.now() self.send("gotod",await_response=False) print("[INFO] gotod sent") - time.sleep(deltaTgotod) + time.sleep(self.deltaTgotod) self.dt = (datetime.now()-self.t_gotod_1).total_seconds() print("[INFO] gotod DT", self.dt) self.GET("gotod") + # === Optimizer methods === + + def ask_oracle(self, X) -> np.ndarray: + """ + 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. + """ + y = [] + + assert X.shape[1] == len(self.parameters) + assert len(X.shape) == 2, "X must be a 2D array." + partitions = np.array_split(X, len(self.reactors)) + + for partition in partitions: + payload = self.dictfy(partition) + reactors = payload.keys() + + self.gotod() + data0 = pd.DataFrame(self.F_get()) + f0 = data0.loc[reactors,self.density_param] + + self.F_set(payload) + time.sleep(self.deltaT) + data = pd.DataFrame(self.F_get()) + f = data.loc[reactors,self.density_param] + + alpha = np.log(f/f0)/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ + + y += list(alpha) + return np.array(y) + # === * === + def run( self, - deltaT: int, - run_ga: bool = True, + deltaT: float, + run_optim: bool = True, deltaTgotod: int = None ): """ Runs reading and wiriting operations in an infinite loop on intervals given by `deltaT`. Args: - deltaT (int): Amount of time in seconds to wait in each iteration. - run_ga (bool): Whether or not execute a step in the genetic algorithm. + deltaT (float): Amount of time in seconds to wait in each iteration. + run_optim (bool): Whether or not to use the optimizer. deltaTgotod (int, optional): Time to wait after sending `gotod` command. """ #Checking if gotod time is at least five minutes - if run_ga and deltaTgotod is None: raise ValueError("deltaTgotod must be at least 5 minutes.") - if run_ga and deltaTgotod <= 5*60: raise ValueError("deltaTgotod must be at least 5 minutes.") + if run_optim and (deltaTgotod is None or deltaTgotod <= 300): raise ValueError("deltaTgotod must be at least 5 minutes.") + self.deltaT = deltaT + self.deltaTgotod = deltaTgotod self.iteration_counter = 1 - self.GET("growing") with open("error_traceback.log","w") as log_file: log_file.write(datetime_to_str(self.log.timestamp)+'\n') try: - self.deltaT = deltaT print("START") while True: #growing @@ -265,34 +293,18 @@ def run( print("[INFO]","DT",self.dt) self.GET("growing") self.update_fitness(self.data) - #GA - if run_ga: - #self.p = softmax(self.fitness/100) - #self.p = ReLUP(self.fitness*self.fitness*self.fitness) - self.p = ReLUP(self.fitness*self.fitness) - #Hotfix for elitism - print(f"{bcolors.OKCYAN}self.data{bcolors.ENDC}") - self.data.loc['p',:] = self.p.copy() - print(f"{bcolors.BOLD}{self.data.T.loc[:,self.titled_parameters+['power','efficiency','growth_rate','p']]}{bcolors.ENDC}") - if self.elitism: - self.elite_ix = self.ids[self.p.argmax()] - self.anti_elite_ix = self.ids[self.p.argmin()] - self.elite = self.G[self.p.argmax()].copy() - self.crossover() - self.mutation() - if self.elitism: - self.G[self.p.argmin()] = self.elite.copy() - self.payload = self.G_as_keyed() + print(f"{bcolors.OKCYAN}self.data{bcolors.ENDC}") + print(f"{bcolors.BOLD}{self.data.T.loc[:,self.titled_parameters+['power','efficiency','growth_rate','p']]}{bcolors.ENDC}") + #Optimizer + if run_optim: + self.step() else: df = self.data.T df.columns = df.columns.str.lower() self.payload = df[self.parameters].T.to_dict() - self.G = self.inverse_view(self.payload_to_matrix()).astype(int) + self.population = self.inverse_view(self.payload_to_matrix()).astype(int) + self.gotod() print("[INFO]","SET",datetime.now().strftime("%c")) - self.F_set(self.payload) if run_ga else None - #gotod - if self.do_gotod: - self.gotod(deltaTgotod) self.iteration_counter += 1 except Exception as e: traceback.print_exc(file=log_file) @@ -358,7 +370,7 @@ def run_incremental( df.columns = df.columns.str.lower() self.payload = df[self.parameters].T.to_dict() - self.G = self.inverse_view(self.payload_to_matrix()).astype(int) + self.population = self.inverse_view(self.payload_to_matrix()).astype(int) print("[INFO]","SET",self.t1.strftime("%c")) self.F_set(self.payload) time.sleep(max(2,deltaT)) diff --git a/tests/test_optim.py b/tests/test_optim.py index 0410db4..106c336 100644 --- a/tests/test_optim.py +++ b/tests/test_optim.py @@ -8,13 +8,10 @@ def test_nelder_mead_parabola(): optimizer = NelderMead(population_size=10, ranges=[(-10, 10)] * 4) class Oracle: - def set(self, X): - self.X = X - def ask(self): - return (self.X**2).sum(axis=1) + def ask(self, X): + return (X**2).sum(axis=1) oracle = Oracle() optimizer.ask_oracle = oracle.ask - optimizer.set_oracle = lambda X: oracle.set(X) # Set initial population to oracle optimizer.init_oracle() @@ -24,18 +21,15 @@ def ask(self): assert optimizer.view(optimizer.population).shape == (10, 4) assert optimizer.view_g().shape == (10, 4) assert optimizer.inverse_view(optimizer.view(optimizer.population)).shape == (10, 4) - assert np.isclose(optimizer.ask_oracle().min(), 0, atol=1e-4), f"Oracle: {optimizer.ask_oracle().min()}" + assert np.isclose(optimizer.y.min(), 0, atol=1e-4), f"Oracle: {optimizer.y.min()}" def test_nelder_mead_rosenbrock(): optimizer = NelderMead(population_size=10, ranges=[(-10, 10)] * 4) class Oracle: - def set(self, X): - self.X = X - def ask(self): - return ((1 - self.X[:, :-1])**2).sum(axis=1) + 100 * ((self.X[:, 1:] - self.X[:, :-1]**2)**2).sum(axis=1) + def ask(self, X): + return ((1 - X[:, :-1])**2).sum(axis=1) + 100 * ((X[:, 1:] - X[:, :-1]**2)**2).sum(axis=1) oracle = Oracle() optimizer.ask_oracle = oracle.ask - optimizer.set_oracle = lambda X: oracle.set(X) # Set initial population to oracle optimizer.init_oracle() @@ -45,4 +39,4 @@ def ask(self): assert optimizer.view(optimizer.population).shape == (10, 4) assert optimizer.view_g().shape == (10, 4) assert optimizer.inverse_view(optimizer.view(optimizer.population)).shape == (10, 4) - assert np.isclose(optimizer.ask_oracle().min(), 0, atol=1e-4), f"Oracle: {optimizer.ask_oracle().min()}" \ No newline at end of file + assert np.isclose(optimizer.y.min(), 0, atol=1e-4), f"Oracle: {optimizer.y.min()}" \ No newline at end of file From 0c7c82125cd2388e5a24a8c11cb52af1479ef275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 16 Mar 2024 22:28:25 -0300 Subject: [PATCH 04/54] dot --- pyduino/spectra.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 0678986..64be66c 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -122,9 +122,9 @@ def __init__(self,elitism,f_param,ranges,density_param,maximize=True,log_name=No self.maximize = maximize self.dt = np.nan self.elitism = elitism - def dictfy(self, x): + def assign_to_reactors(self, x): """ - Converts the given input into an ordered dictionary. + Assigns a list of parameters to the reactors. Parameters: x (list): The input list to be converted. @@ -148,7 +148,7 @@ def population_as_dict(self): """ Converts genome matrix into an appropriate format to send to the reactors. """ - return self.dictfy(self.population) + return self.assign_to_reactors(self.population) def payload_to_matrix(self): return np.nan_to_num( np.array( @@ -240,7 +240,7 @@ def ask_oracle(self, X) -> np.ndarray: partitions = np.array_split(X, len(self.reactors)) for partition in partitions: - payload = self.dictfy(partition) + payload = self.assign_to_reactors(partition) reactors = payload.keys() self.gotod() From 876473fc5e0308d0934a5475395c27aec816b996 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 17 Mar 2024 20:08:32 -0300 Subject: [PATCH 05/54] Update gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 704ae3d..a7e163a 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,8 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ + +cache.csv +log/ +calibrate/ \ No newline at end of file From eda684ccd24e5850824f3e28876bd43d69d2d535 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 17 Mar 2024 20:20:47 -0300 Subject: [PATCH 06/54] Debugging --- pyduino/optimization/__init__.py | 4 ++-- pyduino/optimization/nelder_mead.py | 4 ++-- pyduino/spectra.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyduino/optimization/__init__.py b/pyduino/optimization/__init__.py index ea0a857..06d21d5 100644 --- a/pyduino/optimization/__init__.py +++ b/pyduino/optimization/__init__.py @@ -1,9 +1,9 @@ -from typing import Callable +from typing import Callable, List, Tuple from abc import ABC, abstractmethod import warnings import numpy as np -def linmap(domain: list[tuple[float, float]], codomain: list[tuple[float, float]]) -> Callable[[np.ndarray], np.ndarray]: +def linmap(domain: List[Tuple[float, float]], codomain: List[Tuple[float, float]]) -> Callable[[np.ndarray], np.ndarray]: """ Linear mapping from domain to codomain. domain list of pairs: diff --git a/pyduino/optimization/nelder_mead.py b/pyduino/optimization/nelder_mead.py index 6dde03b..2b1ae3c 100644 --- a/pyduino/optimization/nelder_mead.py +++ b/pyduino/optimization/nelder_mead.py @@ -1,9 +1,9 @@ import numpy as np -from typing import Callable +from typing import Callable, List from . import Optimizer, linmap class NelderMead(Optimizer): - def __init__(self, population_size: int, ranges: list[float], rng_seed: int = 0): + def __init__(self, population_size: int, ranges: List[float], rng_seed: int = 0): """ This Nelder-Mead algorithm assumes that the optimization function 'f' is to be minimized and time independent. diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 64be66c..0fceafc 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -143,7 +143,7 @@ def assign_to_reactors(self, x): ) ) ) - @property.getter + @property def population_as_dict(self): """ Converts genome matrix into an appropriate format to send to the reactors. From 8bbe42c4cbbaa1f27f8efef6ba48d5f7e3e18616 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 17 Mar 2024 21:50:50 -0300 Subject: [PATCH 07/54] Use End of Text for Arduino instead of End of Transmission --- pyduino/slave.py | 6 +++--- pyduino/spectra.py | 3 --- tests/test_spectra.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 tests/test_spectra.py diff --git a/pyduino/slave.py b/pyduino/slave.py index d578364..27a37dc 100644 --- a/pyduino/slave.py +++ b/pyduino/slave.py @@ -170,16 +170,16 @@ def _send(self, msg: str): def _recv(self) -> str: """ - Reads from the serial port until it finds an EOT ASCII token. + Reads from the serial port until it finds an End-of-Text ASCII token. Returns: str: The response received from the reactor. """ - response = self.serial.read_until(b'\x04') \ + response = self.serial.read_until(b'\x03') \ .decode('ascii') \ .strip("\n") \ .strip("\r") \ - .strip("\x04") + .strip("\x03") return response diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 0fceafc..2430aaa 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -1,12 +1,10 @@ from pyduino.optimization.nelder_mead import NelderMead from pyduino.pyduino2 import ReactorManager, chunks, PATHS import numpy as np -from functools import partial import json import os import pandas as pd import numpy as np -from typing import Union import time from datetime import date, datetime from pyduino.data_parser import yaml_genetic_algorithm, RangeParser, get_datetimes @@ -166,7 +164,6 @@ def pretty_print_dict(self,D): df.index = df.index.str.lower() df = df.loc[self.parameters,:] df.loc['fitness'] = self.fitness - df.loc['probs'] = 100*self.p return df.round(decimals=2) def F_get(self): """ diff --git a/tests/test_spectra.py b/tests/test_spectra.py new file mode 100644 index 0000000..d59b565 --- /dev/null +++ b/tests/test_spectra.py @@ -0,0 +1,31 @@ +import sys +import os +import numpy as np + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from pyduino.spectra import Spectra, PATHS + +class TestSpectra: + g = Spectra(**PATHS.HYPERPARAMETERS) + g.deltaTgotod = 1 + g.deltaT = 1 + def test_functions(self): + assert len(self.g.reactors) != 0 + assert len(self.g.ids) == self.g.population_size + + def test_array_assignment(self): + X = np.random.random((len(self.g.ids),len(self.g.parameters))) + assigned = self.g.assign_to_reactors(X) + keys = list(assigned.keys()) + assert len(keys)==len(self.g.ids) + assert len(assigned[keys[0]]) == len(self.g.parameters) + def test_update_fitness(self): + self.g.update_fitness() + def test_oracle(self): + data = self.g.F_get() + for k in data.keys(): + data[k][self.g.fparam] = '0' + df = self.g.pretty_print_dict(data) + y = self.g.ask_oracle(self.g.population) + def test_logger(self): + self.g.GET("test") \ No newline at end of file From bf69700c2087e15f2337db5db763412640aba14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 17 Mar 2024 23:02:47 -0300 Subject: [PATCH 08/54] Increased timeout on slave --- pyduino/slave.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyduino/slave.py b/pyduino/slave.py index 27a37dc..656535b 100644 --- a/pyduino/slave.py +++ b/pyduino/slave.py @@ -12,7 +12,7 @@ import socket from typing import Any, Optional, Dict -STEP = 1/8.0 +TIMEOUT = 60 HEADER_DELAY = 5 class ReactorServer(Flask): @@ -111,10 +111,10 @@ def serial_connect(self): self.available_ports = list_ports.comports() self.available_ports = list(filter(lambda x: (x.vid,x.pid) in {(1027,24577),(9025,16),(6790,29987)},self.available_ports)) self.port = self.available_ports[0].device - self.serial = Serial(self.port, baudrate=self.baudrate, timeout=STEP) + self.serial = Serial(self.port, baudrate=self.baudrate, timeout=TIMEOUT) logging.info(f"Connected to serial port {self.serial}.") - def connect(self, delay: float = STEP): + def connect(self): """ Begins the connection to the reactor. @@ -175,11 +175,11 @@ def _recv(self) -> str: Returns: str: The response received from the reactor. """ - response = self.serial.read_until(b'\x03') \ + response = self.serial.read_until(b'\x04') \ .decode('ascii') \ .strip("\n") \ .strip("\r") \ - .strip("\x03") + .strip("\x04") return response From d635ac0895ecc51afab5cbc53ba8f1a823234629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 17 Mar 2024 23:29:25 -0300 Subject: [PATCH 09/54] Adding reset input buffers --- pyduino/pyduino2.py | 9 ++++----- pyduino/slave.py | 8 +++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pyduino/pyduino2.py b/pyduino/pyduino2.py index 3163321..be5c1a8 100644 --- a/pyduino/pyduino2.py +++ b/pyduino/pyduino2.py @@ -71,11 +71,10 @@ def __init__(self, url): def http_get(self,route): return requests.get(urljoin(self.url,route)) - def http_post(self,route,command,await_response,delay): + def http_post(self,route,command,await_response): return requests.post(urljoin(self.url,route),json={ "command": command, - "await_response": await_response, - "delay": delay + "await_response": await_response }) def connect(self): @@ -113,11 +112,11 @@ def close(self): """ self.http_post("send","fim",False,0) - def send(self, msg, delay=5): + def send(self, msg): """ Sends command and awaits for a response """ - resp = self.http_post("send",msg,True,delay) + resp = self.http_post("send",msg,True) return resp.json()["response"] def _send(self, msg): diff --git a/pyduino/slave.py b/pyduino/slave.py index 656535b..e77a39a 100644 --- a/pyduino/slave.py +++ b/pyduino/slave.py @@ -82,7 +82,7 @@ def http_send(): content = request.json logging.info(f"Received request: {content['command']}") if content['await_response']: - response = self.send(content["command"], delay=content["delay"]) + response = self.send(content["command"]) else: response = self._send(content["command"]) return jsonify({"response": response}), 200 @@ -123,6 +123,7 @@ def connect(self): """ sleep(HEADER_DELAY) self.serial.flush() + self.serial.reset_input_buffer() self._send("quiet_connect") self.connected = True @@ -140,22 +141,23 @@ def close(self): Interrupts the connection with the reactor. """ if self.serial.is_open: + self.serial.reset_input_buffer() self.send("fim") self.serial.close() - def send(self, msg: str, delay: float = 0) -> str: + def send(self, msg: str) -> str: """ Sends a command to the reactor and receives the response. Args: msg (str): The command to send to the reactor. - delay (float, optional): Delay in seconds before sending the command. Returns: str: The response received from the reactor. """ if not self.connected: self.connect() + self.serial.reset_input_buffer() self._send(msg) return self._recv() From bdf8334de1c164bb6a72562ed06552c81bb04d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 17 Mar 2024 23:44:17 -0300 Subject: [PATCH 10/54] update test --- tests/test_spectra.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_spectra.py b/tests/test_spectra.py index d59b565..8acdc8f 100644 --- a/tests/test_spectra.py +++ b/tests/test_spectra.py @@ -19,13 +19,12 @@ def test_array_assignment(self): keys = list(assigned.keys()) assert len(keys)==len(self.g.ids) assert len(assigned[keys[0]]) == len(self.g.parameters) - def test_update_fitness(self): - self.g.update_fitness() def test_oracle(self): data = self.g.F_get() for k in data.keys(): data[k][self.g.fparam] = '0' df = self.g.pretty_print_dict(data) y = self.g.ask_oracle(self.g.population) + self.g.update_fitness(y) def test_logger(self): self.g.GET("test") \ No newline at end of file From f92ce9dd8616198732e6065e6a0abc7bc7b0edbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 17 Mar 2024 23:46:20 -0300 Subject: [PATCH 11/54] Removed delay from send_wrapper --- pyduino/pyduino2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyduino/pyduino2.py b/pyduino/pyduino2.py index be5c1a8..3d16515 100644 --- a/pyduino/pyduino2.py +++ b/pyduino/pyduino2.py @@ -160,10 +160,10 @@ def horacerta(self): self._send("horacerta") -def send_wrapper(reactor,command,delay,await_response): +def send_wrapper(reactor,command,await_response): id, reactor = reactor if await_response: - return (id,reactor.send(command,delay)) + return (id,reactor.send(command)) else: return (id,reactor._send(command)) @@ -214,7 +214,7 @@ def send(self,command,await_response=True,**kwargs): def send_parallel(self,command,delay,await_response=True): out = [] with Pool(7) as p: - out = p.map(partial(send_wrapper,command=command,delay=delay,await_response=await_response),list(self.reactors.items())) + out = p.map(partial(send_wrapper,command=command,await_response=await_response),list(self.reactors.items())) return out def set(self, data=None, **kwargs): From 34e9d48ab53312fccec54b422374bbc4a17b4a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 17 Mar 2024 23:48:57 -0300 Subject: [PATCH 12/54] Debug --- pyduino/pyduino2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyduino/pyduino2.py b/pyduino/pyduino2.py index 3d16515..2321683 100644 --- a/pyduino/pyduino2.py +++ b/pyduino/pyduino2.py @@ -123,7 +123,7 @@ def _send(self, msg): """ Sends command and doesn't await for a response """ - resp = self.http_post("send",msg,False,0) + resp = self.http_post("send",msg,False) return resp.ok def set_in_chunks(self,params,chunksize=4): From 829fb133ef35025019547670035517ac45956a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 17 Mar 2024 23:53:13 -0300 Subject: [PATCH 13/54] replacing meta documents --- pyduino/CITATION.cff => CITATION.cff | 0 pyduino/LICENSE => LICENSE | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename pyduino/CITATION.cff => CITATION.cff (100%) rename pyduino/LICENSE => LICENSE (100%) diff --git a/pyduino/CITATION.cff b/CITATION.cff similarity index 100% rename from pyduino/CITATION.cff rename to CITATION.cff diff --git a/pyduino/LICENSE b/LICENSE similarity index 100% rename from pyduino/LICENSE rename to LICENSE From 572fd8eecb5133559ab79326fa4103a56e54ff26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Mon, 18 Mar 2024 00:05:54 -0300 Subject: [PATCH 14/54] Debug --- pyduino/spectra.py | 1 - tests/test_spectra.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 2430aaa..9c55737 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -216,7 +216,6 @@ def gotod(self): time.sleep(self.deltaTgotod) self.dt = (datetime.now()-self.t_gotod_1).total_seconds() print("[INFO] gotod DT", self.dt) - self.GET("gotod") # === Optimizer methods === diff --git a/tests/test_spectra.py b/tests/test_spectra.py index 8acdc8f..62df042 100644 --- a/tests/test_spectra.py +++ b/tests/test_spectra.py @@ -1,6 +1,7 @@ import sys import os import numpy as np +import pandas as pd sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from pyduino.spectra import Spectra, PATHS @@ -21,9 +22,8 @@ def test_array_assignment(self): assert len(assigned[keys[0]]) == len(self.g.parameters) def test_oracle(self): data = self.g.F_get() - for k in data.keys(): - data[k][self.g.fparam] = '0' df = self.g.pretty_print_dict(data) + assert isinstance(df, pd.DataFrame) y = self.g.ask_oracle(self.g.population) self.g.update_fitness(y) def test_logger(self): From 3cd8bb8a11e2c8770b62cb1a9a9136a5769e37f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 17:12:29 -0300 Subject: [PATCH 15/54] Debug --- .gitignore | 2 ++ pyduino/__init__.py | 5 +++++ pyduino/spectra.py | 20 +++++++++++--------- pyduino/utils.py | 21 +++++++++++++++++++++ requirements.txt | 3 ++- tests/test_utils.py | 25 +++++++++++++++++++++++++ 6 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 tests/test_utils.py diff --git a/.gitignore b/.gitignore index a7e163a..17219c8 100644 --- a/.gitignore +++ b/.gitignore @@ -123,12 +123,14 @@ celerybeat.pid # Environments .env +.env.local .venv env/ venv/ ENV/ env.bak/ venv.bak/ +.vscode/ # Spyder project settings .spyderproject diff --git a/pyduino/__init__.py b/pyduino/__init__.py index 355c638..40e9c58 100644 --- a/pyduino/__init__.py +++ b/pyduino/__init__.py @@ -1,5 +1,10 @@ import logging import os +from dotenv import load_dotenv + +load_dotenv() +load_dotenv(dotenv_path=".env.local") + logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s', diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 9c55737..0a2cda1 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -10,7 +10,7 @@ from pyduino.data_parser import yaml_genetic_algorithm, RangeParser, get_datetimes from collections import OrderedDict from scipy.special import softmax -from pyduino.utils import yaml_get, bcolors, TriangleWave, ReLUP +from pyduino.utils import yaml_get, bcolors, TriangleWave, get_param from pyduino.log import datetime_to_str import traceback @@ -229,7 +229,7 @@ def ask_oracle(self, X) -> np.ndarray: Returns: np.ndarray: The fitness value calculated by the oracle. """ - y = [] + y = np.array([]) assert X.shape[1] == len(self.parameters) assert len(X.shape) == 2, "X must be a 2D array." @@ -240,18 +240,20 @@ def ask_oracle(self, X) -> np.ndarray: reactors = payload.keys() self.gotod() - data0 = pd.DataFrame(self.F_get()) - f0 = data0.loc[reactors,self.density_param] + data0 = self.F_get() + f0 = get_param(data0, self.density_param, reactors) + f0 = np.array(f0.values()) self.F_set(payload) time.sleep(self.deltaT) - data = pd.DataFrame(self.F_get()) - f = data.loc[reactors,self.density_param] - + data = self.F_get() + f = get_param(data, self.density_param, reactors) + f = np.array(f.values()) + alpha = np.log(f/f0)/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ - y += list(alpha) - return np.array(y) + y = np.append(y,alpha) + return y # === * === def run( diff --git a/pyduino/utils.py b/pyduino/utils.py index 44cfa51..331705d 100644 --- a/pyduino/utils.py +++ b/pyduino/utils.py @@ -2,6 +2,7 @@ from nmap import PortScanner import requests import numpy as np +from collections import OrderedDict class bcolors: HEADER = '\033[95m' @@ -14,6 +15,26 @@ class bcolors: BOLD = '\033[1m' UNDERLINE = '\033[4m' +def get_param(data, key: str, ids: set = False) -> OrderedDict: + + """ + Retrieve a specific parameter from a dictionary of data. + + Parameters: + - data: The dictionary containing the data. + - key: The key of the parameter to retrieve. + - ids: (optional) A set of IDs to filter the data. If not provided, all data will be returned. + + Returns: + - An ordered dictionary containing the filtered data. + + """ + filtered = OrderedDict(list(map(lambda x: (x[0], x[1][key]), data.items()))) + if not ids: + return filtered + else: + return OrderedDict(filter(lambda x: x[0] in ids,filtered.items())) + def yaml_get(filename): """ Loads hyperparameters from a YAML file. diff --git a/requirements.txt b/requirements.txt index 9c16729..1e99354 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ pyserial PyYAML requests scipy -tailer \ No newline at end of file +tailer +python-dotenv \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..d183a03 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,25 @@ +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from pyduino.utils import get_param + +def test_get_param(): + # Test case 1: No IDs provided + data = {'A': {'param1': 1, 'param2': 2}, 'B': {'param1': 3, 'param2': 4}} + key = 'param1' + expected_output = {'A': 1, 'B': 3} + assert get_param(data, key) == expected_output + + # Test case 2: IDs provided + data = {'A': {'param1': 1, 'param2': 2}, 'B': {'param1': 3, 'param2': 4}} + key = 'param1' + ids = {'A'} + expected_output = {'A': 1} + assert get_param(data, key, ids) == expected_output + + # Test case 5: Check order of keys with IDs provided + data = {'A': {'param1': 1, 'param2': 2}, 'B': {'param1': 3, 'param2': 4}, 'C': {'param1': 5, 'param2': 6}} + key = 'param1' + ids = {'C', 'A'} + expected_output = {'C': 5, 'A': 1} + assert get_param(data, key, ids) == expected_output \ No newline at end of file From e1d35c4af3501d968dabc84d54b18234b0837e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 17:17:40 -0300 Subject: [PATCH 16/54] debug --- pyduino/spectra.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 0a2cda1..6e4d38d 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -242,13 +242,13 @@ def ask_oracle(self, X) -> np.ndarray: self.gotod() data0 = self.F_get() f0 = get_param(data0, self.density_param, reactors) - f0 = np.array(f0.values()) + f0 = np.array(list(f0.values())) self.F_set(payload) time.sleep(self.deltaT) data = self.F_get() f = get_param(data, self.density_param, reactors) - f = np.array(f.values()) + f = np.array(list(f.values())) alpha = np.log(f/f0)/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ @@ -291,8 +291,6 @@ def run( print("[INFO]","DT",self.dt) self.GET("growing") self.update_fitness(self.data) - print(f"{bcolors.OKCYAN}self.data{bcolors.ENDC}") - print(f"{bcolors.BOLD}{self.data.T.loc[:,self.titled_parameters+['power','efficiency','growth_rate','p']]}{bcolors.ENDC}") #Optimizer if run_optim: self.step() From 88ac876bf2a13daf6c33fd32b251aa329a7a9e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 17:22:23 -0300 Subject: [PATCH 17/54] debug --- pyduino/spectra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 6e4d38d..b8dc40b 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -250,7 +250,7 @@ def ask_oracle(self, X) -> np.ndarray: f = get_param(data, self.density_param, reactors) f = np.array(list(f.values())) - alpha = np.log(f/f0)/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ + alpha = (np.log(f) - np.log(f0))/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ y = np.append(y,alpha) return y From fa0a9e97945572088b8b3a04d4f5cda55799c2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 17:43:16 -0300 Subject: [PATCH 18/54] debug --- pyduino/spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index b8dc40b..00f2a7b 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -242,13 +242,13 @@ def ask_oracle(self, X) -> np.ndarray: self.gotod() data0 = self.F_get() f0 = get_param(data0, self.density_param, reactors) - f0 = np.array(list(f0.values())) + f0 = np.array(list(f0.values())).astype(float) self.F_set(payload) time.sleep(self.deltaT) data = self.F_get() f = get_param(data, self.density_param, reactors) - f = np.array(list(f.values())) + f = np.array(list(f.values())).astype(float) alpha = (np.log(f) - np.log(f0))/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ From 14d16e0fb89fbbddd90ff3f35cb84f6ed0f4c301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 18:49:56 -0300 Subject: [PATCH 19/54] debug --- pyduino/config.yaml | 1 - pyduino/spectra.py | 40 +++++++++++++--------------------------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/pyduino/config.yaml b/pyduino/config.yaml index d6bfb4b..4cf2b36 100644 --- a/pyduino/config.yaml +++ b/pyduino/config.yaml @@ -1,7 +1,6 @@ hyperparameters: reset_density: true #Whether or not to use gotod log_name: "ga_dcte2400_branco_15" #Log folder name (delete to use the default) - f_param: efficiency #Parameter to read as fitness from the reactors density_param: DensidadeAtual #Parameter name for density counts mutation_probability: 0.01 #0.15 maximize: true diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 00f2a7b..e0b14d5 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -7,15 +7,12 @@ import numpy as np import time from datetime import date, datetime -from pyduino.data_parser import yaml_genetic_algorithm, RangeParser, get_datetimes +from pyduino.data_parser import RangeParser from collections import OrderedDict -from scipy.special import softmax from pyduino.utils import yaml_get, bcolors, TriangleWave, get_param from pyduino.log import datetime_to_str import traceback -# asd - __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) #Path to spectrum.json @@ -114,9 +111,8 @@ def __init__(self,elitism,f_param,ranges,density_param,maximize=True,log_name=No self.payload = self.population_as_dict if self.payload is None else self.payload self.data = None self.do_gotod = reset_density - self.fparam = f_param + self.density_param = f_param self.density_param = density_param - self.fitness = np.nan * np.ones(len(self.reactors)) self.maximize = maximize self.dt = np.nan self.elitism = elitism @@ -163,7 +159,7 @@ def pretty_print_dict(self,D): df = pd.DataFrame(D) df.index = df.index.str.lower() df = df.loc[self.parameters,:] - df.loc['fitness'] = self.fitness + df.loc['fitness'] = self.y return df.round(decimals=2) def F_get(self): """ @@ -191,12 +187,7 @@ def set_spectrum(self,preset): def set_preset_state_spectra(self,*args,**kwargs): self.set_preset_state(*args,**kwargs) - self.population = self.inverse_view(self.payload_to_matrix()).astype(int) - - def update_fitness(self,X): - #Get and return parameter chosen for fitness - self.fitness = ((-1)**(self.maximize))*X.loc[self.fparam].astype(float).to_numpy() - return self.fitness + self.population = self.inverse_view(self.payload_to_matrix()).astype(int) def GET(self,tag): """ @@ -204,9 +195,9 @@ def GET(self,tag): """ print("[INFO]","GET",datetime.now().strftime("%c")) self.past_data = self.data.copy() if self.data is not None else pd.DataFrame(self.payload) - self.data = pd.DataFrame(self.F_get()) - self.log.log_many_rows(self.data,tags={'growth_state':tag}) - self.log.log_optimal(column=self.fparam,maximum=self.maximize,tags={'growth_state':tag}) + self.data = self.F_get() + self.log.log_many_rows(pd.DataFrame(self.data),tags={'growth_state':tag}) + self.log.log_optimal(column=self.density_param,maximum=self.maximize,tags={'growth_state':tag}) self.log.log_average(tags={'growth_state':tag}) def gotod(self): @@ -218,7 +209,6 @@ def gotod(self): print("[INFO] gotod DT", self.dt) # === Optimizer methods === - def ask_oracle(self, X) -> np.ndarray: """ Asks the oracle for the fitness of the given input. @@ -233,7 +223,8 @@ def ask_oracle(self, X) -> np.ndarray: assert X.shape[1] == len(self.parameters) assert len(X.shape) == 2, "X must be a 2D array." - partitions = np.array_split(X, len(self.reactors)) + n_partitions = len(X) // len(self.reactors) + (len(X) % len(self.reactors) > 0) + partitions = np.array_split(X, n_partitions) for partition in partitions: payload = self.assign_to_reactors(partition) @@ -253,7 +244,7 @@ def ask_oracle(self, X) -> np.ndarray: alpha = (np.log(f) - np.log(f0))/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ y = np.append(y,alpha) - return y + return -y # === * === def run( @@ -290,16 +281,10 @@ def run( self.dt = (datetime.now()-self.t_grow_1).total_seconds() print("[INFO]","DT",self.dt) self.GET("growing") - self.update_fitness(self.data) #Optimizer if run_optim: self.step() - else: - df = self.data.T - df.columns = df.columns.str.lower() - self.payload = df[self.parameters].T.to_dict() - self.population = self.inverse_view(self.payload_to_matrix()).astype(int) - self.gotod() + self.gotod() print("[INFO]","SET",datetime.now().strftime("%c")) self.iteration_counter += 1 except Exception as e: @@ -316,6 +301,8 @@ def run_incremental( bounds:list = [100,0] ): """ + OBSOLETE + Runs reading and wiriting operations in an infinite loop on intervals given by `deltaT` and increments parameters periodically on an interval given by `deltaClockHours`. @@ -345,7 +332,6 @@ def run_incremental( while True: self.t1 = datetime.now() self.GET("growing") - self.update_fitness(self.data) #gotod if self.do_gotod: self.send("gotod",await_response=False) From 65c5182a4a1ecb003d0d254978d168922f5e9d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 18:51:12 -0300 Subject: [PATCH 20/54] debug --- pyduino/spectra.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index e0b14d5..3e335b8 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -74,10 +74,9 @@ def parse_dados(X,param): return np.array(list(map(seval,map(lambda x: x[1].get(param,0),sorted(X.items(),key=lambda x: x[0]))))) class Spectra(RangeParser,ReactorManager,NelderMead): - def __init__(self,elitism,f_param,ranges,density_param,maximize=True,log_name=None,reset_density=False,**kwargs): + def __init__(self,elitism,ranges,density_param,maximize=True,log_name=None,reset_density=False,**kwargs): """ Args: - f_param (str): Parameter name to be extracted from `ReactorManager.log_dados`. ranges (:obj:dict of :obj:list): Dictionary of parameters with a two element list containing the its minimum and maximum attainable values respectively. reset_density (bool): Whether or not to reset density values on the reactors at each iteration. From ea0036086697281118719acaed948da2c5eef3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 18:52:11 -0300 Subject: [PATCH 21/54] debug --- pyduino/spectra.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 3e335b8..eaf45c0 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -110,7 +110,6 @@ def __init__(self,elitism,ranges,density_param,maximize=True,log_name=None,reset self.payload = self.population_as_dict if self.payload is None else self.payload self.data = None self.do_gotod = reset_density - self.density_param = f_param self.density_param = density_param self.maximize = maximize self.dt = np.nan From 1bdd6807376aa8fe2afed855f5c943911d529302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 18:54:39 -0300 Subject: [PATCH 22/54] debug --- pyduino/optimization/nelder_mead.py | 3 +++ tests/test_spectra.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyduino/optimization/nelder_mead.py b/pyduino/optimization/nelder_mead.py index 2b1ae3c..c24b701 100644 --- a/pyduino/optimization/nelder_mead.py +++ b/pyduino/optimization/nelder_mead.py @@ -25,6 +25,9 @@ def __init__(self, population_size: int, ranges: List[float], rng_seed: int = 0) # Initialize the population (random position and initial momentum) self.population = self.rng_seed.random((self.population_size, len(self.ranges))) + + # Initialize y as vector of nans + self.y = np.full(self.population_size, np.nan) def view(self, x): """ diff --git a/tests/test_spectra.py b/tests/test_spectra.py index 62df042..f7eb51f 100644 --- a/tests/test_spectra.py +++ b/tests/test_spectra.py @@ -25,6 +25,5 @@ def test_oracle(self): df = self.g.pretty_print_dict(data) assert isinstance(df, pd.DataFrame) y = self.g.ask_oracle(self.g.population) - self.g.update_fitness(y) def test_logger(self): self.g.GET("test") \ No newline at end of file From 4f3ad35c9c990fc9f0ef142079fbd0f2d475eafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 19:07:58 -0300 Subject: [PATCH 23/54] debug --- pyduino/log.py | 2 +- pyduino/spectra.py | 6 ++++++ tests/test_spectra.py | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pyduino/log.py b/pyduino/log.py index 3322f3a..ad68aa5 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -124,7 +124,7 @@ def log_optimal(self,column,maximum=True,**kwargs): """ Logs optima of all rows into a single file. """ - i=self.data_frames.loc[:,column].argmax() if maximum else self.data_frames.loc[:,column].argmin() + i=self.data_frames.loc[:,column].astype(float).argmax() if maximum else self.data_frames.loc[:,column].astype(float).argmin() self.df_opt = self.data_frames.iloc[i,:] self.log_rows(rows=[self.df_opt.to_dict()],subdir='opt',sep='\t',**kwargs) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index eaf45c0..357112a 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -176,6 +176,12 @@ def F_set(self,x): for chk in chunks(list(params.items()),3): self.reactors[_id].set(dict(chk)) time.sleep(1) + def init(self): + """ + Sets payload to the reactors. + """ + self.F_set(self.payload) + def set_spectrum(self,preset): """ Sets all reactors with a preset spectrum contained in `SPECTRUM_PATH`. diff --git a/tests/test_spectra.py b/tests/test_spectra.py index f7eb51f..65b21e6 100644 --- a/tests/test_spectra.py +++ b/tests/test_spectra.py @@ -10,6 +10,8 @@ class TestSpectra: g = Spectra(**PATHS.HYPERPARAMETERS) g.deltaTgotod = 1 g.deltaT = 1 + g.init() + def test_functions(self): assert len(self.g.reactors) != 0 assert len(self.g.ids) == self.g.population_size From 2d404df15581abfb7b763de190cfdae0dd014433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 19:12:38 -0300 Subject: [PATCH 24/54] debug --- pyduino/log.py | 12 ++++++++---- pyduino/spectra.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pyduino/log.py b/pyduino/log.py index ad68aa5..7a05db1 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -128,14 +128,18 @@ def log_optimal(self,column,maximum=True,**kwargs): self.df_opt = self.data_frames.iloc[i,:] self.log_rows(rows=[self.df_opt.to_dict()],subdir='opt',sep='\t',**kwargs) - def log_average(self,**kwargs): + def log_average(self, cols: list, **kwargs): """ - Logs average of all rows into a single file. + Calculate the average values of specified columns in the data frames and log the results. + + Parameters: + - cols (list): A list of column names to calculate the average for. + - **kwargs: Additional keyword arguments to customize the logging process. """ df = self.data_frames.copy() df.elapsed_time_hours = df.elapsed_time_hours.round(decimals=2) - self.df_avg = df.groupby("elapsed_time_hours").mean().reset_index() - self.log_rows(rows=self.df_avg,subdir='avg',sep='\t',**kwargs) + self.df_avg = df.loc[:, cols].groupby("elapsed_time_hours").mean().reset_index() + self.log_rows(rows=self.df_avg, subdir='avg', sep='\t', **kwargs) def cache_data(self,rows,path="./cache.csv",**kwargs): """ diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 357112a..5210b60 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -202,7 +202,7 @@ def GET(self,tag): self.data = self.F_get() self.log.log_many_rows(pd.DataFrame(self.data),tags={'growth_state':tag}) self.log.log_optimal(column=self.density_param,maximum=self.maximize,tags={'growth_state':tag}) - self.log.log_average(tags={'growth_state':tag}) + self.log.log_average(tags={'growth_state':tag}, cols=[self.density_param]) def gotod(self): self.t_gotod_1 = datetime.now() From 3aecd86f787d3b9415f68bce81b2d8a2f3791db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 19:14:33 -0300 Subject: [PATCH 25/54] debug --- pyduino/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyduino/log.py b/pyduino/log.py index 7a05db1..469cde2 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -138,7 +138,7 @@ def log_average(self, cols: list, **kwargs): """ df = self.data_frames.copy() df.elapsed_time_hours = df.elapsed_time_hours.round(decimals=2) - self.df_avg = df.loc[:, cols].groupby("elapsed_time_hours").mean().reset_index() + self.df_avg = df.loc[:, cols + ['elapsed_time_hours']].groupby("elapsed_time_hours").mean().reset_index() self.log_rows(rows=self.df_avg, subdir='avg', sep='\t', **kwargs) def cache_data(self,rows,path="./cache.csv",**kwargs): From dbf224f34ad67cbb2bedf5a35067652f9733c78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 23 Mar 2024 19:16:25 -0300 Subject: [PATCH 26/54] debug --- pyduino/log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyduino/log.py b/pyduino/log.py index 469cde2..3ac1d5d 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -137,6 +137,7 @@ def log_average(self, cols: list, **kwargs): - **kwargs: Additional keyword arguments to customize the logging process. """ df = self.data_frames.copy() + df.loc[:, cols] = df.loc[:, cols].astype(float) df.elapsed_time_hours = df.elapsed_time_hours.round(decimals=2) self.df_avg = df.loc[:, cols + ['elapsed_time_hours']].groupby("elapsed_time_hours").mean().reset_index() self.log_rows(rows=self.df_avg, subdir='avg', sep='\t', **kwargs) From 493597c206bcf4d1a0b43ac06f8d2597c140d300 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 24 Mar 2024 15:50:12 -0300 Subject: [PATCH 27/54] Changed growth rate to yield rate --- pyduino/config.yaml | 2 +- pyduino/spectra.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyduino/config.yaml b/pyduino/config.yaml index 4cf2b36..3e367e6 100644 --- a/pyduino/config.yaml +++ b/pyduino/config.yaml @@ -58,7 +58,7 @@ hyperparameters: slave: port: "5000" #Must be a string network: "192.168.1.1/24" - exclude: "192.168.1.112" + exclude: #"192.168.1.112" system: initial_state: "preset_state.d/no4.csv" reboot_wait_time: 5 #Time in seconds to wait after a reboot. diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 5210b60..de70ac7 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -245,7 +245,8 @@ def ask_oracle(self, X) -> np.ndarray: f = get_param(data, self.density_param, reactors) f = np.array(list(f.values())).astype(float) - alpha = (np.log(f) - np.log(f0))/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ + #alpha = (np.log(f) - np.log(f0))/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ + alpha = (f/f0 - 1)/self.deltaT #Yield rate (not growth rate) y = np.append(y,alpha) return -y From 2c4a84d272b0f464842e98d070a284cb52b2bbaa Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 24 Mar 2024 16:01:37 -0300 Subject: [PATCH 28/54] Removed clutter --- pyduino/spectra.py | 82 +++------------------------------------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index de70ac7..bebc236 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -12,6 +12,7 @@ from pyduino.utils import yaml_get, bcolors, TriangleWave, get_param from pyduino.log import datetime_to_str import traceback +import warnings __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) @@ -268,7 +269,10 @@ def run( """ #Checking if gotod time is at least five minutes - if run_optim and (deltaTgotod is None or deltaTgotod <= 300): raise ValueError("deltaTgotod must be at least 5 minutes.") + if isinstance(deltaTgotod, int): + if run_optim and deltaTgotod <= 300: warnings.warn("deltaTgotod should be at least 5 minutes.") + else: + raise ValueError("deltaTgotod must be an integer") self.deltaT = deltaT self.deltaTgotod = deltaTgotod @@ -296,81 +300,5 @@ def run( traceback.print_exc(file=log_file) raise(e) - def run_incremental( - self, - deltaT: int, - parameter: str, - deltaTgotod: int = None, - N: int = 1, - M: int = 1, - bounds:list = [100,0] - ): - """ - OBSOLETE - - Runs reading and wiriting operations in an infinite loop on intervals given by `deltaT` and increments parameters - periodically on an interval given by `deltaClockHours`. - - Args: - deltaT (int): Amount of time in seconds to wait in each iteration. - parameter (str): Name of the parameters to be updated. - deltaTgotod (int, optional): Time to wait after sending `gotod` command. - N (int): Number of iteration groups to wait to trigger a parameter update. - M (int): Number of iterations to wait to increment `N`. - bounds: Starts on `bounds[0]` and goes towards `bounds[1]`. Then, the other way around. - """ - - #Initialize stepping - df = pd.DataFrame(self.payload).T - self.triangles = list(map(lambda x: TriangleWave(x,bounds[0],bounds[1],N),df.loc[:,parameter].to_list())) - self.triangle_wave_state = 1 if bounds[1] >= bounds[0] else -1 - c = 0 - m = 0 - - #Checking if gotod time is at least five minutes - if deltaTgotod is not None and deltaTgotod < 5*60: print(bcolors.WARNING,"[WARNING]","deltaTgotod should be at least 5 minutes.",bcolors.ENDC) - - with open("error_traceback.log","w") as log_file: - log_file.write(datetime_to_str(self.log.timestamp)+'\n') - try: - self.deltaT = deltaT - while True: - self.t1 = datetime.now() - self.GET("growing") - #gotod - if self.do_gotod: - self.send("gotod",await_response=False) - print("[INFO] gotod sent") - time.sleep(deltaTgotod) - self.dt = (datetime.now()-self.t1).total_seconds() - print("[INFO] gotod DT", self.dt) - self.GET("gotod") - self.t1 = datetime.now() - - # Pick up original parameters from preset state and increment them with `parameter_increment`. - df = pd.DataFrame(self.data).T - df.loc[:,parameter] = list(map(lambda T: T.y(c),self.triangles)) - print("[INFO]","WAVE","UP" if self.triangle_wave_state > 0 else "DOWN", "COUNTER", str(m), "LEVEL", str(c)) - if c%N==0: - self.triangle_wave_state *= -1 - # --------------------------- - - df.columns = df.columns.str.lower() - self.payload = df[self.parameters].T.to_dict() - self.population = self.inverse_view(self.payload_to_matrix()).astype(int) - print("[INFO]","SET",self.t1.strftime("%c")) - self.F_set(self.payload) - time.sleep(max(2,deltaT)) - m+=1 - if (m%M)==0: - c += 1 - self.t2 = datetime.now() - self.dt = (self.t2-self.t1).total_seconds() - print("[INFO]","DT",self.dt) - except Exception as e: - traceback.print_exc(file=log_file) - raise(e) - - if __name__ == "__main__": g = Spectra(**hyperparameters) From db279541e67004a853633f8924d57257b75eef17 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 24 Mar 2024 16:35:39 -0300 Subject: [PATCH 29/54] Fitness will now divide by total power consumption --- pyduino/preset_state.d/all.csv | 8 ++++++++ pyduino/spectra.py | 20 +++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 pyduino/preset_state.d/all.csv diff --git a/pyduino/preset_state.d/all.csv b/pyduino/preset_state.d/all.csv new file mode 100644 index 0000000..7ac6f84 --- /dev/null +++ b/pyduino/preset_state.d/all.csv @@ -0,0 +1,8 @@ +ID brilho branco full 440 470 495 530 595 634 660 684 cor modopainel bomdia boanoite tau step modotemp temp vent pelt densidade mododil dil dildt r b ir ar ima ganho ti modoco2 co2 co2ref dtco2 dil dilDt in out +1 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 15000000 " " " " " " 40 25 1 1000 +2 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 12500000 " " " " " " 40 25 1 1000 +3 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 10000000 " " " " " " 40 25 1 1000 +8 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 10000000 " " " " " " 40 25 1 1000 +5 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 7500000 " " " " " " 40 25 1 1000 +6 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 5000000 " " " " " " 40 25 1 1000 +7 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 2500000 " " " " " " 40 25 1 1000 \ No newline at end of file diff --git a/pyduino/spectra.py b/pyduino/spectra.py index bebc236..6134b04 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -95,8 +95,7 @@ def __init__(self,elitism,ranges,density_param,maximize=True,log_name=None,reset RangeParser.__init__(self,ranges,self.parameters) - self.irradiance = PATHS.SYSTEM_PARAMETERS['irradiance']#yaml_get(IRRADIANCE_PATH) - self.irradiance = pd.Series(self.irradiance) + self.irradiance = PATHS.SYSTEM_PARAMETERS['irradiance'] ReactorManager.__init__(self) NelderMead.__init__( @@ -142,6 +141,11 @@ def population_as_dict(self): Converts genome matrix into an appropriate format to send to the reactors. """ return self.assign_to_reactors(self.population) + + @property + def power(self): + 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): return np.nan_to_num( np.array( @@ -232,24 +236,26 @@ def ask_oracle(self, X) -> np.ndarray: partitions = np.array_split(X, n_partitions) for partition in partitions: - payload = self.assign_to_reactors(partition) - reactors = payload.keys() + self.payload = self.assign_to_reactors(partition) + reactors = self.payload.keys() self.gotod() data0 = self.F_get() f0 = get_param(data0, self.density_param, reactors) f0 = np.array(list(f0.values())).astype(float) - self.F_set(payload) + self.F_set(self.payload) time.sleep(self.deltaT) data = self.F_get() f = get_param(data, self.density_param, reactors) f = np.array(list(f.values())).astype(float) #alpha = (np.log(f) - np.log(f0))/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ - alpha = (f/f0 - 1)/self.deltaT #Yield rate (not growth rate) + yield_rate = (f/f0 - 1)/self.deltaT #Yield rate (not growth rate) + + fitness = yield_rate/self.power - y = np.append(y,alpha) + y = np.append(y,fitness) return -y # === * === From 2926e1784c610306f0848cd53ac01f7a7c88d274 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 24 Mar 2024 17:12:33 -0300 Subject: [PATCH 30/54] power to zero test --- pyduino/spectra.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 6134b04..46aadf9 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -242,18 +242,15 @@ def ask_oracle(self, X) -> np.ndarray: self.gotod() data0 = self.F_get() f0 = get_param(data0, self.density_param, reactors) - f0 = np.array(list(f0.values())).astype(float) self.F_set(self.payload) time.sleep(self.deltaT) data = self.F_get() f = get_param(data, self.density_param, reactors) - f = np.array(list(f.values())).astype(float) - #alpha = (np.log(f) - np.log(f0))/self.deltaT #Growth Rate $f=f_0 exp(alpha T)$ - yield_rate = (f/f0 - 1)/self.deltaT #Yield rate (not growth rate) + #yield_rate = np.array([(float(f[id])/float(f[id]) - 1)/self.deltaT/self.power[id] for id in reactors]).astype(float) - fitness = yield_rate/self.power + fitness = np.array([self.power[id] for id in reactors]).astype(float) y = np.append(y,fitness) return -y From 5afd3b8a01438ded4d413baf0935314e515953c1 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 24 Mar 2024 18:13:58 -0300 Subject: [PATCH 31/54] debug --- pyduino/config.yaml | 36 ++++++++++++++++-------------------- pyduino/dashboard.py | 2 +- pyduino/spectra.py | 10 +++++----- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/pyduino/config.yaml b/pyduino/config.yaml index 3e367e6..b3d6f6f 100644 --- a/pyduino/config.yaml +++ b/pyduino/config.yaml @@ -1,12 +1,8 @@ hyperparameters: reset_density: true #Whether or not to use gotod - log_name: "ga_dcte2400_branco_15" #Log folder name (delete to use the default) + log_name: "nelder_mead_power_test" #Log folder name (delete to use the default) density_param: DensidadeAtual #Parameter name for density counts - mutation_probability: 0.01 #0.15 - maximize: true - resolution: 4 #Number of bits used for each parameter - elitism: true #Whether or not to use elitism - do_crossover: false #Whether or not to perform crossover when the genetic algorithm is enabled + maximize: false rng_seed: 2 #Random seed for genetic algorithm ranges: # brilho: @@ -60,7 +56,7 @@ slave: network: "192.168.1.1/24" exclude: #"192.168.1.112" system: - initial_state: "preset_state.d/no4.csv" + initial_state: "preset_state.d/all.csv" reboot_wait_time: 5 #Time in seconds to wait after a reboot. relevant_parameters: # - brilho @@ -115,7 +111,7 @@ system: co2: int dtco2: int dash: - glob: "./log/pre_free1212_branco_3/*.csv" + glob: "./log/nelder_mead_power_test/*.csv" plot: - name: Temperature @@ -131,18 +127,18 @@ dash: name: Density cols: DensidadeAtual: - - - name: Efficiency - cols: - efficiency: - growing_only: true - #mode: 'lines+markers' - - - name: GrowthRate - cols: - growth_rate: - growing_only: true - #mode: 'lines+markers' + # - + # name: Efficiency + # cols: + # efficiency: + # growing_only: true + # #mode: 'lines+markers' + # - + # name: GrowthRate + # cols: + # growth_rate: + # growing_only: true + # #mode: 'lines+markers' - name: power cols: diff --git a/pyduino/dashboard.py b/pyduino/dashboard.py index 31f1504..0103a7e 100644 --- a/pyduino/dashboard.py +++ b/pyduino/dashboard.py @@ -40,7 +40,7 @@ NROWSDF = y["tail"] if len(LOG_PATH)!=0: with open(LOG_PATH[0]) as file: - HEAD = pd.read_csv(io.StringIO(''.join([file.readline() for _ in range(2)])),sep=SEP) + HEAD = pd.read_csv(io.StringIO(''.join([file.readline() for _ in range(1)])),sep=SEP) print(HEAD) HEAD = HEAD.columns else: diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 46aadf9..7d4b0f2 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -75,7 +75,7 @@ def parse_dados(X,param): return np.array(list(map(seval,map(lambda x: x[1].get(param,0),sorted(X.items(),key=lambda x: x[0]))))) class Spectra(RangeParser,ReactorManager,NelderMead): - def __init__(self,elitism,ranges,density_param,maximize=True,log_name=None,reset_density=False,**kwargs): + def __init__(self,ranges,density_param,maximize=True,log_name=None,reset_density=False,**kwargs): """ Args: ranges (:obj:dict of :obj:list): Dictionary of parameters with a two element list containing the @@ -100,7 +100,7 @@ def __init__(self,elitism,ranges,density_param,maximize=True,log_name=None,reset ReactorManager.__init__(self) NelderMead.__init__( self, - population_size=len(self.reactors), + population_size=len(self.parameters), ranges=self.ranges_as_list(), rng_seed=kwargs.get('rng_seed',0) ) @@ -113,7 +113,6 @@ def __init__(self,elitism,ranges,density_param,maximize=True,log_name=None,reset self.density_param = density_param self.maximize = maximize self.dt = np.nan - self.elitism = elitism def assign_to_reactors(self, x): """ Assigns a list of parameters to the reactors. @@ -252,8 +251,9 @@ def ask_oracle(self, X) -> np.ndarray: fitness = np.array([self.power[id] for id in reactors]).astype(float) - y = np.append(y,fitness) - return -y + y = np.append(y,((-1)**(self.maximize))*(fitness)) + + return y # === * === def run( From c98817ec04cbbe38239e86d6c9e4a957eab95196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Mon, 25 Mar 2024 04:57:32 -0300 Subject: [PATCH 32/54] Limited Nelder Mead with sigmoid --- pyduino/__init__.py | 5 +- pyduino/optimization/nelder_mead.py | 18 ++++-- tests/__init__.py | 3 + tests/test_linmap.py | 3 + tests/test_optim.py | 87 ++++++++++++++++++----------- tests/test_spectra.py | 3 + tests/test_utils.py | 3 + 7 files changed, 81 insertions(+), 41 deletions(-) create mode 100644 tests/__init__.py diff --git a/pyduino/__init__.py b/pyduino/__init__.py index 40e9c58..e59edfd 100644 --- a/pyduino/__init__.py +++ b/pyduino/__init__.py @@ -5,11 +5,14 @@ load_dotenv() load_dotenv(dotenv_path=".env.local") +log_file_path = os.environ.get('PYDUINO_SLAVE_LOG','/var/log/pyduino/slave.log') +os.makedirs(os.path.dirname(log_file_path), exist_ok=True) + logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s', handlers=[ - logging.FileHandler(os.environ.get('PYDUINO_SLAVE_LOG','/var/log/pyduino/slave.log')), + logging.FileHandler(log_file_path), logging.StreamHandler() ] ) \ No newline at end of file diff --git a/pyduino/optimization/nelder_mead.py b/pyduino/optimization/nelder_mead.py index c24b701..094b169 100644 --- a/pyduino/optimization/nelder_mead.py +++ b/pyduino/optimization/nelder_mead.py @@ -2,8 +2,13 @@ from typing import Callable, List from . import Optimizer, linmap +def sigmoid(x): + return 1 / (1 + np.exp(-x)) +def logit(x, inf=100): + return np.where(x==0, -inf, np.where(x==1, inf, np.log(x) - np.log(1-x))) + class NelderMead(Optimizer): - def __init__(self, population_size: int, ranges: List[float], rng_seed: int = 0): + def __init__(self, population_size: int, ranges: List[float], rng_seed: int = 0, hypercube_radius = 100): """ This Nelder-Mead algorithm assumes that the optimization function 'f' is to be minimized and time independent. @@ -20,8 +25,9 @@ def __init__(self, population_size: int, ranges: List[float], rng_seed: int = 0) self.rng_seed = np.random.default_rng(rng_seed) # Derived attributes - self.invlinmap = linmap(self.ranges, np.array([[0, 1]] * len(self.ranges))) - self.linmap = linmap(np.array([[0, 1]] * len(self.ranges)), self.ranges) + self.a = 1/hypercube_radius + self.backward = lambda x: logit(linmap(self.ranges, np.array([[0, 1]] * len(self.ranges)))(x))/self.a + self.forward = lambda x: linmap(np.array([[0, 1]] * len(self.ranges)), self.ranges)(sigmoid(self.a * x)) # Initialize the population (random position and initial momentum) self.population = self.rng_seed.random((self.population_size, len(self.ranges))) @@ -33,19 +39,19 @@ def view(self, x): """ Maps the input from the domain to the codomain. """ - return self.linmap(x) + return self.forward(x) def view_g(self): """ Maps the input from the domain to the codomain. """ - return self.linmap(self.population) + return self.forward(self.population) def inverse_view(self, x): """ Maps the input from the codomain to the domain. """ - return self.invlinmap(x) + return self.backward(x) def ask_oracle(self, X: np.ndarray) -> np.ndarray: return super().ask_oracle() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..afb9462 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +import dotenv +dotenv.load_dotenv() +dotenv.load_dotenv(dotenv_path=".env.local") \ No newline at end of file diff --git a/tests/test_linmap.py b/tests/test_linmap.py index 204dc74..e1ad299 100644 --- a/tests/test_linmap.py +++ b/tests/test_linmap.py @@ -2,6 +2,9 @@ import sys import os import numpy as np +import dotenv +dotenv.load_dotenv() +dotenv.load_dotenv(dotenv_path=".env.local") sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from pyduino.optimization import linmap diff --git a/tests/test_optim.py b/tests/test_optim.py index 106c336..d752b50 100644 --- a/tests/test_optim.py +++ b/tests/test_optim.py @@ -1,42 +1,61 @@ import sys import os import numpy as np +import dotenv +import dotenv +import pytest +dotenv.load_dotenv() +dotenv.load_dotenv(dotenv_path=".env.local") sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from pyduino.optimization.nelder_mead import NelderMead -def test_nelder_mead_parabola(): +class TestOptim: optimizer = NelderMead(population_size=10, ranges=[(-10, 10)] * 4) - class Oracle: - def ask(self, X): - return (X**2).sum(axis=1) - oracle = Oracle() - optimizer.ask_oracle = oracle.ask - - # Set initial population to oracle - optimizer.init_oracle() - - for i in range(100): - optimizer.step() - assert optimizer.view(optimizer.population).shape == (10, 4) - assert optimizer.view_g().shape == (10, 4) - assert optimizer.inverse_view(optimizer.view(optimizer.population)).shape == (10, 4) - assert np.isclose(optimizer.y.min(), 0, atol=1e-4), f"Oracle: {optimizer.y.min()}" - -def test_nelder_mead_rosenbrock(): - optimizer = NelderMead(population_size=10, ranges=[(-10, 10)] * 4) - class Oracle: - def ask(self, X): - return ((1 - X[:, :-1])**2).sum(axis=1) + 100 * ((X[:, 1:] - X[:, :-1]**2)**2).sum(axis=1) - oracle = Oracle() - optimizer.ask_oracle = oracle.ask - - # Set initial population to oracle - optimizer.init_oracle() - - for i in range(1000): - optimizer.step() - assert optimizer.view(optimizer.population).shape == (10, 4) - assert optimizer.view_g().shape == (10, 4) - assert optimizer.inverse_view(optimizer.view(optimizer.population)).shape == (10, 4) - assert np.isclose(optimizer.y.min(), 0, atol=1e-4), f"Oracle: {optimizer.y.min()}" \ No newline at end of file + + def test_logic(self): + x = (2*np.random.random((10, 4))-1)*100 + + print("X") + print(x) + print("Forward -> Backward") + print(self.optimizer.backward(self.optimizer.forward(x))) + + assert np.allclose(x, self.optimizer.backward(self.optimizer.forward(x))) + assert np.allclose(x, self.optimizer.inverse_view(self.optimizer.view(x))) + + assert np.all(self.optimizer.backward(np.zeros((10,4))) < 1) + + def test_nelder_mead_parabola(self): + class Oracle: + def ask(self, X): + return (X**2).sum(axis=1) + oracle = Oracle() + self.optimizer.ask_oracle = oracle.ask + + # Set initial population to oracle + self.optimizer.init_oracle() + + for i in range(100): + self.optimizer.step() + assert self.optimizer.view(self.optimizer.population).shape == (10, 4) + assert self.optimizer.view_g().shape == (10, 4) + assert self.optimizer.inverse_view(self.optimizer.view(self.optimizer.population)).shape == (10, 4) + assert np.isclose(self.optimizer.y.min(), 0, atol=1e-4), f"Oracle: {self.optimizer.y.min()}" + + def test_nelder_mead_rosenbrock(self): + class Oracle: + def ask(self, X): + return ((1 - X[:, :-1])**2).sum(axis=1) + 100 * ((X[:, 1:] - X[:, :-1]**2)**2).sum(axis=1) + oracle = Oracle() + self.optimizer.ask_oracle = oracle.ask + + # Set initial population to oracle + self.optimizer.init_oracle() + + for i in range(1000): + self.optimizer.step() + assert self.optimizer.view(self.optimizer.population).shape == (10, 4) + assert self.optimizer.view_g().shape == (10, 4) + assert self.optimizer.inverse_view(self.optimizer.view(self.optimizer.population)).shape == (10, 4) + assert np.isclose(self.optimizer.y.min(), 0, atol=1e-4), f"Oracle: {self.optimizer.y.min()}" \ No newline at end of file diff --git a/tests/test_spectra.py b/tests/test_spectra.py index 65b21e6..163ee89 100644 --- a/tests/test_spectra.py +++ b/tests/test_spectra.py @@ -2,6 +2,9 @@ import os import numpy as np import pandas as pd +import dotenv +dotenv.load_dotenv() +dotenv.load_dotenv(dotenv_path=".env.local") sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from pyduino.spectra import Spectra, PATHS diff --git a/tests/test_utils.py b/tests/test_utils.py index d183a03..31c47fe 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,6 +2,9 @@ import os sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from pyduino.utils import get_param +import dotenv +dotenv.load_dotenv() +dotenv.load_dotenv(dotenv_path=".env.local") def test_get_param(): # Test case 1: No IDs provided From 798ab0a82491688ec02e4b642642bb0e416c7326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 01:47:39 -0300 Subject: [PATCH 33/54] Spectra will now log to tensorbaord --- pyduino/spectra.py | 19 ++++++++++++++++--- requirements.txt | 3 ++- tests/test_spectra.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 7d4b0f2..5c5efdc 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -6,6 +6,7 @@ import pandas as pd import numpy as np import time +from tensorboardX import SummaryWriter from datetime import date, datetime from pyduino.data_parser import RangeParser from collections import OrderedDict @@ -106,7 +107,8 @@ def __init__(self,ranges,density_param,maximize=True,log_name=None,reset_density ) self.ids = list(self.reactors.keys()) self.sorted_ids = sorted(self.ids) - self.log_init(name=log_name) + self.log_init(name=log_name) + self.writer = SummaryWriter(self.log.path) self.payload = self.population_as_dict if self.payload is None else self.payload self.data = None self.do_gotod = reset_density @@ -205,8 +207,17 @@ def GET(self,tag): self.past_data = self.data.copy() if self.data is not None else pd.DataFrame(self.payload) self.data = self.F_get() self.log.log_many_rows(pd.DataFrame(self.data),tags={'growth_state':tag}) - self.log.log_optimal(column=self.density_param,maximum=self.maximize,tags={'growth_state':tag}) - self.log.log_average(tags={'growth_state':tag}, cols=[self.density_param]) + + def log_tensor(self, i): + """ + Logs the tensor values and fitness scores. + + This method iterates over the tensor values and fitness scores and logs them using the writer object. + """ + for j, params in enumerate(self.view_g()): + for param_name, param in zip(self.parameters,params): + self.writer.add_scalar(f"{param_name}/{j}", param, i) + self.writer.add_scalar(f'Fitness/{j}', self.y[j], i) def gotod(self): self.t_gotod_1 = datetime.now() @@ -253,6 +264,7 @@ def ask_oracle(self, X) -> np.ndarray: y = np.append(y,((-1)**(self.maximize))*(fitness)) + self.y = y return y # === * === @@ -298,6 +310,7 @@ def run( self.step() self.gotod() print("[INFO]","SET",datetime.now().strftime("%c")) + self.log_tensor(self.iteration_counter) self.iteration_counter += 1 except Exception as e: traceback.print_exc(file=log_file) diff --git a/requirements.txt b/requirements.txt index 1e99354..0dabaa0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ PyYAML requests scipy tailer -python-dotenv \ No newline at end of file +python-dotenv +tensorboardX==2.6.2.2 diff --git a/tests/test_spectra.py b/tests/test_spectra.py index 163ee89..272bdca 100644 --- a/tests/test_spectra.py +++ b/tests/test_spectra.py @@ -31,4 +31,37 @@ def test_oracle(self): assert isinstance(df, pd.DataFrame) y = self.g.ask_oracle(self.g.population) def test_logger(self): - self.g.GET("test") \ No newline at end of file + self.g.GET("test") + def test_log_tensor(self): + # Create a mock writer object + class MockWriter: + def __init__(self): + self.scalar_values = {} + + def add_scalar(self, name, value, step): + if name not in self.scalar_values: + self.scalar_values[name] = {} + self.scalar_values[name][step] = value + + # Create an instance of the Spectra class + g = Spectra(**PATHS.HYPERPARAMETERS) + g.init() + g.y = list(range(len(g.parameters))) + + # Create a mock writer object and assign it to the Spectra instance + mock_writer = MockWriter() + g.writer = mock_writer + + # Call the log_tensor method + g.log_tensor(0) + + # Assert that the scalar values were logged correctly + for j, params in enumerate(g.view_g()): + for param_name, param in zip(g.parameters, params): + expected_name = f"{param_name}/{j}" + expected_value = param + assert mock_writer.scalar_values[expected_name][0] == expected_value + + expected_fitness_name = f"Fitness/{j}" + expected_fitness_value = g.y[j] + assert mock_writer.scalar_values[expected_fitness_name][0] == expected_fitness_value \ No newline at end of file From 94a608afc53f44951c82346aa7127132a2178c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 02:03:41 -0300 Subject: [PATCH 34/54] Debug Nelder Mead --- pyduino/optimization/nelder_mead.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyduino/optimization/nelder_mead.py b/pyduino/optimization/nelder_mead.py index 094b169..81324ee 100644 --- a/pyduino/optimization/nelder_mead.py +++ b/pyduino/optimization/nelder_mead.py @@ -65,8 +65,8 @@ def step(self): """ # Sort the population by the value of the oracle - self.y = self.ask_oracle(self.view_g()) - idx = np.argsort(self.y) + y = self.ask_oracle(self.view_g()) + idx = np.argsort(y) self.population = self.population[idx] # Compute the centroid of the population @@ -81,7 +81,7 @@ def step(self): # If the reflected point is better than the second worst, but not better than the best, then expand - if y_reflected < self.y[-2] and y_reflected > self.y[0]: + if y_reflected < y[-2] and y_reflected > y[0]: expanded = centroid + (reflected - centroid) y_expanded = self.ask_oracle(self.view(expanded.reshape(1,-1))) if y_expanded < y_reflected: @@ -89,7 +89,7 @@ def step(self): else: self.population[-1] = reflected # If the reflected point is better than the best, then expand - elif y_reflected < self.y[0]: + elif y_reflected < y[0]: expanded = centroid + 2 * (reflected - centroid) y_expanded = self.ask_oracle(self.view(expanded.reshape(1,-1))) if y_expanded < y_reflected: @@ -97,17 +97,18 @@ def step(self): else: self.population[-1] = reflected # If the reflected point is worse than the second worst, then contract - elif y_reflected > self.y[-2]: + elif y_reflected > y[-2]: contracted = centroid + 0.5 * (self.population[-1] - centroid) y_contracted = self.ask_oracle(self.view(contracted.reshape(1,-1))) - if y_contracted < self.y[-1]: + if y_contracted < y[-1]: self.population[-1] = contracted else: for i in range(1, len(self.population)): self.population[i] = 0.5 * (self.population[i] + self.population[0]) # If the reflected point is worse than the worst, then shrink - elif y_reflected > self.y[-1]: + elif y_reflected > y[-1]: for i in range(1, len(self.population)): self.population[i] = 0.5 * (self.population[i] + self.population[0]) + self.y = y.copy() return self.view_g() \ No newline at end of file From 45145138cca3da621d607a4cc0d9eaf538ddc856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 02:14:38 -0300 Subject: [PATCH 35/54] . --- pyduino/spectra.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 5c5efdc..cdd42b5 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -214,6 +214,7 @@ def log_tensor(self, i): This method iterates over the tensor values and fitness scores and logs them using the writer object. """ + print(bcolors.BOLD,"[INFO]","LOGGING",datetime.now().strftime("%c"), bcolors.ENDC) for j, params in enumerate(self.view_g()): for param_name, param in zip(self.parameters,params): self.writer.add_scalar(f"{param_name}/{j}", param, i) From 71012b1cd9752accb26cb111384f55a609568cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 02:23:37 -0300 Subject: [PATCH 36/54] removed GET --- pyduino/spectra.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index cdd42b5..dff0f5d 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -293,7 +293,6 @@ def run( self.deltaT = deltaT self.deltaTgotod = deltaTgotod self.iteration_counter = 1 - self.GET("growing") with open("error_traceback.log","w") as log_file: log_file.write(datetime_to_str(self.log.timestamp)+'\n') @@ -305,7 +304,6 @@ def run( time.sleep(max(2,deltaT)) self.dt = (datetime.now()-self.t_grow_1).total_seconds() print("[INFO]","DT",self.dt) - self.GET("growing") #Optimizer if run_optim: self.step() From d631b8043814c14c7a5e5fde7525147b4bb00e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 02:25:11 -0300 Subject: [PATCH 37/54] dot --- pyduino/spectra.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index dff0f5d..ad9a299 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -108,7 +108,8 @@ def __init__(self,ranges,density_param,maximize=True,log_name=None,reset_density self.ids = list(self.reactors.keys()) self.sorted_ids = sorted(self.ids) self.log_init(name=log_name) - self.writer = SummaryWriter(self.log.path) + self.writer = SummaryWriter(self.log.path) + print(bcolors.OKGREEN,"[INFO]", "Created tensorboard log at", self.log.path,bcolors.ENDC) self.payload = self.population_as_dict if self.payload is None else self.payload self.data = None self.do_gotod = reset_density From 26a9028a4f9d255a8d6127c817ac5e5a4be0ecdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 02:27:39 -0300 Subject: [PATCH 38/54] . --- pyduino/spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index ad9a299..fd7cfc6 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -108,8 +108,8 @@ def __init__(self,ranges,density_param,maximize=True,log_name=None,reset_density self.ids = list(self.reactors.keys()) self.sorted_ids = sorted(self.ids) self.log_init(name=log_name) - self.writer = SummaryWriter(self.log.path) - print(bcolors.OKGREEN,"[INFO]", "Created tensorboard log at", self.log.path,bcolors.ENDC) + self.writer = SummaryWriter(self.log.prefix) + print(bcolors.OKGREEN,"[INFO]", "Created tensorboard log at", self.log.prefix, bcolors.ENDC) self.payload = self.population_as_dict if self.payload is None else self.payload self.data = None self.do_gotod = reset_density From b448c8369609bfef98a6c8b7f3233507c42cd0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 02:45:53 -0300 Subject: [PATCH 39/54] Debug tensorboard --- pyduino/spectra.py | 10 +++++----- tests/test_spectra.py | 35 +---------------------------------- 2 files changed, 6 insertions(+), 39 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index fd7cfc6..aff0c1b 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -108,7 +108,7 @@ def __init__(self,ranges,density_param,maximize=True,log_name=None,reset_density self.ids = list(self.reactors.keys()) self.sorted_ids = sorted(self.ids) self.log_init(name=log_name) - self.writer = SummaryWriter(self.log.prefix) + self.writer = SummaryWriter(self.log.prefix) print(bcolors.OKGREEN,"[INFO]", "Created tensorboard log at", self.log.prefix, bcolors.ENDC) self.payload = self.population_as_dict if self.payload is None else self.payload self.data = None @@ -216,10 +216,10 @@ def log_tensor(self, i): This method iterates over the tensor values and fitness scores and logs them using the writer object. """ print(bcolors.BOLD,"[INFO]","LOGGING",datetime.now().strftime("%c"), bcolors.ENDC) - for j, params in enumerate(self.view_g()): - for param_name, param in zip(self.parameters,params): - self.writer.add_scalar(f"{param_name}/{j}", param, i) - self.writer.add_scalar(f'Fitness/{j}', self.y[j], i) + for k, v in enumerate(self.y): + self.writer.add_scalar(f'fitness/{self.ids[k]}', v, i) + for j, u in enumerate(self.parameters): + self.writer.add_scalar(f'{u}/{self.ids[k]}', self.population[k][j], i) def gotod(self): self.t_gotod_1 = datetime.now() diff --git a/tests/test_spectra.py b/tests/test_spectra.py index 272bdca..163ee89 100644 --- a/tests/test_spectra.py +++ b/tests/test_spectra.py @@ -31,37 +31,4 @@ def test_oracle(self): assert isinstance(df, pd.DataFrame) y = self.g.ask_oracle(self.g.population) def test_logger(self): - self.g.GET("test") - def test_log_tensor(self): - # Create a mock writer object - class MockWriter: - def __init__(self): - self.scalar_values = {} - - def add_scalar(self, name, value, step): - if name not in self.scalar_values: - self.scalar_values[name] = {} - self.scalar_values[name][step] = value - - # Create an instance of the Spectra class - g = Spectra(**PATHS.HYPERPARAMETERS) - g.init() - g.y = list(range(len(g.parameters))) - - # Create a mock writer object and assign it to the Spectra instance - mock_writer = MockWriter() - g.writer = mock_writer - - # Call the log_tensor method - g.log_tensor(0) - - # Assert that the scalar values were logged correctly - for j, params in enumerate(g.view_g()): - for param_name, param in zip(g.parameters, params): - expected_name = f"{param_name}/{j}" - expected_value = param - assert mock_writer.scalar_values[expected_name][0] == expected_value - - expected_fitness_name = f"Fitness/{j}" - expected_fitness_value = g.y[j] - assert mock_writer.scalar_values[expected_fitness_name][0] == expected_fitness_value \ No newline at end of file + self.g.GET("test") \ No newline at end of file From c561ca27ca9f38a57f135b211bdea716794b5543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 02:51:48 -0300 Subject: [PATCH 40/54] Debug tensorboard --- pyduino/spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index aff0c1b..35d92c4 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -217,9 +217,9 @@ def log_tensor(self, i): """ print(bcolors.BOLD,"[INFO]","LOGGING",datetime.now().strftime("%c"), bcolors.ENDC) for k, v in enumerate(self.y): - self.writer.add_scalar(f'fitness/{self.ids[k]}', v, i) + self.writer.add_scalar(f'fitness/{k}', v, i) for j, u in enumerate(self.parameters): - self.writer.add_scalar(f'{u}/{self.ids[k]}', self.population[k][j], i) + self.writer.add_scalar(f'{u}/{k}', self.population[k][j], i) def gotod(self): self.t_gotod_1 = datetime.now() From 8aa4805036aface271b84a4c16084c05a36368ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 03:06:45 -0300 Subject: [PATCH 41/54] tensorboard --- pyduino/spectra.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 35d92c4..03b4b92 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -220,6 +220,10 @@ def log_tensor(self, i): self.writer.add_scalar(f'fitness/{k}', v, i) for j, u in enumerate(self.parameters): self.writer.add_scalar(f'{u}/{k}', self.population[k][j], i) + if self.maximize: + self.writer.add_scalar('optima', max(self.y), i) + else: + self.writer.add_scalar('optima', min(self.y), i) def gotod(self): self.t_gotod_1 = datetime.now() From 43b279027ece8533322e5ca8ed599f4dc0c2ee9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Fri, 29 Mar 2024 04:23:39 -0300 Subject: [PATCH 42/54] Update logging of fitness and parameters in Spectra class --- pyduino/spectra.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 03b4b92..3151fee 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -216,10 +216,11 @@ def log_tensor(self, i): This method iterates over the tensor values and fitness scores and logs them using the writer object. """ print(bcolors.BOLD,"[INFO]","LOGGING",datetime.now().strftime("%c"), bcolors.ENDC) + P = self.view_g() for k, v in enumerate(self.y): self.writer.add_scalar(f'fitness/{k}', v, i) for j, u in enumerate(self.parameters): - self.writer.add_scalar(f'{u}/{k}', self.population[k][j], i) + self.writer.add_scalar(f'{u}/{k}', P[k][j], i) if self.maximize: self.writer.add_scalar('optima', max(self.y), i) else: From ab0b327b3dc0f9c3d638ccd2dcfa154f37510d98 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 7 Apr 2024 16:22:32 -0300 Subject: [PATCH 43/54] Add new files and update Spectra class in pyduino package --- .gitignore | 4 +++- pyduino/config.yaml | 2 +- pyduino/spectra.py | 53 +++++++++++++++++++++++++++++---------------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 17219c8..5c88834 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,6 @@ cython_debug/ cache.csv log/ -calibrate/ \ No newline at end of file +calibrate/ +experiment.py +nohup.out \ No newline at end of file diff --git a/pyduino/config.yaml b/pyduino/config.yaml index b3d6f6f..234ed37 100644 --- a/pyduino/config.yaml +++ b/pyduino/config.yaml @@ -3,7 +3,7 @@ hyperparameters: log_name: "nelder_mead_power_test" #Log folder name (delete to use the default) density_param: DensidadeAtual #Parameter name for density counts maximize: false - rng_seed: 2 #Random seed for genetic algorithm + rng_seed: 2 #Random seed parameter initialization ranges: # brilho: # - 0 diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 3151fee..a84f0e1 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -278,43 +278,58 @@ def ask_oracle(self, X) -> np.ndarray: def run( self, deltaT: float, - run_optim: bool = True, + mode: str = 'optimize', deltaTgotod: int = None - ): + ): """ - Runs reading and wiriting operations in an infinite loop on intervals given by `deltaT`. + Run the bioreactor simulation. Args: - deltaT (float): Amount of time in seconds to wait in each iteration. - run_optim (bool): Whether or not to use the optimizer. - deltaTgotod (int, optional): Time to wait after sending `gotod` command. - """ + 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 the 'gotod' operation. Defaults to None. + + Raises: + ValueError: If deltaTgotod is not an integer. + + Notes: + - If mode is 'optimize' and deltaTgotod is less than or equal to 300, a warning is raised. + - If mode is 'free', the number of rows in X must be equal to the number of reactors. - #Checking if gotod time is at least five minutes + """ + # Checking if gotod time is at least five minutes if isinstance(deltaTgotod, int): - if run_optim and deltaTgotod <= 300: warnings.warn("deltaTgotod should be at least 5 minutes.") + if mode == "optimize" and deltaTgotod <= 300: + warnings.warn("deltaTgotod should be at least 5 minutes.") else: raise ValueError("deltaTgotod must be an integer") + if mode == "free": + assert X.shape[0] == len(self.reactors), "X must have the same number of rows as reactors in free mode." + self.deltaT = deltaT self.deltaTgotod = deltaTgotod self.iteration_counter = 1 - with open("error_traceback.log","w") as log_file: - log_file.write(datetime_to_str(self.log.timestamp)+'\n') + with open("error_traceback.log", "w") as log_file: + log_file.write(datetime_to_str(self.log.timestamp) + '\n') try: print("START") while True: - #growing + # growing self.t_grow_1 = datetime.now() - time.sleep(max(2,deltaT)) - self.dt = (datetime.now()-self.t_grow_1).total_seconds() - print("[INFO]","DT",self.dt) - #Optimizer - if run_optim: + time.sleep(max(2, deltaT)) + self.dt = (datetime.now() - self.t_grow_1).total_seconds() + print("[INFO]", "DT", self.dt) + # Optimizer + if mode == "optimize": self.step() - self.gotod() - print("[INFO]","SET",datetime.now().strftime("%c")) + if isinstance(self.deltaTgotod, int): + self.gotod() + elif mode == "free": + data = self.F_get() + self.y = get_param(data, self.density_param, self.reactors) + print("[INFO]", "SET", datetime.now().strftime("%c")) self.log_tensor(self.iteration_counter) self.iteration_counter += 1 except Exception as e: From 0c97efd604cf669e788c8d9992949bcc1a2a14b1 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 7 Apr 2024 16:39:34 -0300 Subject: [PATCH 44/54] Update preset_state.d/all.csv and spectra.py --- pyduino/preset_state.d/all.csv | 14 +++++++------- pyduino/spectra.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyduino/preset_state.d/all.csv b/pyduino/preset_state.d/all.csv index 7ac6f84..3d4403e 100644 --- a/pyduino/preset_state.d/all.csv +++ b/pyduino/preset_state.d/all.csv @@ -1,8 +1,8 @@ ID brilho branco full 440 470 495 530 595 634 660 684 cor modopainel bomdia boanoite tau step modotemp temp vent pelt densidade mododil dil dildt r b ir ar ima ganho ti modoco2 co2 co2ref dtco2 dil dilDt in out -1 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 15000000 " " " " " " 40 25 1 1000 -2 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 12500000 " " " " " " 40 25 1 1000 -3 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 10000000 " " " " " " 40 25 1 1000 -8 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 10000000 " " " " " " 40 25 1 1000 -5 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 7500000 " " " " " " 40 25 1 1000 -6 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 5000000 " " " " " " 40 25 1 1000 -7 100 20 0 0 0 0 0 0 0 0 0 0 6 18 1 27 2500000 " " " " " " 40 25 1 1000 \ No newline at end of file +1 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 15000000 " " " " " " 40 25 0 1000 +2 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 12500000 " " " " " " 40 25 0 1000 +3 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 10000000 " " " " " " 40 25 0 1000 +8 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 10000000 " " " " " " 40 25 0 1000 +5 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 7500000 " " " " " " 40 25 0 1000 +6 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 5000000 " " " " " " 40 25 0 1000 +7 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 2500000 " " " " " " 40 25 0 1000 \ No newline at end of file diff --git a/pyduino/spectra.py b/pyduino/spectra.py index a84f0e1..00e2191 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -305,7 +305,7 @@ def run( raise ValueError("deltaTgotod must be an integer") if mode == "free": - assert X.shape[0] == len(self.reactors), "X must have the same number of rows as reactors in free mode." + assert self.population.shape[0] == len(self.reactors), "X must have the same number of rows as reactors in free mode." self.deltaT = deltaT self.deltaTgotod = deltaTgotod From 3104d2b1f9b832e21405e20ed86d61803c7a724f Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 7 Apr 2024 19:03:48 -0300 Subject: [PATCH 45/54] Update config.yaml and all.csv files --- pyduino/config.yaml | 45 +++------------------------------- pyduino/preset_state.d/all.csv | 14 +++++------ 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/pyduino/config.yaml b/pyduino/config.yaml index 234ed37..e8c065f 100644 --- a/pyduino/config.yaml +++ b/pyduino/config.yaml @@ -1,6 +1,6 @@ hyperparameters: reset_density: true #Whether or not to use gotod - log_name: "nelder_mead_power_test" #Log folder name (delete to use the default) + log_name: "free_growth_test_2024_04_07" #Log folder name (delete to use the default) density_param: DensidadeAtual #Parameter name for density counts maximize: false rng_seed: 2 #Random seed parameter initialization @@ -10,47 +10,10 @@ hyperparameters: # - 100 branco: - 1 - #- 100 - - 19 - full: + - 100 + others: - 0 - #- 100 - - 17 - "440": - - 0 - #- 100 - - 26 - "470": - - 0 - #- 100 - - 24 - "495": - - 0 - #- 100 - - 24 - "530": - - 0 - #- 100 - - 24 - "595": - - 0 - #- 100 - - 92 - "634": - - 0 - #- 100 - - 58 - "660": - - 0 - #- 100 - - 44 - "684": - - 0 - #- 100 - - 35 - #others: - # - 0 - # - 100 + - 100 slave: port: "5000" #Must be a string network: "192.168.1.1/24" diff --git a/pyduino/preset_state.d/all.csv b/pyduino/preset_state.d/all.csv index 3d4403e..873fd7d 100644 --- a/pyduino/preset_state.d/all.csv +++ b/pyduino/preset_state.d/all.csv @@ -1,8 +1,8 @@ ID brilho branco full 440 470 495 530 595 634 660 684 cor modopainel bomdia boanoite tau step modotemp temp vent pelt densidade mododil dil dildt r b ir ar ima ganho ti modoco2 co2 co2ref dtco2 dil dilDt in out -1 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 15000000 " " " " " " 40 25 0 1000 -2 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 12500000 " " " " " " 40 25 0 1000 -3 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 10000000 " " " " " " 40 25 0 1000 -8 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 10000000 " " " " " " 40 25 0 1000 -5 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 7500000 " " " " " " 40 25 0 1000 -6 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 5000000 " " " " " " 40 25 0 1000 -7 100 20 0 0 0 0 0 0 0 0 0 1 6 18 0 27 2500000 " " " " " " 40 25 0 1000 \ No newline at end of file +1 100 50 0 0 0 0 0 0 0 0 0 1 6 18 0 27 15000000 " " " " " " 50 25 0 1000 +2 100 50 0 0 0 0 0 0 0 0 0 1 6 18 0 27 12500000 " " " " " " 50 25 0 1000 +3 100 50 0 0 0 0 0 0 0 0 0 1 6 18 0 27 10000000 " " " " " " 50 25 0 1000 +8 100 50 0 0 0 0 0 0 0 0 0 1 6 18 0 27 10000000 " " " " " " 50 25 0 1000 +5 100 50 0 0 0 0 0 0 0 0 0 1 6 18 0 27 7500000 " " " " " " 50 25 0 1000 +6 100 50 0 0 0 0 0 0 0 0 0 1 6 18 0 27 5000000 " " " " " " 50 25 0 1000 +7 100 50 0 0 0 0 0 0 0 0 0 1 6 18 0 27 2500000 " " " " " " 50 25 0 1000 \ No newline at end of file From 84bcaa17929f8508404184e1417d8943fa8fa670 Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 7 Apr 2024 19:53:05 -0300 Subject: [PATCH 46/54] Update deltaTgotod parameter description in Spectra class --- pyduino/spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 00e2191..19df475 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -287,13 +287,13 @@ def run( Args: 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 the 'gotod' operation. Defaults to None. + deltaTgotod (int, optional): The time interval for performing optimization. Defaults to None. Raises: ValueError: If deltaTgotod is not an integer. Notes: - - If mode is 'optimize' and deltaTgotod is less than or equal to 300, a warning is raised. + - 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. """ From 81c011c94a573ae08d3b47c5c9a04dc83c36b6ac Mon Sep 17 00:00:00 2001 From: Icaro Costa Date: Sun, 7 Apr 2024 19:55:07 -0300 Subject: [PATCH 47/54] Refactor deltaTgotod parameter handling in Spectra class --- pyduino/spectra.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 19df475..e33ce97 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -289,20 +289,14 @@ def run( mode (str, optional): The mode of operation. Defaults to 'optimize'. deltaTgotod (int, optional): The time interval for performing optimization. Defaults to None. - Raises: - ValueError: If deltaTgotod is not an integer. - 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. """ # Checking if gotod time is at least five minutes - if isinstance(deltaTgotod, int): - if mode == "optimize" and deltaTgotod <= 300: - warnings.warn("deltaTgotod should be at least 5 minutes.") - else: - raise ValueError("deltaTgotod must be an integer") + if mode == "optimize" and deltaTgotod <= 300: + warnings.warn("deltaTgotod should be at least 5 minutes.") if mode == "free": assert self.population.shape[0] == len(self.reactors), "X must have the same number of rows as reactors in free mode." From 4e1c04129b45bbc4f7bc74728b9e270093e4e7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sat, 13 Apr 2024 23:55:48 -0300 Subject: [PATCH 48/54] Update logging of fitness and parameters in Spectra class --- pyduino/log.py | 18 ++++++++++++++++++ pyduino/spectra.py | 20 ++++++++++++++------ requirements.txt | 1 + 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/pyduino/log.py b/pyduino/log.py index 3ac1d5d..c3dd293 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -5,6 +5,8 @@ import io from glob import glob from uuid import uuid1 +from tabulate import tabulate +from collections import OrderedDict __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) config_file = os.path.join(__location__,"config.yaml") @@ -15,6 +17,22 @@ def datetime_from_str(x): def datetime_to_str(x): return x.strftime("%Y%m%d%H%M%S") +def to_markdown_table(data: OrderedDict[OrderedDict]) -> str: + """ + Converts the given data into a markdown table format. + + Args: + data (OrderedDict[OrderedDict]): The data to be converted into a markdown table. + + Returns: + str: The markdown table representation of the data. + """ + rows = [] + for rid, rdata in data.items(): + rdata = OrderedDict({"ID": rid, **rdata}) + rows.append(rdata) + return tabulate(rows, headers="keys", tablefmt="pipe") + class log: @property def timestamp(self): diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 3151fee..2666b75 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -209,7 +209,7 @@ def GET(self,tag): self.data = self.F_get() self.log.log_many_rows(pd.DataFrame(self.data),tags={'growth_state':tag}) - def log_tensor(self, i): + def log_data(self, i, tags={}): """ Logs the tensor values and fitness scores. @@ -217,15 +217,22 @@ def log_tensor(self, i): """ print(bcolors.BOLD,"[INFO]","LOGGING",datetime.now().strftime("%c"), bcolors.ENDC) P = self.view_g() - for k, v in enumerate(self.y): - self.writer.add_scalar(f'fitness/{k}', v, i) - for j, u in enumerate(self.parameters): - self.writer.add_scalar(f'{u}/{k}', P[k][j], i) + for rid, ry in self.y.items(): + self.writer.add_scalar(f'reactor_{rid}/y', ry, i) + for r_param_id, rparam in enumerate(self.parameters): + self.writer.add_scalar(f'reactor_{rid}/{rparam}', P[rid][r_param_id], i) if self.maximize: self.writer.add_scalar('optima', max(self.y), i) else: self.writer.add_scalar('optima', min(self.y), i) + data = self.F_get() + + # Log the DataFrame as a table in text format + self.writer.add_text(self.log.to_markdown_table(data), i) + + self.log.log_many_rows(data,tags=tags) + def gotod(self): self.t_gotod_1 = datetime.now() self.send("gotod",await_response=False) @@ -315,7 +322,8 @@ def run( self.step() self.gotod() print("[INFO]","SET",datetime.now().strftime("%c")) - self.log_tensor(self.iteration_counter) + print("[DEBUG]", "y VALUES:", self.y) + self.log_data(self.iteration_counter) self.iteration_counter += 1 except Exception as e: traceback.print_exc(file=log_file) diff --git a/requirements.txt b/requirements.txt index 0dabaa0..2f82394 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ scipy tailer python-dotenv tensorboardX==2.6.2.2 +tabulate \ No newline at end of file From 6d75c60d5568bcf499c7ba519f386e9b760bf8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 14 Apr 2024 00:05:33 -0300 Subject: [PATCH 49/54] Update logging of y-values in Spectra class --- pyduino/log.py | 3 +++ pyduino/spectra.py | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyduino/log.py b/pyduino/log.py index c3dd293..51638cd 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -33,6 +33,9 @@ def to_markdown_table(data: OrderedDict[OrderedDict]) -> str: rows.append(rdata) return tabulate(rows, headers="keys", tablefmt="pipe") +def y_to_table(y): + return tabulate(list(y.items()), tablefmt="pipe") + class log: @property def timestamp(self): diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 60dde17..c3793fe 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -11,7 +11,7 @@ from pyduino.data_parser import RangeParser from collections import OrderedDict from pyduino.utils import yaml_get, bcolors, TriangleWave, get_param -from pyduino.log import datetime_to_str +from pyduino.log import datetime_to_str, y_to_table, to_markdown_table import traceback import warnings @@ -229,7 +229,7 @@ def log_data(self, i, tags={}): data = self.F_get() # Log the DataFrame as a table in text format - self.writer.add_text(self.log.to_markdown_table(data), i) + self.writer.add_text(to_markdown_table(data), i) self.log.log_many_rows(data,tags=tags) @@ -331,7 +331,8 @@ def run( data = self.F_get() self.y = get_param(data, self.density_param, self.reactors) print("[INFO]", "SET", datetime.now().strftime("%c")) - self.log_tensor(self.iteration_counter) + print("[DEBUG]", "Y-VALUES", y_to_table(self.y)) + self.log_data(self.iteration_counter) self.iteration_counter += 1 except Exception as e: traceback.print_exc(file=log_file) From 5059e8e1f5b2460db82e6f59ab5e4d13a6a83f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 14 Apr 2024 00:12:52 -0300 Subject: [PATCH 50/54] Update logging of datetime in log.py --- pyduino/log.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pyduino/log.py b/pyduino/log.py index 51638cd..879ddac 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -7,6 +7,7 @@ from uuid import uuid1 from tabulate import tabulate from collections import OrderedDict +from datetime import datetime __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) config_file = os.path.join(__location__,"config.yaml") @@ -53,7 +54,7 @@ def __init__(self,subdir,path="./log",name=None): Example: log_obj = log(['reactor_0','reactor_1'],path='./log',name='experiment_0') - log/ + log/YEAR/MONTH/ ├─ experiment_0/ │ ├─ reactor_0.csv │ ├─ reactor_1.csv @@ -63,7 +64,8 @@ def __init__(self,subdir,path="./log",name=None): path (str): Save path for the logs. name (str): Name given for this particular instance. If none will name it with the current timestamp. """ - self.path = path + self.today = datetime.now() + self.path = os.path.join(path, self.today.strftime("%Y"), self.today.strftime("%m")) self.start_timestamp = datetime_to_str(self.timestamp) if name is None else name self.log_name = name Path(os.path.join(self.path,self.start_timestamp)).mkdir(parents=True,exist_ok=True) @@ -81,6 +83,22 @@ def __init__(self,subdir,path="./log",name=None): with open(config_file) as cfile, open(os.path.join(self.path,self.start_timestamp,f"{self.start_timestamp.replace('/','-')}-{str(uuid1())}.yaml"),'w') as wfile: wfile.write(cfile.read()) + self.log_name = name + Path(os.path.join(self.path,self.start_timestamp)).mkdir(parents=True,exist_ok=True) + if isinstance(subdir,str): + self.subdir = list(map(os.path.basename,glob(os.path.join(self.prefix,subdir)))) + elif isinstance(subdir,list): + self.subdir = subdir + else: + raise ValueError("Invalid type for subdir. Must be either a list of strings or a glob string.") + self.subdir = list(map(lambda x: str(x)+".csv" if len(os.path.splitext(str(x))[1])==0 else str(x),self.subdir)) + self.first_timestamp = None + self.data_frames = {} + + self.paths = list(map(lambda x: os.path.join(self.prefix,x),self.subdir)) + + with open(config_file) as cfile, open(os.path.join(self.path,self.start_timestamp,f"{self.start_timestamp.replace('/','-')}-{str(uuid1())}.yaml"),'w') as wfile: + wfile.write(cfile.read()) def log_rows(self,rows,subdir,add_timestamp=True,tags=None,**kwargs): From e4d85e43b4c1f0422887a4deeb679c054cb99d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 14 Apr 2024 00:14:24 -0300 Subject: [PATCH 51/54] Update to_markdown_table function signature in log.py --- pyduino/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyduino/log.py b/pyduino/log.py index 879ddac..9a509d5 100644 --- a/pyduino/log.py +++ b/pyduino/log.py @@ -18,7 +18,7 @@ def datetime_from_str(x): def datetime_to_str(x): return x.strftime("%Y%m%d%H%M%S") -def to_markdown_table(data: OrderedDict[OrderedDict]) -> str: +def to_markdown_table(data: OrderedDict) -> str: """ Converts the given data into a markdown table format. From 6650f5ac3e64f13b309ff9855c234337ed3b6b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 14 Apr 2024 00:21:33 -0300 Subject: [PATCH 52/54] Update logging of y-values and parameters in Spectra class --- pyduino/spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index c3793fe..49e7ab1 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -218,9 +218,9 @@ def log_data(self, i, tags={}): print(bcolors.BOLD,"[INFO]","LOGGING",datetime.now().strftime("%c"), bcolors.ENDC) P = self.view_g() for rid, ry in self.y.items(): - self.writer.add_scalar(f'reactor_{rid}/y', ry, i) + self.writer.add_scalar(f'reactor_{rid}/y', float(ry), i) for r_param_id, rparam in enumerate(self.parameters): - self.writer.add_scalar(f'reactor_{rid}/{rparam}', P[rid][r_param_id], i) + self.writer.add_scalar(f'reactor_{rid}/{rparam}', float(P[rid][r_param_id]), i) if self.maximize: self.writer.add_scalar('optima', max(self.y), i) else: From 9df610c7cb61a8f7d1d38304e2c591eb3ca1c524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 14 Apr 2024 00:24:26 -0300 Subject: [PATCH 53/54] debug log --- pyduino/spectra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index 49e7ab1..a8d2922 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -217,10 +217,10 @@ def log_data(self, i, tags={}): """ print(bcolors.BOLD,"[INFO]","LOGGING",datetime.now().strftime("%c"), bcolors.ENDC) P = self.view_g() - for rid, ry in self.y.items(): + for k,(rid, ry) in enumerate(self.y.items()): self.writer.add_scalar(f'reactor_{rid}/y', float(ry), i) for r_param_id, rparam in enumerate(self.parameters): - self.writer.add_scalar(f'reactor_{rid}/{rparam}', float(P[rid][r_param_id]), i) + self.writer.add_scalar(f'reactor_{rid}/{rparam}', float(P[k][r_param_id]), i) if self.maximize: self.writer.add_scalar('optima', max(self.y), i) else: From 39ce2df0e547e1ded36e080a13f5d65469624e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro?= Date: Sun, 14 Apr 2024 00:28:56 -0300 Subject: [PATCH 54/54] Update logging of DataFrame as a table in Spectra class --- pyduino/spectra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyduino/spectra.py b/pyduino/spectra.py index a8d2922..94466f8 100644 --- a/pyduino/spectra.py +++ b/pyduino/spectra.py @@ -229,7 +229,7 @@ def log_data(self, i, tags={}): data = self.F_get() # Log the DataFrame as a table in text format - self.writer.add_text(to_markdown_table(data), i) + self.writer.add_text("reactor_state", text_string=to_markdown_table(data), global_step=i) self.log.log_many_rows(data,tags=tags)