From eb9590358b72cdef88ad0d8726e623318954c98f Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:46:52 +0200 Subject: [PATCH 01/26] chore: ruff format --- .../lateral_dynamics_control.ipynb | 303 +- .../lateral_dynamics_model.ipynb | 214 +- .../lateral_dynamics_model_torch.ipynb | 307 +- .../mass_spring_damper_nnodely.ipynb | 137 +- .../comparison/mass_spring_damper_torch.ipynb | 175 +- .../comparison/saved/msd_final_with_PID.py | 242 +- .../mass_spring_damper/mass_spring_damper.py | 123 +- case-studies/neuralODE/neuralODE_msd.ipynb | 449 +- case-studies/neuralODE/saved/neuralODE_msd.py | 178 +- case-studies/pinn/pinn_Burgers_equation.ipynb | 567 +-- .../_autodoc/tutorials/examples/dataset.ipynb | 57 +- .../tutorials/examples/equation_learner.ipynb | 117 +- docs/_autodoc/tutorials/examples/export.ipynb | 190 +- docs/_autodoc/tutorials/examples/fir.ipynb | 45 +- .../_autodoc/tutorials/examples/fuzzify.ipynb | 62 +- .../tutorials/examples/inference.ipynb | 63 +- .../tutorials/examples/interpolation.ipynb | 16 +- docs/_autodoc/tutorials/examples/linear.ipynb | 22 +- .../tutorials/examples/localmodel.ipynb | 110 +- .../tutorials/examples/parameter.ipynb | 38 +- .../examples/parametric_functions.ipynb | 70 +- .../tutorials/examples/partitioning.ipynb | 62 +- docs/_autodoc/tutorials/examples/states.ipynb | 153 +- .../tutorials/examples/training.ipynb | 127 +- docs/conf.py | 33 +- mplplots/__init__.py | 8 +- mplplots/plots.py | 232 +- nnodely/__init__.py | 114 +- nnodely/basic/loss.py | 31 +- nnodely/basic/model.py | 252 +- nnodely/basic/modeldef.py | 335 +- nnodely/basic/optimizer.py | 66 +- nnodely/basic/relation.py | 234 +- nnodely/exporter/emptyexporter.py | 24 +- nnodely/exporter/export.py | 422 +- nnodely/exporter/reporter.py | 54 +- nnodely/exporter/standardexporter.py | 170 +- nnodely/layers/activation.py | 164 +- nnodely/layers/arithmetic.py | 323 +- nnodely/layers/equationlearner.py | 95 +- nnodely/layers/fir.py | 155 +- nnodely/layers/fuzzify.py | 209 +- nnodely/layers/input.py | 208 +- nnodely/layers/interpolation.py | 80 +- nnodely/layers/linear.py | 144 +- nnodely/layers/localmodel.py | 54 +- nnodely/layers/neuralODE.py | 88 +- nnodely/layers/output.py | 15 +- nnodely/layers/parameter.py | 161 +- nnodely/layers/parametricfunction.py | 294 +- nnodely/layers/part.py | 393 +- nnodely/layers/rungekutta.py | 48 +- nnodely/layers/timeoperation.py | 74 +- nnodely/layers/trigonometric.py | 180 +- nnodely/nnodely.py | 148 +- nnodely/operators/composer.py | 292 +- nnodely/operators/exporter.py | 183 +- nnodely/operators/loader.py | 179 +- nnodely/operators/network.py | 570 ++- nnodely/operators/trainer.py | 419 +- nnodely/operators/validator.py | 308 +- nnodely/support/earlystopping.py | 44 +- nnodely/support/fixstepsolver.py | 39 +- nnodely/support/initializer.py | 81 +- nnodely/support/jsonutils.py | 649 ++- nnodely/support/logger.py | 51 +- nnodely/support/mathutils.py | 11 +- nnodely/support/odeint/adjoint.py | 204 +- nnodely/support/odeint/dopri5.py | 65 +- nnodely/support/odeint/fixed_grid.py | 2 +- nnodely/support/odeint/my_odeint.py | 35 +- nnodely/support/odeint/rk_solvers.py | 219 +- nnodely/support/odeint/solvers.py | 71 +- nnodely/support/odeint/utils.py | 88 +- nnodely/support/utils.py | 66 +- nnodely/visualizer/__init__.py | 2 +- nnodely/visualizer/dynamicmpl/functionplot.py | 19 +- nnodely/visualizer/dynamicmpl/fuzzyplot.py | 12 +- nnodely/visualizer/dynamicmpl/resultsplot.py | 15 +- nnodely/visualizer/dynamicmpl/trainingplot.py | 17 +- nnodely/visualizer/emptyvisualizer.py | 12 +- nnodely/visualizer/mplnotebookvisualizer.py | 97 +- nnodely/visualizer/mplvisualizer.py | 221 +- nnodely/visualizer/textvisualizer.py | 457 +- setup.py | 14 +- tests/__init__.py | 3 +- tests/test_dataset.py | 3043 ++++++++++--- tests/test_documentation.py | 23 +- tests/test_export.py | 369 +- tests/test_export_recurrent.py | 1522 ++++--- tests/test_input_dimensions.py | 459 +- tests/test_json.py | 1181 +++-- tests/test_losses.py | 548 ++- tests/test_model_predict.py | 3446 +++++++++----- tests/test_model_predict_recurrent.py | 2462 ++++++---- tests/test_network_element.py | 827 ++-- tests/test_parameters_of_train.py | 2159 ++++++--- tests/test_results.py | 872 ++-- tests/test_train.py | 558 ++- tests/test_train_recurrent.py | 4042 ++++++++++++----- tests/test_utils.py | 17 +- tests/test_visualizer.py | 270 +- 102 files changed, 23568 insertions(+), 11310 deletions(-) diff --git a/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb index ef3ffe6f..b29fff17 100644 --- a/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb +++ b/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb @@ -57,20 +57,22 @@ "\n", "# import a library for plots\n", "import matplotlib as mpl\n", + "\n", "mpl.rcParams.update(mpl.rcParamsDefault)\n", "import matplotlib.pyplot as plt\n", - "plt.close('all')\n", + "\n", + "plt.close(\"all\")\n", "SMALL_SIZE = 14\n", "MEDIUM_SIZE = 22\n", "BIGGER_SIZE = 26\n", - "plt.rc('font', size=SMALL_SIZE) # controls default text sizes\n", - "plt.rc('axes', titlesize=MEDIUM_SIZE) # fontsize of the axes title\n", - "plt.rc('axes', labelsize=SMALL_SIZE) # fontsize of the x and y labels\n", - "plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", - "plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", - "plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize\n", - "plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title\n", - "plt.rc('grid', linestyle=\"--\", color='grey')" + "plt.rc(\"font\", size=SMALL_SIZE) # controls default text sizes\n", + "plt.rc(\"axes\", titlesize=MEDIUM_SIZE) # fontsize of the axes title\n", + "plt.rc(\"axes\", labelsize=SMALL_SIZE) # fontsize of the x and y labels\n", + "plt.rc(\"xtick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n", + "plt.rc(\"ytick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n", + "plt.rc(\"legend\", fontsize=SMALL_SIZE) # legend fontsize\n", + "plt.rc(\"figure\", titlesize=BIGGER_SIZE) # fontsize of the figure title\n", + "plt.rc(\"grid\", linestyle=\"--\", color=\"grey\")" ] }, { @@ -81,8 +83,10 @@ "outputs": [], "source": [ "# Configurations\n", - "path_folder = 'trained_models' # folder to save the model\n", - "lat_dyna_control = nnodely(visualizer=MPLNotebookVisualizer(),seed=1,workspace=path_folder,log_internal=True)" + "path_folder = \"trained_models\" # folder to save the model\n", + "lat_dyna_control = nnodely(\n", + " visualizer=MPLNotebookVisualizer(), seed=1, workspace=path_folder, log_internal=True\n", + ")" ] }, { @@ -594,7 +598,7 @@ } ], "source": [ - "lat_dyna_control.loadModel('vehicle_model')\n", + "lat_dyna_control.loadModel(\"vehicle_model\")\n", "lat_dyna_control.neuralizeModel()" ] }, @@ -621,76 +625,121 @@ "# ----------------------------------------------------------------\n", "# Inputs\n", "# ----------------------------------------------------------------\n", - "curv_in = Input('controller_curv_in') # [1/m] path curvature\n", - "vx_in = Input('controller_vx_in') # [m/s] longitudinal velocity\n", - "steer_in = Input('controller_steer_in') # [rad] steering wheel angle\n", - "ax_in = Input('controller_ax_in') # [m/s^2] longitudinal acceleration\n", + "curv_in = Input(\"controller_curv_in\") # [1/m] path curvature\n", + "vx_in = Input(\"controller_vx_in\") # [m/s] longitudinal velocity\n", + "steer_in = Input(\"controller_steer_in\") # [rad] steering wheel angle\n", + "ax_in = Input(\"controller_ax_in\") # [m/s^2] longitudinal acceleration\n", "\n", "# ----------------------------------------------------------------\n", "# Hyperparameters\n", "# ----------------------------------------------------------------\n", - "samples_past_steer = 30 # number of samples in the past for the steering wheel angle prediction\n", + "samples_past_steer = (\n", + " 30 # number of samples in the past for the steering wheel angle prediction\n", + ")\n", "samples_prediction = 30 # number of samples for future manoeuvre\n", - "n_channels_vx = 8 # number of channels for activation function vx\n", - "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n", - "chan_ax = [-2,-1,0.0,1.0,2.0] # centers of the channels ax\n", + "n_channels_vx = 8 # number of channels for activation function vx\n", + "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n", + "chan_ax = [-2, -1, 0.0, 1.0, 2.0] # centers of the channels ax\n", "\n", "# Exponential weights for normalisation of FIR parameters during training\n", - "W_prev = np.flip(np.array([[np.exp(-(i/(samples_prediction/2))**2)] for i in range(-samples_prediction+1,1)]))\n", - "W_past = np.array([[np.exp(-(i/(samples_past_steer/2))**2)] for i in range(-samples_past_steer+2,1)])\n", + "W_prev = np.flip(\n", + " np.array(\n", + " [\n", + " [np.exp(-((i / (samples_prediction / 2)) ** 2))]\n", + " for i in range(-samples_prediction + 1, 1)\n", + " ]\n", + " )\n", + ")\n", + "W_past = np.array(\n", + " [\n", + " [np.exp(-((i / (samples_past_steer / 2)) ** 2))]\n", + " for i in range(-samples_past_steer + 2, 1)\n", + " ]\n", + ")\n", + "\n", + "W_fir_init = Constant(\"W_fir_init\", sw=(samples_past_steer - 1), values=W_past)\n", + "W_fir_target = Constant(\"W_fir_target\", sw=samples_prediction, values=W_prev)\n", "\n", - "W_fir_init = Constant('W_fir_init', sw = (samples_past_steer-1), values=W_past)\n", - "W_fir_target = Constant('W_fir_target', sw =samples_prediction, values=W_prev)\n", "\n", "# ----------------------------------------------------------------\n", "# Understeer correction function\n", "# ----------------------------------------------------------------\n", - "def understeer_corr_local_control(vx,curv, # inputs\n", - " A, # constant\n", - " ):\n", + "def understeer_corr_local_control(\n", + " vx,\n", + " curv, # inputs\n", + " A, # constant\n", + "):\n", " return curv * (1 + A * torch.pow(vx, 2))\n", "\n", + "\n", "understeer_corr = ParamFun(understeer_corr_local_control)\n", "\n", "# ----------------------------------------------------------------\n", "# Local models\n", "# ----------------------------------------------------------------\n", "# Activation functions\n", - "activ_fnc_ax = Fuzzify(centers=chan_ax, functions='Triangular')(ax_in.last())\n", - "activ_fnc_vx = Fuzzify(centers=chan_vx, functions='Triangular')(vx_in.last())\n", + "activ_fnc_ax = Fuzzify(centers=chan_ax, functions=\"Triangular\")(ax_in.last())\n", + "activ_fnc_vx = Fuzzify(centers=chan_vx, functions=\"Triangular\")(vx_in.last())\n", "\n", "# Understeer coefficient: the value of the constant is taken by the model and is function of the acceleration ax\n", - "A = Constant('A',values=[lat_dyna_control.parameters['A_0'][0][0],lat_dyna_control.parameters['A_1'][0][0],lat_dyna_control.parameters['A_2'][0][0],lat_dyna_control.parameters['A_3'][0][0],lat_dyna_control.parameters['A_4'][0][0]])\n", - "A_ax = Sum( activ_fnc_ax * A ) # Constant A function of ax\n", + "A = Constant(\n", + " \"A\",\n", + " values=[\n", + " lat_dyna_control.parameters[\"A_0\"][0][0],\n", + " lat_dyna_control.parameters[\"A_1\"][0][0],\n", + " lat_dyna_control.parameters[\"A_2\"][0][0],\n", + " lat_dyna_control.parameters[\"A_3\"][0][0],\n", + " lat_dyna_control.parameters[\"A_4\"][0][0],\n", + " ],\n", + ")\n", + "A_ax = Sum(activ_fnc_ax * A) # Constant A function of ax\n", "\n", "# FIR target trajectory\n", - "local_model_target = LocalModel(pass_indexes=True,\n", - " input_function=lambda idx: Fir(output_dimension=1,W_init = 'init_constant',W_init_params={\"value\": 0.01}, W=f'Fir_target_{idx[0]}'))\n", + "local_model_target = LocalModel(\n", + " pass_indexes=True,\n", + " input_function=lambda idx: Fir(\n", + " output_dimension=1,\n", + " W_init=\"init_constant\",\n", + " W_init_params={\"value\": 0.01},\n", + " W=f\"Fir_target_{idx[0]}\",\n", + " ),\n", + ")\n", "# FIR initial condition\n", - "delta_ic = Fir(output_dimension=1, W=\"Fir_InitCondition\", W_init=\"init_constant\", W_init_params={\"value\": 0.01})(steer_in.sw([-samples_past_steer,-1])*W_fir_init)\n", + "delta_ic = Fir(\n", + " output_dimension=1,\n", + " W=\"Fir_InitCondition\",\n", + " W_init=\"init_constant\",\n", + " W_init_params={\"value\": 0.01},\n", + ")(steer_in.sw([-samples_past_steer, -1]) * W_fir_init)\n", "\n", "# Understeer correction\n", - "out = understeer_corr( vx_in.sw([-1,(samples_prediction-1)]),curv_in.sw([-1,(samples_prediction-1)]), A_ax )\n", + "out = understeer_corr(\n", + " vx_in.sw([-1, (samples_prediction - 1)]),\n", + " curv_in.sw([-1, (samples_prediction - 1)]),\n", + " A_ax,\n", + ")\n", "\n", "# Local model\n", - "delta_target = local_model_target(out*W_fir_target , activ_fnc_vx)\n", + "delta_target = local_model_target(out * W_fir_target, activ_fnc_vx)\n", "\n", "# NN output\n", - "delta = delta_target + delta_ic\n", + "delta = delta_target + delta_ic\n", "\n", "# ----------------------------------------------------------------\n", "# Outputs\n", "# ----------------------------------------------------------------\n", - "steer_from_target = Output('controller_steer_from_target',delta_target)\n", - "steer_from_ic = Output('controller_steer_from_ic',delta_ic)\n", - "steer_control = Output('controller_steer_out',delta)\n", + "steer_from_target = Output(\"controller_steer_from_target\", delta_target)\n", + "steer_from_ic = Output(\"controller_steer_from_ic\", delta_ic)\n", + "steer_control = Output(\"controller_steer_out\", delta)\n", "\n", "# ----------------------------------------------------------------\n", "# Add controller model and connect to the vehicle model\n", "# ----------------------------------------------------------------\n", - "lat_dyna_control.addModel('control_steer',[steer_from_target,steer_from_ic,steer_control])\n", - "lat_dyna_control.addClosedLoop(steer_control,steer_in)\n", - "lat_dyna_control.addConnect(steer_control,'model_steer_in')" + "lat_dyna_control.addModel(\n", + " \"control_steer\", [steer_from_target, steer_from_ic, steer_control]\n", + ")\n", + "lat_dyna_control.addClosedLoop(steer_control, steer_in)\n", + "lat_dyna_control.addConnect(steer_control, \"model_steer_in\")" ] }, { @@ -1847,7 +1896,9 @@ ], "source": [ "# Generate the model\n", - "lat_dyna_control.neuralizeModel(sample_time=0.05) # neuralize the model with the chosen sample time" + "lat_dyna_control.neuralizeModel(\n", + " sample_time=0.05\n", + ") # neuralize the model with the chosen sample time" ] }, { @@ -1922,15 +1973,48 @@ } ], "source": [ - "lat_dyna_control.loadData('test_set', source='dataset/test',\n", - " format=['', ('controller_ax_in','model_ax_in'),'', ('controller_vx_in','model_vx_in'), ('model_curv_in','controller_curv_in'), ('model_steer_in','controller_steer_in')], skiplines=1)\n", + "lat_dyna_control.loadData(\n", + " \"test_set\",\n", + " source=\"dataset/test\",\n", + " format=[\n", + " \"\",\n", + " (\"controller_ax_in\", \"model_ax_in\"),\n", + " \"\",\n", + " (\"controller_vx_in\", \"model_vx_in\"),\n", + " (\"model_curv_in\", \"controller_curv_in\"),\n", + " (\"model_steer_in\", \"controller_steer_in\"),\n", + " ],\n", + " skiplines=1,\n", + ")\n", "\n", - "lat_dyna_control.loadData('training_set', source='dataset/training',\n", - " format=['', ('model_ax_in','controller_ax_in'), '', ('controller_vx_in','model_vx_in'), ('model_curv_in','controller_curv_in'), 'model_steer_in', ''],\n", - " skiplines=1)\n", - "lat_dyna_control.loadData('validation_set', source='dataset/validation',\n", - " format=['', ('model_ax_in','controller_ax_in'), '', ('controller_vx_in','model_vx_in'), ('model_curv_in','controller_curv_in'), 'model_steer_in', ''],\n", - " skiplines=1)" + "lat_dyna_control.loadData(\n", + " \"training_set\",\n", + " source=\"dataset/training\",\n", + " format=[\n", + " \"\",\n", + " (\"model_ax_in\", \"controller_ax_in\"),\n", + " \"\",\n", + " (\"controller_vx_in\", \"model_vx_in\"),\n", + " (\"model_curv_in\", \"controller_curv_in\"),\n", + " \"model_steer_in\",\n", + " \"\",\n", + " ],\n", + " skiplines=1,\n", + ")\n", + "lat_dyna_control.loadData(\n", + " \"validation_set\",\n", + " source=\"dataset/validation\",\n", + " format=[\n", + " \"\",\n", + " (\"model_ax_in\", \"controller_ax_in\"),\n", + " \"\",\n", + " (\"controller_vx_in\", \"model_vx_in\"),\n", + " (\"model_curv_in\", \"controller_curv_in\"),\n", + " \"model_steer_in\",\n", + " \"\",\n", + " ],\n", + " skiplines=1,\n", + ")" ] }, { @@ -1954,18 +2038,20 @@ "outputs": [], "source": [ "# Default training parameters definition\n", - "training_pars = {'num_of_epochs': 5,\n", - " 'val_batch_size': 64,\n", - " 'train_batch_size': 16,\n", - " 'lr': 1e-3,\n", - " 'train_dataset': 'training_set',\n", - " 'validation_dataset': 'validation_set',\n", - " 'models': 'control_steer',\n", - " 'optimizer': 'Adam',\n", - " 'shuffle_data': True,\n", - " 'select_model': select_best_model,\n", - " 'early_stopping': earlystopping.early_stop_patience,\n", - " 'early_stopping_params': {'patience': 3, 'error': 'heading_error'}}" + "training_pars = {\n", + " \"num_of_epochs\": 5,\n", + " \"val_batch_size\": 64,\n", + " \"train_batch_size\": 16,\n", + " \"lr\": 1e-3,\n", + " \"train_dataset\": \"training_set\",\n", + " \"validation_dataset\": \"validation_set\",\n", + " \"models\": \"control_steer\",\n", + " \"optimizer\": \"Adam\",\n", + " \"shuffle_data\": True,\n", + " \"select_model\": select_best_model,\n", + " \"early_stopping\": earlystopping.early_stop_patience,\n", + " \"early_stopping_params\": {\"patience\": 3, \"error\": \"heading_error\"},\n", + "}" ] }, { @@ -2080,8 +2166,7 @@ ], "source": [ "# First training without few samples in the future\n", - "lat_dyna_control.trainModel(training_params=training_pars,\n", - " prediction_samples=5)" + "lat_dyna_control.trainModel(training_params=training_pars, prediction_samples=5)" ] }, { @@ -2201,10 +2286,12 @@ ], "source": [ "# Training with integral error 4s in the future\n", - "lat_dyna_control.trainModel(training_params=training_pars,\n", - " num_of_epochs=10,\n", - " prediction_samples=80, # 4s in the future\n", - " step=10)" + "lat_dyna_control.trainModel(\n", + " training_params=training_pars,\n", + " num_of_epochs=10,\n", + " prediction_samples=80, # 4s in the future\n", + " step=10,\n", + ")" ] }, { @@ -2264,19 +2351,19 @@ "plt.figure()\n", "\n", "for key, value in lat_dyna_control.parameters.items():\n", - " if key.startswith('Fir_target'):\n", + " if key.startswith(\"Fir_target\"):\n", " params_fir_target[key] = lat_dyna_control.parameters[key]\n", "\n", - " if key.startswith('Fir_InitCondition'):\n", + " if key.startswith(\"Fir_InitCondition\"):\n", " params_fir_ic[key] = lat_dyna_control.parameters[key]\n", "\n", "for key, value in params_fir_target.items():\n", " vals = np.array(value).flatten()\n", " t = np.arange(0, sampling_time * len(vals), sampling_time)\n", - " plt.plot(t,vals * W_prev.flatten(), 'o', label=key)\n", + " plt.plot(t, vals * W_prev.flatten(), \"o\", label=key)\n", "\n", - "plt.xlabel('time (s)')\n", - "plt.ylabel('amplitude [m-1 rad-1]')\n", + "plt.xlabel(\"time (s)\")\n", + "plt.ylabel(\"amplitude [m-1 rad-1]\")\n", "plt.legend()\n", "plt.grid(True)\n", "plt.show(block=False)\n", @@ -2284,10 +2371,10 @@ "plt.figure()\n", "for key, value in params_fir_ic.items():\n", " vals = np.array(value).flatten()\n", - " t = np.arange(sampling_time, sampling_time * (len(vals)+1), sampling_time)\n", - " plt.plot(t,np.flip(vals * W_past.flatten()),'o',label=key)\n", - "plt.xlabel('time (s)')\n", - "plt.ylabel('amplitude [-]')\n", + " t = np.arange(sampling_time, sampling_time * (len(vals) + 1), sampling_time)\n", + " plt.plot(t, np.flip(vals * W_past.flatten()), \"o\", label=key)\n", + "plt.xlabel(\"time (s)\")\n", + "plt.ylabel(\"amplitude [-]\")\n", "plt.legend()\n", "plt.grid(True)\n", "plt.show(block=False)" @@ -2349,7 +2436,7 @@ } ], "source": [ - "lat_dyna_control.analyzeModel('test_set',prediction_samples=200) # 10s prediction" + "lat_dyna_control.analyzeModel(\"test_set\", prediction_samples=200) # 10s prediction" ] }, { @@ -2403,32 +2490,34 @@ "data = pd.read_csv(\"dataset/test/test_set.csv\")\n", "\n", "sample_test_set = {\n", - " 'controller_vx_in' : np.array(data['vx']),\n", - " 'model_vx_in' : np.array(data['vx']),\n", - " 'controller_curv_in' : np.array(data['curv']),\n", - " 'model_curv_in' : np.array(data['curv']),\n", - " 'controller_steer_in' : np.array(data['steer']),\n", - " 'model_steer_in': np.array(data['steer']),\n", - " 'model_ax_in' : np.array(data['ax']),\n", - " 'controller_ax_in': np.array(data['ax'])\n", + " \"controller_vx_in\": np.array(data[\"vx\"]),\n", + " \"model_vx_in\": np.array(data[\"vx\"]),\n", + " \"controller_curv_in\": np.array(data[\"curv\"]),\n", + " \"model_curv_in\": np.array(data[\"curv\"]),\n", + " \"controller_steer_in\": np.array(data[\"steer\"]),\n", + " \"model_steer_in\": np.array(data[\"steer\"]),\n", + " \"model_ax_in\": np.array(data[\"ax\"]),\n", + " \"controller_ax_in\": np.array(data[\"ax\"]),\n", "}\n", "\n", - "out_nn_test_set = lat_dyna_control(sample_test_set, sampled=False, prediction_samples=1100)\n", + "out_nn_test_set = lat_dyna_control(\n", + " sample_test_set, sampled=False, prediction_samples=1100\n", + ")\n", "\n", "# plot the results\n", "plt.figure()\n", - "plt.plot(data['steer'],label='telem')\n", - "plt.plot(out_nn_test_set['controller_steer_out'],label='control')\n", - "plt.xlabel('samples')\n", - "plt.ylabel('steer (rad)')\n", + "plt.plot(data[\"steer\"], label=\"telem\")\n", + "plt.plot(out_nn_test_set[\"controller_steer_out\"], label=\"control\")\n", + "plt.xlabel(\"samples\")\n", + "plt.ylabel(\"steer (rad)\")\n", "plt.legend()\n", "plt.grid()\n", "\n", "plt.figure()\n", - "plt.plot(data['curv'],label='telem')\n", - "plt.plot(out_nn_test_set['model_curv'],label='control')\n", - "plt.xlabel('samples')\n", - "plt.ylabel('curvature (rad)')\n", + "plt.plot(data[\"curv\"], label=\"telem\")\n", + "plt.plot(out_nn_test_set[\"model_curv\"], label=\"control\")\n", + "plt.xlabel(\"samples\")\n", + "plt.ylabel(\"curvature (rad)\")\n", "plt.legend()\n", "plt.grid()" ] @@ -3550,14 +3639,28 @@ ], "source": [ "# Save json model\n", - "lat_dyna_control.saveModel('control_model')\n", + "lat_dyna_control.saveModel(\"control_model\")\n", "\n", "# Remove Minimize\n", - "lat_dyna_control.removeMinimize(['heading_error','curv_error'])\n", + "lat_dyna_control.removeMinimize([\"heading_error\", \"curv_error\"])\n", "lat_dyna_control.neuralizeModel()\n", "\n", "# Export ONNX\n", - "lat_dyna_control.exportONNX(['controller_curv_in', 'controller_vx_in','controller_ax_in','controller_steer_in'],['controller_steer_out','controller_steer_from_ic','controller_steer_from_target'],'controller',models='control_steer')\n" + "lat_dyna_control.exportONNX(\n", + " [\n", + " \"controller_curv_in\",\n", + " \"controller_vx_in\",\n", + " \"controller_ax_in\",\n", + " \"controller_steer_in\",\n", + " ],\n", + " [\n", + " \"controller_steer_out\",\n", + " \"controller_steer_from_ic\",\n", + " \"controller_steer_from_target\",\n", + " ],\n", + " \"controller\",\n", + " models=\"control_steer\",\n", + ")" ] } ], diff --git a/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb index c8c9491c..daa5f142 100644 --- a/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb +++ b/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb @@ -58,20 +58,22 @@ "\n", "# import a library for plots\n", "import matplotlib as mpl\n", + "\n", "mpl.rcParams.update(mpl.rcParamsDefault)\n", "import matplotlib.pyplot as plt\n", - "plt.close('all')\n", + "\n", + "plt.close(\"all\")\n", "SMALL_SIZE = 14\n", "MEDIUM_SIZE = 22\n", "BIGGER_SIZE = 26\n", - "plt.rc('font', size=SMALL_SIZE) # controls default text sizes\n", - "plt.rc('axes', titlesize=MEDIUM_SIZE) # fontsize of the axes title\n", - "plt.rc('axes', labelsize=SMALL_SIZE) # fontsize of the x and y labels\n", - "plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", - "plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", - "plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize\n", - "plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title\n", - "plt.rc('grid', linestyle=\"--\", color='grey')" + "plt.rc(\"font\", size=SMALL_SIZE) # controls default text sizes\n", + "plt.rc(\"axes\", titlesize=MEDIUM_SIZE) # fontsize of the axes title\n", + "plt.rc(\"axes\", labelsize=SMALL_SIZE) # fontsize of the x and y labels\n", + "plt.rc(\"xtick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n", + "plt.rc(\"ytick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n", + "plt.rc(\"legend\", fontsize=SMALL_SIZE) # legend fontsize\n", + "plt.rc(\"figure\", titlesize=BIGGER_SIZE) # fontsize of the figure title\n", + "plt.rc(\"grid\", linestyle=\"--\", color=\"grey\")" ] }, { @@ -87,8 +89,10 @@ "outputs": [], "source": [ "# Configurations\n", - "path_folder = 'trained_models' # folder to save the model\n", - "lat_dyna_model = nnodely(visualizer=MPLNotebookVisualizer(),seed=1,workspace=path_folder,log_internal=True)" + "path_folder = \"trained_models\" # folder to save the model\n", + "lat_dyna_model = nnodely(\n", + " visualizer=MPLNotebookVisualizer(), seed=1, workspace=path_folder, log_internal=True\n", + ")" ] }, { @@ -132,58 +136,77 @@ "# ----------------------------------------------------------------\n", "# Inputs\n", "# ----------------------------------------------------------------\n", - "vx_in = Input('model_vx_in') # [m/s] longitudinal velocity\n", - "steer_in = Input('model_steer_in') # [rad] steering wheel angle\n", - "ax_in = Input('model_ax_in') # [m/s^2] lateral acceleration\n", + "vx_in = Input(\"model_vx_in\") # [m/s] longitudinal velocity\n", + "steer_in = Input(\"model_steer_in\") # [rad] steering wheel angle\n", + "ax_in = Input(\"model_ax_in\") # [m/s^2] lateral acceleration\n", "\n", "# ----------------------------------------------------------------\n", "# Hyperparameters\n", "# ----------------------------------------------------------------\n", - "samples_past_steer = 30 # number of samples in the past for the steering wheel angle prediction\n", - "n_channels_vx = 8 # number of channels for activation function vx\n", - "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n", - "chan_ax = [-2,-1,0.0,1.0,2.0] # centers of the channels ax\n", + "samples_past_steer = (\n", + " 30 # number of samples in the past for the steering wheel angle prediction\n", + ")\n", + "n_channels_vx = 8 # number of channels for activation function vx\n", + "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n", + "chan_ax = [-2, -1, 0.0, 1.0, 2.0] # centers of the channels ax\n", "\n", "# Exponential weights for normalisation of FIR parameters during training\n", - "W_fir = np.array([[np.exp(-(i/(samples_past_steer/2))**2)] for i in range(-samples_past_steer+1,1)])\n", - "W_constant = Constant('W_fir',sw=30, values=W_fir)\n", + "W_fir = np.array(\n", + " [\n", + " [np.exp(-((i / (samples_past_steer / 2)) ** 2))]\n", + " for i in range(-samples_past_steer + 1, 1)\n", + " ]\n", + ")\n", + "W_constant = Constant(\"W_fir\", sw=30, values=W_fir)\n", + "\n", "\n", "# ----------------------------------------------------------------\n", "# Understeer correction function\n", "# ----------------------------------------------------------------\n", - "def understeer_corr_local(vx, # input\n", - " A # learnable parameter\n", - " ):\n", + "def understeer_corr_local(\n", + " vx, # input\n", + " A, # learnable parameter\n", + "):\n", " return 1 / (1 + A * torch.pow(vx, 2))\n", "\n", + "\n", "understeer_corr = ParamFun(understeer_corr_local)\n", "\n", "# ----------------------------------------------------------------\n", "# Local models\n", "# ----------------------------------------------------------------\n", "# Activation functions\n", - "fuzzy_vx = Fuzzify(centers=chan_vx, functions='Triangular')(vx_in.last())\n", - "fuzzy_ax = Fuzzify(centers=chan_ax, functions='Triangular')(ax_in.last())\n", + "fuzzy_vx = Fuzzify(centers=chan_vx, functions=\"Triangular\")(vx_in.last())\n", + "fuzzy_ax = Fuzzify(centers=chan_ax, functions=\"Triangular\")(ax_in.last())\n", + "\n", "\n", "# FIR input functions\n", "def Input_Function_Gen(idx):\n", " def fir_out(fir_input):\n", - " out = Fir(W_init='init_constant', W_init_params={\"value\": 1}, W=f'Fir_{idx[0]}')(fir_input)\n", + " out = Fir(\n", + " W_init=\"init_constant\", W_init_params={\"value\": 1}, W=f\"Fir_{idx[0]}\"\n", + " )(fir_input)\n", " return out\n", "\n", " return fir_out\n", "\n", + "\n", "# Local Input Model\n", "local_input_model = LocalModel(pass_indexes=True, input_function=Input_Function_Gen)\n", - "out_input_model = local_input_model((steer_in.sw(samples_past_steer) * W_constant ), fuzzy_vx)\n", + "out_input_model = local_input_model(\n", + " (steer_in.sw(samples_past_steer) * W_constant), fuzzy_vx\n", + ")\n", + "\n", "\n", "# Parametric functions: Understeer correction\n", "def Understeer_Function_Gen(idx):\n", " def osus_out(vx_input):\n", - " A = Parameter('A_' + str(idx[0]), values=[[1e-5]])\n", + " A = Parameter(\"A_\" + str(idx[0]), values=[[1e-5]])\n", " return understeer_corr(vx_input, A)\n", + "\n", " return osus_out\n", "\n", + "\n", "# Local Understeer Correction Model\n", "local_osus_corr = LocalModel(pass_indexes=True, input_function=Understeer_Function_Gen)\n", "out_osus = local_osus_corr(vx_in.last(), fuzzy_ax)\n", @@ -191,12 +214,12 @@ "# ----------------------------------------------------------------\n", "# Outputs\n", "# ----------------------------------------------------------------\n", - "curv_out = Output('model_curv', out_input_model*out_osus) # output of the model\n", + "curv_out = Output(\"model_curv\", out_input_model * out_osus) # output of the model\n", "\n", "# ----------------------------------------------------------------\n", "# Add model\n", "# ----------------------------------------------------------------\n", - "lat_dyna_model.addModel('vehicle_model', curv_out)" + "lat_dyna_model.addModel(\"vehicle_model\", curv_out)" ] }, { @@ -236,21 +259,16 @@ "# ------------------------------------------------------------------------\n", "# Targets\n", "# ------------------------------------------------------------------------\n", - "curv_in = Input('model_curv_in') # [1/m] path curvature\n", + "curv_in = Input(\"model_curv_in\") # [1/m] path curvature\n", "\n", "# Definition of heading as integral of curvature multiplied by the longitudinal speed\n", - "heading_target = Integrate(curv_in.next()*vx_in.next())\n", - "heading_nn = Integrate(out_input_model*out_osus*vx_in.next())\n", + "heading_target = Integrate(curv_in.next() * vx_in.next())\n", + "heading_nn = Integrate(out_input_model * out_osus * vx_in.next())\n", "\n", "# Add a loss function: mean squared error for the curvature prediction\n", - "lat_dyna_model.addMinimize('curv_error',\n", - " curv_in.next(),\n", - " curv_out,\n", - " loss_function='mse')\n", + "lat_dyna_model.addMinimize(\"curv_error\", curv_in.next(), curv_out, loss_function=\"mse\")\n", "# Add a loss function: mean squared error for the heading prediction\n", - "lat_dyna_model.addMinimize('heading_error',\n", - " heading_target,\n", - " heading_nn)" + "lat_dyna_model.addMinimize(\"heading_error\", heading_target, heading_nn)" ] }, { @@ -742,7 +760,9 @@ ], "source": [ "# Generate the model\n", - "lat_dyna_model.neuralizeModel(sample_time=0.05) # neuralize the model with the chosen sample time" + "lat_dyna_model.neuralizeModel(\n", + " sample_time=0.05\n", + ") # neuralize the model with the chosen sample time" ] }, { @@ -798,14 +818,40 @@ } ], "source": [ - "# Dataset loading: all the csv files in the folders defined in 'source' are loaded, with the format defined in 'format' \n", + "# Dataset loading: all the csv files in the folders defined in 'source' are loaded, with the format defined in 'format'\n", "# (the order of the columns in the csv files) and skipping the number of lines defined in 'skiplines'\n", - "lat_dyna_model.loadData('training_set', source='dataset/training',\n", - " format=['', 'model_ax_in', '', 'model_vx_in', 'model_curv_in', '', '', 'model_steer_in', ''],\n", - " skiplines=1)\n", - "lat_dyna_model.loadData('validation_set', source='dataset/validation',\n", - " format=['', 'model_ax_in', '', 'model_vx_in', 'model_curv_in', '', '', 'model_steer_in', ''],\n", - " skiplines=1)" + "lat_dyna_model.loadData(\n", + " \"training_set\",\n", + " source=\"dataset/training\",\n", + " format=[\n", + " \"\",\n", + " \"model_ax_in\",\n", + " \"\",\n", + " \"model_vx_in\",\n", + " \"model_curv_in\",\n", + " \"\",\n", + " \"\",\n", + " \"model_steer_in\",\n", + " \"\",\n", + " ],\n", + " skiplines=1,\n", + ")\n", + "lat_dyna_model.loadData(\n", + " \"validation_set\",\n", + " source=\"dataset/validation\",\n", + " format=[\n", + " \"\",\n", + " \"model_ax_in\",\n", + " \"\",\n", + " \"model_vx_in\",\n", + " \"model_curv_in\",\n", + " \"\",\n", + " \"\",\n", + " \"model_steer_in\",\n", + " \"\",\n", + " ],\n", + " skiplines=1,\n", + ")" ] }, { @@ -821,19 +867,20 @@ "outputs": [], "source": [ "# Default training parameters definition\n", - "training_pars = { 'num_of_epochs': 150,\n", - " 'val_batch_size': 64,\n", - " 'train_batch_size': 64,\n", - " 'lr': 1e-3 ,\n", - " 'train_dataset': 'training_set',\n", - " 'validation_dataset': 'validation_set',\n", - " 'optimizer': 'Adam',\n", - " 'shuffle_data': True,\n", - " 'prediction_samples': -1, # force to do not evaluate the integral\n", - " 'select_model': select_best_model,\n", - " 'early_stopping': earlystopping.early_stop_patience,\n", - " 'early_stopping_params': {'patience': 20,'error': 'curv_error'}\n", - " }" + "training_pars = {\n", + " \"num_of_epochs\": 150,\n", + " \"val_batch_size\": 64,\n", + " \"train_batch_size\": 64,\n", + " \"lr\": 1e-3,\n", + " \"train_dataset\": \"training_set\",\n", + " \"validation_dataset\": \"validation_set\",\n", + " \"optimizer\": \"Adam\",\n", + " \"shuffle_data\": True,\n", + " \"prediction_samples\": -1, # force to do not evaluate the integral\n", + " \"select_model\": select_best_model,\n", + " \"early_stopping\": earlystopping.early_stop_patience,\n", + " \"early_stopping_params\": {\"patience\": 20, \"error\": \"curv_error\"},\n", + "}" ] }, { @@ -1199,9 +1246,9 @@ ], "source": [ "# Training with intergal error 4s in the future\n", - "lat_dyna_model.trainModel(training_params=training_pars,\n", - " num_of_epochs=20,\n", - " prediction_samples=80)" + "lat_dyna_model.trainModel(\n", + " training_params=training_pars, num_of_epochs=20, prediction_samples=80\n", + ")" ] }, { @@ -1243,16 +1290,16 @@ "plt.figure()\n", "\n", "for key, value in lat_dyna_model.parameters.items():\n", - " if key.startswith('Fir_'):\n", + " if key.startswith(\"Fir_\"):\n", " params_fir[key] = lat_dyna_model.parameters[key]\n", "\n", "for key, value in params_fir.items():\n", " vals = np.array(value).flatten()\n", " t = np.arange(0, sampling_time * len(vals), sampling_time)\n", - " plt.plot(t,np.flip(vals * W_fir.flatten()), 'o', label=key)\n", + " plt.plot(t, np.flip(vals * W_fir.flatten()), \"o\", label=key)\n", "t = np.arange(0, sampling_time * samples_past_steer, sampling_time)\n", - "plt.xlabel('time (s)')\n", - "plt.ylabel('amplitude [m-1 rad-1]')\n", + "plt.xlabel(\"time (s)\")\n", + "plt.ylabel(\"amplitude [m-1 rad-1]\")\n", "plt.legend()\n", "plt.grid(True)\n", "plt.show()" @@ -1306,9 +1353,13 @@ ], "source": [ "# Dataset extraction\n", - "lat_dyna_model.loadData('test_set', source='dataset/test',\n", - " format=['', 'model_ax_in', '','model_vx_in', 'model_curv_in', 'model_steer_in'], skiplines=1)\n", - "data_in = lat_dyna_model.getSamples('test_set',index=1,window=1000)\n", + "lat_dyna_model.loadData(\n", + " \"test_set\",\n", + " source=\"dataset/test\",\n", + " format=[\"\", \"model_ax_in\", \"\", \"model_vx_in\", \"model_curv_in\", \"model_steer_in\"],\n", + " skiplines=1,\n", + ")\n", + "data_in = lat_dyna_model.getSamples(\"test_set\", index=1, window=1000)\n", "\n", "# Model inference\n", "out_nn = lat_dyna_model(data_in, sampled=True)\n", @@ -1316,11 +1367,20 @@ "dataset = pd.read_csv(\"dataset/test/test_set.csv\")\n", "\n", "# Plot\n", - "plt.figure(figsize=(12,6))\n", - "plt.plot(0.05*np.arange(len(np.array(dataset['curv'])[30:])),np.array(dataset['curv'])[30:],label='target')\n", - "plt.plot(0.05*np.arange(len(out_nn['model_curv'])), out_nn['model_curv'],'--',label='prediction')\n", - "plt.xlabel('time (s)')\n", - "plt.ylabel('curvature [1/m]')\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(\n", + " 0.05 * np.arange(len(np.array(dataset[\"curv\"])[30:])),\n", + " np.array(dataset[\"curv\"])[30:],\n", + " label=\"target\",\n", + ")\n", + "plt.plot(\n", + " 0.05 * np.arange(len(out_nn[\"model_curv\"])),\n", + " out_nn[\"model_curv\"],\n", + " \"--\",\n", + " label=\"prediction\",\n", + ")\n", + "plt.xlabel(\"time (s)\")\n", + "plt.ylabel(\"curvature [1/m]\")\n", "plt.grid(True)\n", "plt.legend()\n", "plt.show()" @@ -1357,7 +1417,7 @@ ], "source": [ "# Save json model: will be used in the controller training\n", - "lat_dyna_model.saveModel('vehicle_model')" + "lat_dyna_model.saveModel(\"vehicle_model\")" ] } ], diff --git a/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb index 4bd1ef67..bc406c8c 100644 --- a/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb +++ b/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb @@ -57,20 +57,22 @@ "\n", "# import a library for plots\n", "import matplotlib as mpl\n", + "\n", "mpl.rcParams.update(mpl.rcParamsDefault)\n", "import matplotlib.pyplot as plt\n", - "plt.close('all')\n", + "\n", + "plt.close(\"all\")\n", "SMALL_SIZE = 14\n", "MEDIUM_SIZE = 22\n", "BIGGER_SIZE = 26\n", - "plt.rc('font', size=SMALL_SIZE) # controls default text sizes\n", - "plt.rc('axes', titlesize=MEDIUM_SIZE) # fontsize of the axes title\n", - "plt.rc('axes', labelsize=SMALL_SIZE) # fontsize of the x and y labels\n", - "plt.rc('xtick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", - "plt.rc('ytick', labelsize=SMALL_SIZE) # fontsize of the tick labels\n", - "plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize\n", - "plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title\n", - "plt.rc('grid', linestyle=\"--\", color='grey')\n", + "plt.rc(\"font\", size=SMALL_SIZE) # controls default text sizes\n", + "plt.rc(\"axes\", titlesize=MEDIUM_SIZE) # fontsize of the axes title\n", + "plt.rc(\"axes\", labelsize=SMALL_SIZE) # fontsize of the x and y labels\n", + "plt.rc(\"xtick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n", + "plt.rc(\"ytick\", labelsize=SMALL_SIZE) # fontsize of the tick labels\n", + "plt.rc(\"legend\", fontsize=SMALL_SIZE) # legend fontsize\n", + "plt.rc(\"figure\", titlesize=BIGGER_SIZE) # fontsize of the figure title\n", + "plt.rc(\"grid\", linestyle=\"--\", color=\"grey\")\n", "\n", "# Congigurations\n", "device = \"cpu\"\n", @@ -79,7 +81,7 @@ "torch.manual_seed(1)\n", "np.random.seed(1)\n", "\n", - "path_folder = 'trained_models' # folder to save the model" + "path_folder = \"trained_models\" # folder to save the model" ] }, { @@ -117,14 +119,14 @@ "source": [ "# triangular activation function\n", "class TriangularMembershipFunction(nn.Module):\n", - " def __init__(self,centers: list) -> None:\n", + " def __init__(self, centers: list) -> None:\n", " super().__init__()\n", " self.centers = torch.tensor(centers, dtype=torch.float32) # (C,)\n", " self.C = self.centers.numel()\n", "\n", " def forward(self, x: torch.Tensor) -> torch.Tensor:\n", " \"\"\"\n", - " x: (batch,1) \n", + " x: (batch,1)\n", " out: (batch,C) -> membership per channel/centre\n", " \"\"\"\n", " # get sizes\n", @@ -133,39 +135,45 @@ " centers = self.centers\n", "\n", " # triangural membership function initalization\n", - " phi = torch.zeros(batch,self.C)\n", - " x = x.squeeze(1) # (C,)\n", + " phi = torch.zeros(batch, self.C)\n", + " x = x.squeeze(1) # (C,)\n", "\n", " # evaluate channel width and ceneters\n", " x_contig = x.contiguous()\n", - " idx0 = torch.clip(torch.bucketize(x_contig,centers)-1, 0, C-2) # [0, C-2] \n", - " idx1 = idx0 + 1 # [1, C-1]\n", - " c0 = centers[idx0] # left center\n", - " c1 = centers[idx1] # right center\n", - " ch_w = c1 - c0 # channels distance\n", + " idx0 = torch.clip(torch.bucketize(x_contig, centers) - 1, 0, C - 2) # [0, C-2]\n", + " idx1 = idx0 + 1 # [1, C-1]\n", + " c0 = centers[idx0] # left center\n", + " c1 = centers[idx1] # right center\n", + " ch_w = c1 - c0 # channels distance\n", "\n", " b = torch.arange(batch)\n", "\n", - " phi[b,idx0] = torch.clip( (c1 - x) / ch_w , 0 , 1 ) # left channel weight\n", - " phi[b,idx1] = torch.clip( (x - c0) / ch_w , 0 , 1 ) # right channel width\n", + " phi[b, idx0] = torch.clip((c1 - x) / ch_w, 0, 1) # left channel weight\n", + " phi[b, idx1] = torch.clip((x - c0) / ch_w, 0, 1) # right channel width\n", "\n", " return phi # (batch, C)\n", "\n", "\n", "class FirFuzzyLayer(nn.Module):\n", - " def __init__(self,channels: list,window: int) -> None:\n", + " def __init__(self, channels: list, window: int) -> None:\n", " super().__init__()\n", " self.C = len(channels)\n", - " self.phi_layer = TriangularMembershipFunction(channels) # activation functon weights\n", - " self.fir_layer = nn.Conv1d( # parallel fir filters\n", + " self.phi_layer = TriangularMembershipFunction(\n", + " channels\n", + " ) # activation functon weights\n", + " self.fir_layer = nn.Conv1d( # parallel fir filters\n", " in_channels=self.C,\n", " out_channels=self.C,\n", " kernel_size=window,\n", " groups=self.C,\n", - " bias=False) \n", + " bias=False,\n", + " )\n", " nn.init.constant_(self.fir_layer.weight, 1.0)\n", - " self.W_fir = torch.tensor([[np.exp(-(i/(window/2))**2)] for i in range(-window+1,1)],dtype=torch.float32).view(1,1,-1)\n", - " \n", + " self.W_fir = torch.tensor(\n", + " [[np.exp(-((i / (window / 2)) ** 2))] for i in range(-window + 1, 1)],\n", + " dtype=torch.float32,\n", + " ).view(1, 1, -1)\n", + "\n", " def forward(self, x: torch.Tensor, a: torch.Tensor) -> torch.Tensor:\n", " \"\"\"\n", " x: (batch,window) -> input signal with time windows\n", @@ -173,24 +181,25 @@ " out: (batch,1) -> interpolated fir output\n", " \"\"\"\n", " # evaluate membership function\n", - " phi = self.phi_layer(a) # (batch, C)\n", + " phi = self.phi_layer(a) # (batch, C)\n", "\n", " # replicate input for Conv1d\n", - " x_fir = x.unsqueeze(1)*self.W_fir # (batch, 1, window)\n", - " x_fir = x_fir.repeat(1,self.C, 1) # (batch, C, window)\n", - " y = self.fir_layer(x_fir) # (batch, C, 1)\n", + " x_fir = x.unsqueeze(1) * self.W_fir # (batch, 1, window)\n", + " x_fir = x_fir.repeat(1, self.C, 1) # (batch, C, window)\n", + " y = self.fir_layer(x_fir) # (batch, C, 1)\n", " # evaluate weightet output\n", - " y = y.squeeze(-1) # (batch, C)\n", + " y = y.squeeze(-1) # (batch, C)\n", " y = phi * y\n", "\n", - " return y.sum(dim=1).unsqueeze(-1) # (batch,1)\n", - " \n", + " return y.sum(dim=1).unsqueeze(-1) # (batch,1)\n", + "\n", + "\n", "class UsFuzzyLayer(nn.Module):\n", - " def __init__(self,channels: list) -> None:\n", + " def __init__(self, channels: list) -> None:\n", " super().__init__()\n", " self.C = len(channels)\n", - " self.phi_layer = TriangularMembershipFunction(channels)\n", - " self.A = nn.Parameter(1e-5*torch.ones(1,self.C)) \n", + " self.phi_layer = TriangularMembershipFunction(channels)\n", + " self.A = nn.Parameter(1e-5 * torch.ones(1, self.C))\n", "\n", " def forward(self, x: torch.Tensor, a: torch.Tensor) -> torch.Tensor:\n", " \"\"\"\n", @@ -199,35 +208,37 @@ " out: (batch,1) -> output\n", " \"\"\"\n", " # evaluate membership function\n", - " phi = self.phi_layer(a) # (batch,1)\n", + " phi = self.phi_layer(a) # (batch,1)\n", "\n", " # replicate for channels\n", - " x = x.repeat(1,self.C) # (batch,C)\n", + " x = x.repeat(1, self.C) # (batch,C)\n", "\n", " # evaluate parametric function\n", - " y = 1 / (1 + self.A * x**2) \n", + " y = 1 / (1 + self.A * x**2)\n", " y = phi * y\n", "\n", - " return y.sum(dim=1).unsqueeze(-1) #(batch,1)\n", - " \n", + " return y.sum(dim=1).unsqueeze(-1) # (batch,1)\n", + "\n", "\n", "class LateralDynamics(nn.Module):\n", " def __init__(self, chan_vx: list, chan_ax: list, window: int) -> None:\n", " super().__init__()\n", - " self.fir_layer = FirFuzzyLayer(chan_vx,window)\n", - " self.us_layer = UsFuzzyLayer(chan_ax)\n", + " self.fir_layer = FirFuzzyLayer(chan_vx, window)\n", + " self.us_layer = UsFuzzyLayer(chan_ax)\n", "\n", - " def forward(self, steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor) -> torch.Tensor:\n", + " def forward(\n", + " self, steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor\n", + " ) -> torch.Tensor:\n", " \"\"\"\n", - " steer: (batch,window) -> steer time window \n", + " steer: (batch,window) -> steer time window\n", " vx: (batch,1) -> vx last\n", " ax: (batch,1) -> ax last\n", " out: (batch,1) -> rho next\n", " \"\"\"\n", " # apply fir\n", - " rho_kin = self.fir_layer(steer,vx)\n", + " rho_kin = self.fir_layer(steer, vx)\n", " # apply us correction\n", - " corr_kus = self.us_layer(vx,ax)\n", + " corr_kus = self.us_layer(vx, ax)\n", "\n", " return rho_kin * corr_kus" ] @@ -247,17 +258,19 @@ "# ----------------------------------------------------------------\n", "# Hyperparameters\n", "# ----------------------------------------------------------------\n", - "samples_past_steer = 30 # number of samples in the past for the steering wheel angle prediction\n", - "n_channels_vx = 8 # number of channels for activation function vx\n", - "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n", - "chan_ax = [-2,-1,0.0,1.0,2.0] # centers of the channels ax\n", + "samples_past_steer = (\n", + " 30 # number of samples in the past for the steering wheel angle prediction\n", + ")\n", + "n_channels_vx = 8 # number of channels for activation function vx\n", + "chan_vx = list(np.linspace(8, 23, num=n_channels_vx)) # centers of the channels vx\n", + "chan_ax = [-2, -1, 0.0, 1.0, 2.0] # centers of the channels ax\n", "\n", - "prediction = 80 # 4s prediction\n", + "prediction = 80 # 4s prediction\n", "\n", "# ----------------------------------------------------------------\n", "# MODEL\n", "# ----------------------------------------------------------------\n", - "model = LateralDynamics(chan_vx,chan_ax,samples_past_steer)" + "model = LateralDynamics(chan_vx, chan_ax, samples_past_steer)" ] }, { @@ -284,16 +297,18 @@ "outputs": [], "source": [ "data_training = \"dataset/training/*.csv\"\n", - "data_valid = \"dataset/validation/*.csv\"\n", - "data_test = \"dataset/test/*.csv\"\n", + "data_valid = \"dataset/validation/*.csv\"\n", + "data_test = \"dataset/test/*.csv\"\n", "\n", - "head = ['ax', 'vx', 'curv', 'steer']\n", + "head = [\"ax\", \"vx\", \"curv\", \"steer\"]\n", "sampling = 0.05\n", "\n", "\n", "# loading of csv datasets\n", "import pandas as pd\n", "import glob\n", + "\n", + "\n", "def load_csv_list(data_path: str):\n", " dfs = []\n", " for file in glob.glob(data_path):\n", @@ -302,6 +317,7 @@ " dfs.append(df)\n", " return dfs\n", "\n", + "\n", "class VehicleDataset(Dataset):\n", " def __init__(self, df: pd.DataFrame, past_samples: int, prediction: int):\n", " self.lb = past_samples\n", @@ -309,9 +325,9 @@ " self.df = df\n", "\n", " self.steer = torch.tensor(df[\"steer\"].values, dtype=torch.float32)\n", - " self.vx = torch.tensor(df[\"vx\"].values, dtype=torch.float32)\n", - " self.ax = torch.tensor(df[\"ax\"].values, dtype=torch.float32)\n", - " self.curv = torch.tensor(df[\"curv\"].values, dtype=torch.float32)\n", + " self.vx = torch.tensor(df[\"vx\"].values, dtype=torch.float32)\n", + " self.ax = torch.tensor(df[\"ax\"].values, dtype=torch.float32)\n", + " self.curv = torch.tensor(df[\"curv\"].values, dtype=torch.float32)\n", "\n", " def __len__(self):\n", " return len(self.df) - self.lb - self.lf - 1\n", @@ -320,17 +336,18 @@ " steer = self.steer[i : i + self.lb + self.lf]\n", "\n", " if self.lf > 0:\n", - " vx = self.vx[i + self.lb -1: i + self.lb + self.lf -1]\n", - " ax = self.ax[i + self.lb -1: i + self.lb + self.lf -1]\n", + " vx = self.vx[i + self.lb - 1 : i + self.lb + self.lf - 1]\n", + " ax = self.ax[i + self.lb - 1 : i + self.lb + self.lf - 1]\n", " curv = self.curv[i + self.lb : i + self.lb + self.lf]\n", " else:\n", - " vx = self.vx[i + self.lb -1].unsqueeze(-1)\n", - " ax = self.ax[i + self.lb -1].unsqueeze(-1)\n", + " vx = self.vx[i + self.lb - 1].unsqueeze(-1)\n", + " ax = self.ax[i + self.lb - 1].unsqueeze(-1)\n", " curv = self.curv[i + self.lb].unsqueeze(-1)\n", "\n", " return steer, vx, ax, curv\n", "\n", - "# Concatenate datasets \n", + "\n", + "# Concatenate datasets\n", "def build_dataset(csv_path, past_samples, prediction):\n", " dfs = load_csv_list(csv_path)\n", " datasets = [\n", @@ -340,6 +357,7 @@ " ]\n", " return ConcatDataset(datasets)\n", "\n", + "\n", "# non recurrent dataset\n", "train_dataset = build_dataset(data_training, samples_past_steer, prediction=0)\n", "valid_dataset = build_dataset(data_valid, samples_past_steer, prediction=0)\n", @@ -573,33 +591,35 @@ "# ------------------------------------------------------------------------\n", "# Non-recurrent training\n", "# ------------------------------------------------------------------------\n", - "EPOCHS = 150\n", + "EPOCHS = 150\n", "\n", "train_loss_list = []\n", "valid_loss_list = []\n", "\n", - "def inference(steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor):\n", + "\n", + "def inference(\n", + " steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor\n", + "):\n", "\n", " # Make one-step prediction\n", - " output = model(steer,vx, ax)\n", + " output = model(steer, vx, ax)\n", " # Compute the loss\n", " loss = loss_fn(output, curv)\n", "\n", " return loss\n", "\n", "\n", - "\n", "def train_one_epoch():\n", " avg_loss = 0.0\n", "\n", " for i, data in enumerate(train_dataloader):\n", " steer, vx, ax, curv = data\n", - " \n", + "\n", " # Zero your gradients for every batch!\n", " optimizer.zero_grad()\n", "\n", " # compute loss and its gradient\n", - " loss = inference(steer,vx,ax,curv)\n", + " loss = inference(steer, vx, ax, curv)\n", " loss.backward()\n", "\n", " avg_loss += loss.item()\n", @@ -609,7 +629,7 @@ " return avg_loss / len(train_dataloader)\n", "\n", "\n", - "print('| {:^7} | {:^12} | {:^12} |'.format('EPOCHS','train_loss','valid_loss'))\n", + "print(\"| {:^7} | {:^12} | {:^12} |\".format(\"EPOCHS\", \"train_loss\", \"valid_loss\"))\n", "for epoch in range(EPOCHS):\n", " # Make sure gradient tracking is on, and do a pass over the data\n", " model.train(True)\n", @@ -619,29 +639,29 @@ " # statistics for batch normalization.\n", " model.eval()\n", " avg_vloss = 0.0\n", - " \n", + "\n", " with torch.no_grad():\n", " for i, vdata in enumerate(valid_dataloader):\n", " steer, vx, ax, curv = vdata\n", - " \n", - " vloss = inference(steer,vx,ax,curv)\n", + "\n", + " vloss = inference(steer, vx, ax, curv)\n", " avg_vloss += vloss.item()\n", " avg_vloss /= len(valid_dataloader)\n", - " \n", + "\n", " # save the total losses\n", " train_loss_list.append(avg_loss)\n", " valid_loss_list.append(avg_vloss)\n", "\n", - " print('| {:^7} | {:^12.6e} | {:^12.6e} |'.format(epoch+1, avg_loss, avg_vloss))\n", + " print(\"| {:^7} | {:^12.6e} | {:^12.6e} |\".format(epoch + 1, avg_loss, avg_vloss))\n", " epoch += 1\n", "\n", "plt.figure()\n", - "plt.title('Non recurrent training')\n", - "plt.plot(train_loss_list, label='train loss')\n", - "plt.plot(valid_loss_list, '--', label='validation loss')\n", - "plt.yscale('log')\n", - "plt.xlabel('Epochs')\n", - "plt.ylabel('Loss')\n", + "plt.title(\"Non recurrent training\")\n", + "plt.plot(train_loss_list, label=\"train loss\")\n", + "plt.plot(valid_loss_list, \"--\", label=\"validation loss\")\n", + "plt.yscale(\"log\")\n", + "plt.xlabel(\"Epochs\")\n", + "plt.ylabel(\"Loss\")\n", "plt.legend()\n", "plt.grid()\n", "plt.show()\n", @@ -708,36 +728,39 @@ "# ------------------------------------------------------------------------\n", "# Recurrent training\n", "# ------------------------------------------------------------------------\n", - "EPOCHS = 20\n", + "EPOCHS = 20\n", "\n", "curv_loss_list = []\n", "head_loss_list = []\n", "curv_vloss_list = []\n", "head_vloss_list = []\n", "\n", - "def inference_rec(steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor):\n", + "\n", + "def inference_rec(\n", + " steer: torch.Tensor, vx: torch.Tensor, ax: torch.Tensor, curv: torch.Tensor\n", + "):\n", "\n", " # recurrent training\n", - " psi0_ref = 0\n", - " psi_ref = 0\n", + " psi0_ref = 0\n", + " psi_ref = 0\n", " psi0_comp = 0\n", - " psi_comp = 0\n", + " psi_comp = 0\n", " loss_curv = 0\n", " loss_head = 0\n", "\n", " for j in range(prediction):\n", - " steer_k = steer[:,j:j+samples_past_steer]\n", - " vx_k = vx[:,j].unsqueeze(-1)\n", - " ax_k = ax[:,j].unsqueeze(-1)\n", - " curv_k = curv[:,j].unsqueeze(-1)\n", + " steer_k = steer[:, j : j + samples_past_steer]\n", + " vx_k = vx[:, j].unsqueeze(-1)\n", + " ax_k = ax[:, j].unsqueeze(-1)\n", + " curv_k = curv[:, j].unsqueeze(-1)\n", "\n", " # Make one-step prediction\n", - " output = model(steer_k,vx_k, ax_k)\n", + " output = model(steer_k, vx_k, ax_k)\n", " # Compute the loss\n", " loss_curv += loss_fn(output, curv_k)\n", - " psi_ref = psi0_ref + curv_k*vx_k*sampling\n", - " psi_comp = psi0_comp + output*vx_k*sampling\n", - " loss_head += loss_fn(psi_ref,psi_comp)\n", + " psi_ref = psi0_ref + curv_k * vx_k * sampling\n", + " psi_comp = psi0_comp + output * vx_k * sampling\n", + " loss_head += loss_fn(psi_ref, psi_comp)\n", "\n", " psi0_comp = psi_comp\n", " psi0_ref = psi_ref\n", @@ -750,7 +773,6 @@ " return loss, loss_curv, loss_head\n", "\n", "\n", - "\n", "def train_one_epoch_rec():\n", " avg_loss = 0.0\n", " avg_curv_loss = 0.0\n", @@ -762,7 +784,7 @@ " optimizer.zero_grad()\n", "\n", " # compute loss and its gradient\n", - " loss, loss_curv, loss_head = inference_rec(steer,vx,ax,curv)\n", + " loss, loss_curv, loss_head = inference_rec(steer, vx, ax, curv)\n", " loss.backward()\n", "\n", " avg_loss += loss.item()\n", @@ -773,10 +795,14 @@ " # Adjust learning weights\n", " optimizer.step()\n", "\n", - " return avg_loss / len(train_dataloader_rec), avg_curv_loss / len(train_dataloader_rec), avg_head_loss / len(train_dataloader_rec)\n", + " return (\n", + " avg_loss / len(train_dataloader_rec),\n", + " avg_curv_loss / len(train_dataloader_rec),\n", + " avg_head_loss / len(train_dataloader_rec),\n", + " )\n", "\n", "\n", - "print('| {:^7} | {:^12} | {:^12} |'.format('EPOCHS','train_loss','valid_loss'))\n", + "print(\"| {:^7} | {:^12} | {:^12} |\".format(\"EPOCHS\", \"train_loss\", \"valid_loss\"))\n", "for epoch in range(EPOCHS):\n", " # Make sure gradient tracking is on, and do a pass over the data\n", " model.train(True)\n", @@ -795,8 +821,8 @@ " with torch.no_grad():\n", " for i, vdata in enumerate(valid_dataloader_rec):\n", " steer, vx, ax, curv = vdata\n", - " \n", - " vloss, vloss_curv, vloss_head = inference_rec(steer,vx,ax,curv)\n", + "\n", + " vloss, vloss_curv, vloss_head = inference_rec(steer, vx, ax, curv)\n", " avg_vloss += vloss.item()\n", " avg_vloss_curv += vloss_curv.item()\n", " avg_vloss_head += vloss_head.item()\n", @@ -806,29 +832,29 @@ " avg_vloss_head /= len(valid_dataloader_rec)\n", " head_vloss_list.append(avg_vloss_head)\n", " curv_vloss_list.append(avg_vloss_curv)\n", - " \n", - " print('| {:^7} | {:^12.6e} | {:^12.6e} |'.format(epoch+1, avg_loss, avg_vloss))\n", + "\n", + " print(\"| {:^7} | {:^12.6e} | {:^12.6e} |\".format(epoch + 1, avg_loss, avg_vloss))\n", " epoch += 1\n", "\n", "\n", "plt.figure()\n", - "plt.title('Recurrent training')\n", - "plt.plot(curv_loss_list, label='train curvature loss')\n", - "plt.plot(curv_vloss_list, '--', label='validation curvature loss')\n", - "plt.yscale('log')\n", - "plt.xlabel('Epochs')\n", - "plt.ylabel('Loss')\n", + "plt.title(\"Recurrent training\")\n", + "plt.plot(curv_loss_list, label=\"train curvature loss\")\n", + "plt.plot(curv_vloss_list, \"--\", label=\"validation curvature loss\")\n", + "plt.yscale(\"log\")\n", + "plt.xlabel(\"Epochs\")\n", + "plt.ylabel(\"Loss\")\n", "plt.legend()\n", "plt.grid()\n", "plt.show()\n", "\n", "plt.figure()\n", - "plt.title('Recurrent training')\n", - "plt.plot(head_loss_list, label='train heading loss')\n", - "plt.plot(head_vloss_list, '--', label='validation heading loss')\n", - "plt.yscale('log')\n", - "plt.xlabel('Epochs')\n", - "plt.ylabel('Loss')\n", + "plt.title(\"Recurrent training\")\n", + "plt.plot(head_loss_list, label=\"train heading loss\")\n", + "plt.plot(head_vloss_list, \"--\", label=\"validation heading loss\")\n", + "plt.yscale(\"log\")\n", + "plt.xlabel(\"Epochs\")\n", + "plt.ylabel(\"Loss\")\n", "plt.legend()\n", "plt.grid()\n", "plt.show()\n", @@ -865,14 +891,14 @@ ], "source": [ "# inference\n", - "model = LateralDynamics(chan_vx,chan_ax,samples_past_steer)\n", + "model = LateralDynamics(chan_vx, chan_ax, samples_past_steer)\n", "model.load_state_dict(torch.load(path_folder + \"/vehicle_model_torch.pt\"))\n", "\n", "curv_targ = []\n", "curv_out = []\n", "\n", - "for i,data in enumerate(test_dataloader_analyse):\n", - " steer, vx, ax , curv = data\n", + "for i, data in enumerate(test_dataloader_analyse):\n", + " steer, vx, ax, curv = data\n", "\n", " model.eval()\n", " curv_ = model(steer, vx, ax)\n", @@ -880,16 +906,25 @@ " curv_targ.append(curv)\n", " curv_out.append(curv_)\n", "\n", - "curv_targ = torch.cat(curv_targ,dim=0).squeeze().numpy()\n", - "curv_out = torch.cat(curv_out,dim=0).squeeze().detach().numpy()\n", + "curv_targ = torch.cat(curv_targ, dim=0).squeeze().numpy()\n", + "curv_out = torch.cat(curv_out, dim=0).squeeze().detach().numpy()\n", "\n", "# Plot comparison\n", - "plt.figure(figsize=(12,6))\n", - "plt.plot(sampling*np.linspace(0,len(curv_targ),len(curv_targ)),curv_targ, label='Target curvature')\n", - "plt.plot(sampling*np.linspace(0,len(curv_out),len(curv_out)),curv_out, '--', label='Predicted curvature')\n", - "plt.xlabel('Time (s)')\n", - "plt.ylabel('Curvature (1/m)')\n", - "plt.title('Curvature Prediction vs Target')\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(\n", + " sampling * np.linspace(0, len(curv_targ), len(curv_targ)),\n", + " curv_targ,\n", + " label=\"Target curvature\",\n", + ")\n", + "plt.plot(\n", + " sampling * np.linspace(0, len(curv_out), len(curv_out)),\n", + " curv_out,\n", + " \"--\",\n", + " label=\"Predicted curvature\",\n", + ")\n", + "plt.xlabel(\"Time (s)\")\n", + "plt.ylabel(\"Curvature (1/m)\")\n", + "plt.title(\"Curvature Prediction vs Target\")\n", "plt.legend()\n", "plt.grid()\n", "plt.show()" @@ -922,19 +957,25 @@ ], "source": [ "sampling = 0.05\n", - "W_fir = torch.tensor([[np.exp(-(i/(samples_past_steer/2))**2)] for i in range(-samples_past_steer+1,1)],dtype=torch.float32).squeeze(-1)\n", + "W_fir = torch.tensor(\n", + " [\n", + " [np.exp(-((i / (samples_past_steer / 2)) ** 2))]\n", + " for i in range(-samples_past_steer + 1, 1)\n", + " ],\n", + " dtype=torch.float32,\n", + ").squeeze(-1)\n", "\n", "for name, param in model.named_parameters():\n", - " if name.startswith('fir_layer'):\n", + " if name.startswith(\"fir_layer\"):\n", " fir_param = param\n", "\n", "plt.figure()\n", "\n", - "for i in range(fir_param[:,0,0].detach().numpy().size):\n", - " weight_loc = fir_param[i,0,:]*W_fir\n", + "for i in range(fir_param[:, 0, 0].detach().numpy().size):\n", + " weight_loc = fir_param[i, 0, :] * W_fir\n", " weight_loc = weight_loc.detach().numpy()\n", - " time = np.linspace(0,sampling*(len(weight_loc)-1),len(weight_loc))\n", - " plt.plot(time, np.flip(weight_loc),'o',label=f\"Fir_{i}\")\n", + " time = np.linspace(0, sampling * (len(weight_loc) - 1), len(weight_loc))\n", + " plt.plot(time, np.flip(weight_loc), \"o\", label=f\"Fir_{i}\")\n", "plt.legend()\n", "plt.grid(True)\n", "plt.show()" diff --git a/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb b/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb index 19e7df06..c1106b78 100644 --- a/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb +++ b/case-studies/mass_spring_damper/comparison/mass_spring_damper_nnodely.ipynb @@ -103,22 +103,25 @@ "T_x = 0.1\n", "\n", "# Define the neural model\n", - "x = Input('x') # MSNN input (Mass position)\n", - "F = Input('F') # MSNN input (Force)\n", - "x_free = Fir(W_init = 'init_negexp', W_init_params = {'first_value':0.1,'size_index':0,'lambda':3})(x.tw(T_x))\n", + "x = Input(\"x\") # MSNN input (Mass position)\n", + "F = Input(\"F\") # MSNN input (Force)\n", + "x_free = Fir(\n", + " W_init=\"init_negexp\",\n", + " W_init_params={\"first_value\": 0.1, \"size_index\": 0, \"lambda\": 3},\n", + ")(x.tw(T_x))\n", "x_force = Fir(F.tw(T_x))\n", - "x_n = Output('x_n', x_free + x_force)\n", + "x_n = Output(\"x_n\", x_free + x_force)\n", "\n", "# Add the neural models to the nnodely structure\n", - "msd = Modely(seed=42, workspace='saved')\n", - "msd.addModel('neural_msd', x_n)\n", + "msd = Modely(seed=42, workspace=\"saved\")\n", + "msd.addModel(\"neural_msd\", x_n)\n", "\n", "# These functions are used to impose the minimization objectives.\n", "# Here it is minimized the error between the future position of x get from the dataset\n", "# and the estimator designed using the neural network.\n", "# The minimization is imposed via MSE error.\n", - "x_t = Input('x_t') # Real position\n", - "msd.addMinimize('x[t]', x_t.next(), x_n)\n", + "x_t = Input(\"x_t\") # Real position\n", + "msd.addMinimize(\"x[t]\", x_t.next(), x_n)\n", "\n", "# Nauralize the model and getting the neural network.\n", "# The sampling time depends on the datasets.\n", @@ -277,20 +280,22 @@ } ], "source": [ - "data_struct = ['time', ('x','x_t'), '', 'F']\n", - "msd.loadData(name = 'simulations',\n", - " source = '../msd-data/data',\n", - " format = data_struct, delimiter = ';')\n", + "data_struct = [\"time\", (\"x\", \"x_t\"), \"\", \"F\"]\n", + "msd.loadData(\n", + " name=\"simulations\", source=\"../msd-data/data\", format=data_struct, delimiter=\";\"\n", + ")\n", "\n", "# Neural network train\n", - "param_model = {'num_of_epochs' : 80,\n", - " 'train_batch_size' : 128,\n", - " 'lr' : 0.0005,\n", - " 'splits' : [70,20,10]}\n", - "msd.trainModel(training_params = param_model)\n", + "param_model = {\n", + " \"num_of_epochs\": 80,\n", + " \"train_batch_size\": 128,\n", + " \"lr\": 0.0005,\n", + " \"splits\": [70, 20, 10],\n", + "}\n", + "msd.trainModel(training_params=param_model)\n", "\n", "# Save the neural model\n", - "msd.exportPythonModel(name = 'msd_preliminary')" + "msd.exportPythonModel(name=\"msd_preliminary\")" ] }, { @@ -322,13 +327,15 @@ ], "source": [ "# Show the network performance on the test dataset\n", - "samples = msd.getSamples(dataset='simulations', window=2000-10, index=50*2000-450)\n", - "result = msd(samples, sampled=True, prediction_samples=2000-10, closed_loop={'x':'x_n'})\n", + "samples = msd.getSamples(dataset=\"simulations\", window=2000 - 10, index=50 * 2000 - 450)\n", + "result = msd(\n", + " samples, sampled=True, prediction_samples=2000 - 10, closed_loop={\"x\": \"x_n\"}\n", + ")\n", "\n", - "plt.figure(figsize=(10,5))\n", - "t = np.arange(len(result['x_n'])) * 0.01\n", - "plt.plot(t, result['x_n'], label=\"pred\", linewidth=2)\n", - "plt.plot(t, np.array(samples['x_t'])[:,0,0], '--', label=\"target\", linewidth=2)\n", + "plt.figure(figsize=(10, 5))\n", + "t = np.arange(len(result[\"x_n\"])) * 0.01\n", + "plt.plot(t, result[\"x_n\"], label=\"pred\", linewidth=2)\n", + "plt.plot(t, np.array(samples[\"x_t\"])[:, 0, 0], \"--\", label=\"target\", linewidth=2)\n", "plt.legend()\n", "plt.grid()\n", "plt.title(\"Model Rollout vs Target\")\n", @@ -412,10 +419,17 @@ ], "source": [ "# Refine weights with recurrent train\n", - "msd.trainModel(num_of_epochs = 10, prediction_samples = 1500, step = 500, lr=0.00001, closed_loop={'x':'x_n'}, training_params = param_model)\n", + "msd.trainModel(\n", + " num_of_epochs=10,\n", + " prediction_samples=1500,\n", + " step=500,\n", + " lr=0.00001,\n", + " closed_loop={\"x\": \"x_n\"},\n", + " training_params=param_model,\n", + ")\n", "\n", "# Save the neural model in json format\n", - "msd.saveModel(name = 'msd_final')" + "msd.saveModel(name=\"msd_final\")" ] }, { @@ -437,13 +451,15 @@ ], "source": [ "# Show the network performance on the test dataset after recurrent training\n", - "samples = msd.getSamples(dataset='simulations', window=2000-10, index=50*2000-450)\n", - "result = msd(samples, sampled=True, prediction_samples=2000-10, closed_loop={'x':'x_n'})\n", + "samples = msd.getSamples(dataset=\"simulations\", window=2000 - 10, index=50 * 2000 - 450)\n", + "result = msd(\n", + " samples, sampled=True, prediction_samples=2000 - 10, closed_loop={\"x\": \"x_n\"}\n", + ")\n", "\n", - "plt.figure(figsize=(10,5))\n", - "t = np.arange(len(result['x_n'])) * 0.01\n", - "plt.plot(t, result['x_n'], label=\"pred\", linewidth=2)\n", - "plt.plot(t, np.array(samples['x_t'])[:,0,0], '--', label=\"target\", linewidth=2)\n", + "plt.figure(figsize=(10, 5))\n", + "t = np.arange(len(result[\"x_n\"])) * 0.01\n", + "plt.plot(t, result[\"x_n\"], label=\"pred\", linewidth=2)\n", + "plt.plot(t, np.array(samples[\"x_t\"])[:, 0, 0], \"--\", label=\"target\", linewidth=2)\n", "plt.legend()\n", "plt.grid()\n", "plt.title(\"Model Rollout vs Target\")\n", @@ -587,14 +603,14 @@ } ], "source": [ - "x_m = Input('x_m') # measured position\n", - "kp = Parameter('P', values=0.5)\n", - "ki = Parameter('I', values=0.5)\n", - "kd = Parameter('D', values=0.5)\n", - "e = x_t.next()-x_m.next()\n", - "c = e*kp+Integrate(e)*ki+Differentiate(e)*kd\n", - "controlForce = Output('F_PID', c)\n", - "msd.addModel('PID',controlForce)\n", + "x_m = Input(\"x_m\") # measured position\n", + "kp = Parameter(\"P\", values=0.5)\n", + "ki = Parameter(\"I\", values=0.5)\n", + "kd = Parameter(\"D\", values=0.5)\n", + "e = x_t.next() - x_m.next()\n", + "c = e * kp + Integrate(e) * ki + Differentiate(e) * kd\n", + "controlForce = Output(\"F_PID\", c)\n", + "msd.addModel(\"PID\", controlForce)\n", "\n", "# Neuralization of the whole models\n", "msd.neuralizeModel()" @@ -679,16 +695,21 @@ ], "source": [ "# Train the PID controller\n", - "msd.trainModel(models = 'PID',\n", - " closed_loop = {'x' : 'x_n', 'x_m' : 'x_n'},\n", - " connect = {'F' : 'F_PID'},\n", - " prediction_samples = 500,\n", - " step = 500, num_of_epochs = 20,\n", - " lr = 0.05,\n", - " training_params = param_model)\n", + "msd.trainModel(\n", + " models=\"PID\",\n", + " closed_loop={\"x\": \"x_n\", \"x_m\": \"x_n\"},\n", + " connect={\"F\": \"F_PID\"},\n", + " prediction_samples=500,\n", + " step=500,\n", + " num_of_epochs=20,\n", + " lr=0.05,\n", + " training_params=param_model,\n", + ")\n", "\n", "# Print the parameter of the PID\n", - "print(f\"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}\")" + "print(\n", + " f\"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}\"\n", + ")" ] }, { @@ -698,7 +719,7 @@ "metadata": {}, "outputs": [], "source": [ - "msd.exportPythonModel(name = 'msd_final_with_PID')" + "msd.exportPythonModel(name=\"msd_final_with_PID\")" ] }, { @@ -741,16 +762,20 @@ ], "source": [ "# Test the controller on step and triangular signal\n", - "tt = np.linspace(0, 5, int(5/0.01), endpoint=False)\n", - "data_target = np.concat([np.ones(511,dtype=np.float32)*1.0, # Step\n", - " -2*np.ones(500,dtype=np.float32)*1.0,\n", - " 2 * np.abs( 2 * (tt*1/5 - np.floor(tt*1/5 + 0.5)) ) - 1,\n", - " 2 * np.abs( 2 * (tt*1/2.5 - np.floor(tt*1/2.5 + 0.5)) ) - 1])\n", - "msd.loadData('test_control', {'x_t': data_target})\n", + "tt = np.linspace(0, 5, int(5 / 0.01), endpoint=False)\n", + "data_target = np.concat(\n", + " [\n", + " np.ones(511, dtype=np.float32) * 1.0, # Step\n", + " -2 * np.ones(500, dtype=np.float32) * 1.0,\n", + " 2 * np.abs(2 * (tt * 1 / 5 - np.floor(tt * 1 / 5 + 0.5))) - 1,\n", + " 2 * np.abs(2 * (tt * 1 / 2.5 - np.floor(tt * 1 / 2.5 + 0.5))) - 1,\n", + " ]\n", + ")\n", + "msd.loadData(\"test_control\", {\"x_t\": data_target})\n", "\n", "vis = MPLNotebookVisualizer()\n", "vis.setModely(msd)\n", - "vis.showResult('test_control')\n" + "vis.showResult(\"test_control\")" ] } ], diff --git a/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb b/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb index 62616256..50482538 100644 --- a/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb +++ b/case-studies/mass_spring_damper/comparison/mass_spring_damper_torch.ipynb @@ -54,11 +54,11 @@ "\n", " if init_negexp:\n", " t = torch.arange(window_size).float()\n", - " w = torch.exp(-3*t)\n", + " w = torch.exp(-3 * t)\n", " w = 0.1 * w / w.sum()\n", " self.weight.data[:] = w.flip(0)\n", " else:\n", - " self.weight.data.uniform_(.0, 1.0)\n", + " self.weight.data.uniform_(0.0, 1.0)\n", "\n", " def forward(self, x):\n", " # x: (B, T)\n", @@ -82,7 +82,7 @@ "\n", " def step(self, x, u):\n", " return self.x_free(x) + self.x_force(u)\n", - " \n", + "\n", " def forward(self, x, u):\n", " return self.step(x, u)\n", "\n", @@ -90,9 +90,9 @@ " preds = []\n", "\n", " for k in range(horizon):\n", - " u_k = u_seq[:, k:k+self.window_dim]\n", + " u_k = u_seq[:, k : k + self.window_dim]\n", "\n", - " x_next = self.step(x, u_k) # (B,1)\n", + " x_next = self.step(x, u_k) # (B,1)\n", "\n", " preds.append(x_next.squeeze(1))\n", "\n", @@ -120,23 +120,25 @@ "outputs": [], "source": [ "def load_simulation_file(path):\n", - " data = np.loadtxt(path, delimiter=';')\n", + " data = np.loadtxt(path, delimiter=\";\")\n", "\n", - " time = data[:,0]\n", - " x = data[:,1]\n", - " v = data[:,2]\n", - " u = data[:,3]\n", + " time = data[:, 0]\n", + " x = data[:, 1]\n", + " v = data[:, 2]\n", + " u = data[:, 3]\n", "\n", " return x, v, u\n", "\n", + "\n", "from torch.utils.data import Dataset\n", "from torch.utils.data import DataLoader\n", "from torch.utils.data import random_split\n", "\n", + "\n", "class MSDSimDataset(Dataset):\n", " def __init__(self, folder, window_dim, horizon=1, stride=1):\n", " self.window_dim = window_dim\n", - " self.data = [] # file list\n", + " self.data = [] # file list\n", " self.index_map = [] # global window map\n", " self.horizon = horizon\n", " self.stride = stride\n", @@ -147,9 +149,9 @@ " for f in files:\n", " x, v, u = load_simulation_file(f)\n", "\n", - " x = torch.tensor(x, dtype=torch.float32)\n", + " x = torch.tensor(x, dtype=torch.float32)\n", " v = torch.tensor(v, dtype=torch.float32)\n", - " u = torch.tensor(u, dtype=torch.float32)\n", + " u = torch.tensor(u, dtype=torch.float32)\n", "\n", " self.data.append((x, v, u))\n", "\n", @@ -174,23 +176,24 @@ " return (\n", " x[start : start + W],\n", " x[start + W : start + W + H],\n", - " u[start : start + W + H - 1]\n", + " u[start : start + W + H - 1],\n", " )\n", "\n", + "\n", "WINDOW_DIM = 10\n", "HORIZON = 1\n", "\n", "dataset = MSDSimDataset(\"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON)\n", "n = len(dataset)\n", - "n_train = int(0.7*n)\n", - "n_val = int(0.2*n)\n", - "n_test = n - n_train - n_val\n", + "n_train = int(0.7 * n)\n", + "n_val = int(0.2 * n)\n", + "n_test = n - n_train - n_val\n", "\n", - "train_ds, val_ds, test_ds = random_split(dataset, [n_train,n_val,n_test])\n", + "train_ds, val_ds, test_ds = random_split(dataset, [n_train, n_val, n_test])\n", "\n", "train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)\n", - "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n", - "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)" + "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n", + "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)" ] }, { @@ -222,7 +225,7 @@ "opt = torch.optim.Adam(model.parameters(), lr=5e-4)\n", "loss_fn = nn.MSELoss()\n", "\n", - "best_val_loss = float('inf')\n", + "best_val_loss = float(\"inf\")\n", "\n", "train_losses = []\n", "val_losses = []\n", @@ -258,7 +261,9 @@ " best_val_loss = val_loss\n", " torch.save(model.state_dict(), \"saved/best_model.pth\")\n", " val_losses.append(val_loss)\n", - " pbar.set_description(f\"Epoch {epoch+1} | train loss: {total_loss/len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\")" + " pbar.set_description(\n", + " f\"Epoch {epoch + 1} | train loss: {total_loss / len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\"\n", + " )" ] }, { @@ -294,13 +299,15 @@ "best_model = NeuralMSD(window_dim=WINDOW_DIM)\n", "best_model.load_state_dict(torch.load(\"saved/best_model.pth\"))\n", "\n", - "preds = best_model.rollout(x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x)-WINDOW_DIM).squeeze(0)\n", + "preds = best_model.rollout(\n", + " x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x) - WINDOW_DIM\n", + ").squeeze(0)\n", "\n", "target = x[WINDOW_DIM:]\n", "t = np.arange(len(preds)) * 0.01\n", - "plt.figure(figsize=(12,6))\n", + "plt.figure(figsize=(12, 6))\n", "plt.plot(t, preds.detach().numpy(), label=\"pred\", linewidth=2)\n", - "plt.plot(t, target.numpy(), '--', label=\"target\", linewidth=2)\n", + "plt.plot(t, target.numpy(), \"--\", label=\"target\", linewidth=2)\n", "plt.legend()\n", "plt.grid()\n", "plt.title(\"Model Rollout vs Target\")\n", @@ -331,17 +338,19 @@ "# Recurrent dataset\n", "HORIZON = 1500\n", "\n", - "dataset = MSDSimDataset(\"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5)\n", + "dataset = MSDSimDataset(\n", + " \"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5\n", + ")\n", "n = len(dataset)\n", - "n_train = int(0.7*n)\n", - "n_val = int(0.2*n)\n", - "n_test = n - n_train - n_val\n", + "n_train = int(0.7 * n)\n", + "n_val = int(0.2 * n)\n", + "n_test = n - n_train - n_val\n", "\n", - "train_ds, val_ds, test_ds = random_split(dataset, [n_train,n_val,n_test])\n", + "train_ds, val_ds, test_ds = random_split(dataset, [n_train, n_val, n_test])\n", "\n", "train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)\n", - "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n", - "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)" + "val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)\n", + "test_loader = DataLoader(test_ds, batch_size=128, shuffle=False)" ] }, { @@ -364,7 +373,7 @@ "opt = torch.optim.Adam(model.parameters(), lr=1e-5)\n", "loss_fn = nn.MSELoss()\n", "\n", - "best_val_loss = float('inf')\n", + "best_val_loss = float(\"inf\")\n", "\n", "train_losses = []\n", "val_losses = []\n", @@ -400,7 +409,9 @@ " best_val_loss = val_loss\n", " torch.save(model.state_dict(), \"saved/best_model.pth\")\n", " val_losses.append(val_loss)\n", - " pbar.set_description(f\"Epoch {epoch+1} | train loss: {total_loss/len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\")" + " pbar.set_description(\n", + " f\"Epoch {epoch + 1} | train loss: {total_loss / len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\"\n", + " )" ] }, { @@ -427,13 +438,15 @@ "best_model = NeuralMSD(window_dim=WINDOW_DIM)\n", "best_model.load_state_dict(torch.load(\"saved/best_model.pth\"))\n", "\n", - "preds = best_model.rollout(x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x)-WINDOW_DIM).squeeze(0)\n", + "preds = best_model.rollout(\n", + " x[:WINDOW_DIM].unsqueeze(0), u.unsqueeze(0), horizon=len(x) - WINDOW_DIM\n", + ").squeeze(0)\n", "\n", "target = x[WINDOW_DIM:]\n", "t = np.arange(len(preds)) * 0.01\n", - "plt.figure(figsize=(12,6))\n", + "plt.figure(figsize=(12, 6))\n", "plt.plot(t, preds.detach().numpy(), label=\"pred\", linewidth=2)\n", - "plt.plot(t, target.numpy(), '--', label=\"target\", linewidth=2)\n", + "plt.plot(t, target.numpy(), \"--\", label=\"target\", linewidth=2)\n", "plt.legend()\n", "plt.grid()\n", "plt.title(\"Model Rollout vs Target\")\n", @@ -476,7 +489,7 @@ " def reset_state(self, batch_size, device=None):\n", " device = device or self.kp.device\n", " self.integ_state = torch.zeros((batch_size, 1), device=device)\n", - " self.prev_error = torch.zeros((batch_size, 1), device=device)\n", + " self.prev_error = torch.zeros((batch_size, 1), device=device)\n", "\n", " def forward(self, e):\n", " \"\"\"\n", @@ -492,11 +505,7 @@ "\n", " self.prev_error = e\n", "\n", - " u = (\n", - " self.kp * e +\n", - " self.ki * self.integ_state +\n", - " self.kd * deriv\n", - " )\n", + " u = self.kp * e + self.ki * self.integ_state + self.kd * deriv\n", "\n", " return u" ] @@ -521,11 +530,11 @@ " self.pid.reset_state(batch_size=x0.shape[0], device=x0.device)\n", "\n", " for k in range(target.shape[1]):\n", - " e = target[:,k] - x[:, -1]\n", + " e = target[:, k] - x[:, -1]\n", " # shift control sequence and append new control\n", " u_seq = torch.cat([u_seq[:, 1:], self.pid(e.unsqueeze(1))], dim=1)\n", " x_next = self.plant.step(x, u_seq)\n", - " \n", + "\n", " # shift state sequence and append new state\n", " x = torch.cat([x[:, 1:], x_next], dim=1)\n", " xs.append(x_next.squeeze(1))\n", @@ -553,17 +562,19 @@ "# PID dataset\n", "HORIZON = 500\n", "\n", - "dataset = MSDSimDataset(\"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5)\n", + "dataset = MSDSimDataset(\n", + " \"../msd-data/data\", window_dim=WINDOW_DIM, horizon=HORIZON, stride=5\n", + ")\n", "n = len(dataset)\n", - "n_train = int(0.7*n)\n", - "n_val = int(0.2*n)\n", - "n_test = n - n_train - n_val\n", + "n_train = int(0.7 * n)\n", + "n_val = int(0.2 * n)\n", + "n_test = n - n_train - n_val\n", "\n", - "train_ds, val_ds, test_ds = random_split(dataset, [n_train,n_val,n_test])\n", + "train_ds, val_ds, test_ds = random_split(dataset, [n_train, n_val, n_test])\n", "\n", "train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)\n", - "val_loader = DataLoader(val_ds, batch_size=2048, shuffle=False)\n", - "test_loader = DataLoader(test_ds, batch_size=2048, shuffle=False)" + "val_loader = DataLoader(val_ds, batch_size=2048, shuffle=False)\n", + "test_loader = DataLoader(test_ds, batch_size=2048, shuffle=False)" ] }, { @@ -589,7 +600,7 @@ "opt = torch.optim.Adam(pid.parameters(), lr=0.05)\n", "loss_fn = nn.MSELoss()\n", "\n", - "best_val_loss = float('inf')\n", + "best_val_loss = float(\"inf\")\n", "\n", "train_losses = []\n", "val_losses = []\n", @@ -625,7 +636,9 @@ " best_val_loss = val_loss\n", " torch.save(pid.state_dict(), \"saved/best_controller.pth\")\n", " val_losses.append(val_loss)\n", - " pbar.set_description(f\"Epoch {epoch+1} | train loss: {total_loss/len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\")" + " pbar.set_description(\n", + " f\"Epoch {epoch + 1} | train loss: {total_loss / len(train_loader):.3e} | val_loss: {val_loss:.3e} | best_val_loss: {best_val_loss:.3e}\"\n", + " )" ] }, { @@ -644,7 +657,9 @@ ], "source": [ "# Print the parameter of the PID\n", - "print(f\"Trained PID parameters: kp={pid.kp.item():.3f}, ki={pid.ki.item():.3f}, kd={pid.kd.item():.3f}\")" + "print(\n", + " f\"Trained PID parameters: kp={pid.kp.item():.3f}, ki={pid.ki.item():.3f}, kd={pid.kd.item():.3f}\"\n", + ")" ] }, { @@ -668,13 +683,20 @@ "best_pid.load_state_dict(torch.load(\"saved/best_controller.pth\"))\n", "best_system = ControlledSystem(best_model, best_pid)\n", "\n", - "tt = torch.linspace(0, 5, int(5/0.01))\n", - "data_target = torch.cat([torch.ones(511,dtype=torch.float32)*1.0, # Step\n", - " -2*torch.ones(500,dtype=torch.float32)*1.0,\n", - " 2 * torch.abs( 2 * (tt*1/5 - torch.floor(tt*1/5 + 0.5)) ) - 1, # Triangle\n", - " 2 * torch.abs( 2 * (tt*1/2.5 - torch.floor(tt*1/2.5 + 0.5)) ) - 1])\n", + "tt = torch.linspace(0, 5, int(5 / 0.01))\n", + "data_target = torch.cat(\n", + " [\n", + " torch.ones(511, dtype=torch.float32) * 1.0, # Step\n", + " -2 * torch.ones(500, dtype=torch.float32) * 1.0,\n", + " 2 * torch.abs(2 * (tt * 1 / 5 - torch.floor(tt * 1 / 5 + 0.5))) - 1, # Triangle\n", + " 2 * torch.abs(2 * (tt * 1 / 2.5 - torch.floor(tt * 1 / 2.5 + 0.5))) - 1,\n", + " ]\n", + ")\n", "best_system.eval()\n", - "preds = best_system(torch.zeros_like(data_target[1:WINDOW_DIM+1].unsqueeze(0)), data_target[WINDOW_DIM+1:].unsqueeze(0))" + "preds = best_system(\n", + " torch.zeros_like(data_target[1 : WINDOW_DIM + 1].unsqueeze(0)),\n", + " data_target[WINDOW_DIM + 1 :].unsqueeze(0),\n", + ")" ] }, { @@ -697,18 +719,39 @@ "source": [ "df = pd.read_csv(\"PID_test_nnodely.csv\")\n", "\n", - "plt.figure(figsize=(12,6))\n", + "plt.figure(figsize=(12, 6))\n", "t = torch.arange(len(preds.squeeze(0).detach().numpy())) * 0.01\n", - "plt.plot(t.numpy(), data_target[WINDOW_DIM+1:].numpy(), '-', label=\"Target\", linewidth=1, color='black')\n", - "plt.plot(t.numpy(), df['estimate'].to_numpy(), '--', label=\"nnodely PID\", linewidth=3, color='tab:blue')\n", - "plt.plot(t.numpy(), preds.squeeze(0).detach().numpy(), '-.', label=\"PyTorch PID\", linewidth=3, color='tab:orange')\n", + "plt.plot(\n", + " t.numpy(),\n", + " data_target[WINDOW_DIM + 1 :].numpy(),\n", + " \"-\",\n", + " label=\"Target\",\n", + " linewidth=1,\n", + " color=\"black\",\n", + ")\n", + "plt.plot(\n", + " t.numpy(),\n", + " df[\"estimate\"].to_numpy(),\n", + " \"--\",\n", + " label=\"nnodely PID\",\n", + " linewidth=3,\n", + " color=\"tab:blue\",\n", + ")\n", + "plt.plot(\n", + " t.numpy(),\n", + " preds.squeeze(0).detach().numpy(),\n", + " \"-.\",\n", + " label=\"PyTorch PID\",\n", + " linewidth=3,\n", + " color=\"tab:orange\",\n", + ")\n", "plt.xlim(0, 20)\n", "plt.ylim(-3.5, 1.5)\n", "plt.grid()\n", "plt.title(\"PID Controller Performance Comparison\")\n", "plt.xlabel(\"Time [s]\")\n", "plt.ylabel(\"Position [m]\")\n", - "plt.legend(loc='lower right', fontsize=24)\n", + "plt.legend(loc=\"lower right\", fontsize=24)\n", "plt.show()" ] } diff --git a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py index 1d916b74..1704b63d 100644 --- a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py +++ b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.py @@ -1,107 +1,241 @@ import torch + def nnodely_basic_model_update_state(data_in, rel): data_out = data_in.clone() max_dim = min(rel.size(1), data_in.size(1)) data_out[:, -max_dim:, :] = rel[:, -max_dim:, :] return data_out + def nnodely_basic_model_timeshift(data_in): return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1) + class TracerModel(torch.nn.Module): def __init__(self): super().__init__() self.all_parameters = {} self.all_constants = {} - self.all_constants["SampleTime"] = torch.tensor(0.009999999776482582, requires_grad=False) - self.all_parameters["D"] = torch.nn.Parameter(torch.tensor([8.177507400512695]), requires_grad=True) - self.all_parameters["I"] = torch.nn.Parameter(torch.tensor([18.563440322875977]), requires_grad=True) - self.all_parameters["P"] = torch.nn.Parameter(torch.tensor([16.827768325805664]), requires_grad=True) - self.all_parameters["PFir3W"] = torch.nn.Parameter(torch.tensor([[-0.18469615280628204], [-0.12608873844146729], [-0.06660982221364975], [-0.005940185859799385], [0.05625353008508682], [0.1205686405301094], [0.18780383467674255], [0.2590898871421814], [0.3359234631061554], [0.4205183684825897]]), requires_grad=True) - self.all_parameters["PFir5W"] = torch.nn.Parameter(torch.tensor([[-1.662617978581693e-05], [4.8189936933340505e-05], [7.177559018600732e-05], [5.9527203120524064e-05], [0.00011585278843995184], [0.00019496057939250022], [0.00014330405974760652], [0.00018421869026497006], [0.00017740165640134364], [8.003542461665347e-05]]), requires_grad=True) + self.all_constants["SampleTime"] = torch.tensor( + 0.009999999776482582, requires_grad=False + ) + self.all_parameters["D"] = torch.nn.Parameter( + torch.tensor([8.177507400512695]), requires_grad=True + ) + self.all_parameters["I"] = torch.nn.Parameter( + torch.tensor([18.563440322875977]), requires_grad=True + ) + self.all_parameters["P"] = torch.nn.Parameter( + torch.tensor([16.827768325805664]), requires_grad=True + ) + self.all_parameters["PFir3W"] = torch.nn.Parameter( + torch.tensor( + [ + [-0.18469615280628204], + [-0.12608873844146729], + [-0.06660982221364975], + [-0.005940185859799385], + [0.05625353008508682], + [0.1205686405301094], + [0.18780383467674255], + [0.2590898871421814], + [0.3359234631061554], + [0.4205183684825897], + ] + ), + requires_grad=True, + ) + self.all_parameters["PFir5W"] = torch.nn.Parameter( + torch.tensor( + [ + [-1.662617978581693e-05], + [4.8189936933340505e-05], + [7.177559018600732e-05], + [5.9527203120524064e-05], + [0.00011585278843995184], + [0.00019496057939250022], + [0.00014330405974760652], + [0.00018421869026497006], + [0.00017740165640134364], + [8.003542461665347e-05], + ] + ), + requires_grad=True, + ) self.all_constants["SamplePart10"] = torch.tensor([[1.0]], requires_grad=True) self.all_constants["SamplePart12"] = torch.tensor([[1.0]], requires_grad=True) self.all_constants["SamplePart17"] = torch.tensor([[1.0]], requires_grad=True) - self.all_constants["SamplePart26"] = torch.tensor([[0.0, 1.0]], requires_grad=True) - self.all_constants["SamplePart28"] = torch.tensor([[1.0, 0.0]], requires_grad=True) + self.all_constants["SamplePart26"] = torch.tensor( + [[0.0, 1.0]], requires_grad=True + ) + self.all_constants["SamplePart28"] = torch.tensor( + [[1.0, 0.0]], requires_grad=True + ) self.all_constants["SamplePart8"] = torch.tensor([[1.0]], requires_grad=True) - self.all_constants["TimePart1"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True) - self.all_constants["TimePart4"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True) + self.all_constants["TimePart1"] = torch.tensor( + [ + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + ], + requires_grad=True, + ) + self.all_constants["TimePart4"] = torch.tensor( + [ + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + ], + requires_grad=True, + ) self.all_parameters = torch.nn.ParameterDict(self.all_parameters) self.all_constants = torch.nn.ParameterDict(self.all_constants) def update(self, closed_loop={}, connect={}, disconnect=False): pass - + def forward(self, kwargs): - getitem = kwargs['x_m'] + getitem = kwargs["x_m"] relation_forward_sample_part12_w = self.all_constants.SamplePart12 - einsum = torch.functional.einsum('bij,ki->bkj', getitem, relation_forward_sample_part12_w); getitem = relation_forward_sample_part12_w = None - getitem_1 = kwargs['x_t'] + einsum = torch.functional.einsum( + "bij,ki->bkj", getitem, relation_forward_sample_part12_w + ) + getitem = relation_forward_sample_part12_w = None + getitem_1 = kwargs["x_t"] relation_forward_sample_part10_w = self.all_constants.SamplePart10 - einsum_1 = torch.functional.einsum('bij,ki->bkj', getitem_1, relation_forward_sample_part10_w); getitem_1 = relation_forward_sample_part10_w = None - sub = einsum_1 - einsum; einsum_1 = einsum = None - getitem_2 = kwargs['Sub13_int14'] - update_state = nnodely_basic_model_update_state(getitem_2, sub); getitem_2 = None + einsum_1 = torch.functional.einsum( + "bij,ki->bkj", getitem_1, relation_forward_sample_part10_w + ) + getitem_1 = relation_forward_sample_part10_w = None + sub = einsum_1 - einsum + einsum_1 = einsum = None + getitem_2 = kwargs["Sub13_int14"] + update_state = nnodely_basic_model_update_state(getitem_2, sub) + getitem_2 = None relation_forward_sample_part28_w = self.all_constants.SamplePart28 - einsum_2 = torch.functional.einsum('bij,ki->bkj', update_state, relation_forward_sample_part28_w); relation_forward_sample_part28_w = None + einsum_2 = torch.functional.einsum( + "bij,ki->bkj", update_state, relation_forward_sample_part28_w + ) + relation_forward_sample_part28_w = None relation_forward_sample_part26_w = self.all_constants.SamplePart26 - einsum_3 = torch.functional.einsum('bij,ki->bkj', update_state, relation_forward_sample_part26_w); relation_forward_sample_part26_w = None - sub_1 = einsum_3 - einsum_2; einsum_3 = einsum_2 = None + einsum_3 = torch.functional.einsum( + "bij,ki->bkj", update_state, relation_forward_sample_part26_w + ) + relation_forward_sample_part26_w = None + sub_1 = einsum_3 - einsum_2 + einsum_3 = einsum_2 = None all_constants_sample_time = self.all_constants.SampleTime - truediv = sub_1 / all_constants_sample_time; sub_1 = None + truediv = sub_1 / all_constants_sample_time + sub_1 = None all_parameters_d = self.all_parameters.D - mul = truediv * all_parameters_d; truediv = all_parameters_d = None - mul_1 = sub * all_constants_sample_time; all_constants_sample_time = None - getitem_3 = kwargs['Sub13_int13'] + mul = truediv * all_parameters_d + truediv = all_parameters_d = None + mul_1 = sub * all_constants_sample_time + all_constants_sample_time = None + getitem_3 = kwargs["Sub13_int13"] relation_forward_sample_part17_w = self.all_constants.SamplePart17 - einsum_4 = torch.functional.einsum('bij,ki->bkj', getitem_3, relation_forward_sample_part17_w); getitem_3 = relation_forward_sample_part17_w = None - add = einsum_4 + mul_1; einsum_4 = mul_1 = None + einsum_4 = torch.functional.einsum( + "bij,ki->bkj", getitem_3, relation_forward_sample_part17_w + ) + getitem_3 = relation_forward_sample_part17_w = None + add = einsum_4 + mul_1 + einsum_4 = mul_1 = None all_parameters_i = self.all_parameters.I - mul_2 = add * all_parameters_i; all_parameters_i = None + mul_2 = add * all_parameters_i + all_parameters_i = None all_parameters_p = self.all_parameters.P - mul_3 = sub * all_parameters_p; sub = all_parameters_p = None - add_1 = mul_3 + mul_2; mul_3 = mul_2 = None - add_2 = add_1 + mul; add_1 = mul = None - getitem_4 = kwargs['F'] + mul_3 = sub * all_parameters_p + sub = all_parameters_p = None + add_1 = mul_3 + mul_2 + mul_3 = mul_2 = None + add_2 = add_1 + mul + add_1 = mul = None + getitem_4 = kwargs["F"] relation_forward_time_part4_w = self.all_constants.TimePart4 - einsum_5 = torch.functional.einsum('bij,ki->bkj', getitem_4, relation_forward_time_part4_w); getitem_4 = relation_forward_time_part4_w = None + einsum_5 = torch.functional.einsum( + "bij,ki->bkj", getitem_4, relation_forward_time_part4_w + ) + getitem_4 = relation_forward_time_part4_w = None size = einsum_5.size(0) relation_forward_fir5_weights = self.all_parameters.PFir5W size_1 = relation_forward_fir5_weights.size(1) - squeeze = einsum_5.squeeze(-1); einsum_5 = None - matmul = torch.matmul(squeeze, relation_forward_fir5_weights); squeeze = relation_forward_fir5_weights = None - to = matmul.to(dtype = torch.float32); matmul = None - view = to.view(size, 1, size_1); to = size = size_1 = None - getitem_5 = kwargs['x'] + squeeze = einsum_5.squeeze(-1) + einsum_5 = None + matmul = torch.matmul(squeeze, relation_forward_fir5_weights) + squeeze = relation_forward_fir5_weights = None + to = matmul.to(dtype=torch.float32) + matmul = None + view = to.view(size, 1, size_1) + to = size = size_1 = None + getitem_5 = kwargs["x"] relation_forward_time_part1_w = self.all_constants.TimePart1 - einsum_6 = torch.functional.einsum('bij,ki->bkj', getitem_5, relation_forward_time_part1_w); getitem_5 = relation_forward_time_part1_w = None + einsum_6 = torch.functional.einsum( + "bij,ki->bkj", getitem_5, relation_forward_time_part1_w + ) + getitem_5 = relation_forward_time_part1_w = None size_2 = einsum_6.size(0) relation_forward_fir2_weights = self.all_parameters.PFir3W size_3 = relation_forward_fir2_weights.size(1) - squeeze_1 = einsum_6.squeeze(-1); einsum_6 = None - matmul_1 = torch.matmul(squeeze_1, relation_forward_fir2_weights); squeeze_1 = relation_forward_fir2_weights = None - to_1 = matmul_1.to(dtype = torch.float32); matmul_1 = None - view_1 = to_1.view(size_2, 1, size_3); to_1 = size_2 = size_3 = None - add_3 = view_1 + view; view_1 = view = None - getitem_6 = kwargs['x_t']; kwargs = None + squeeze_1 = einsum_6.squeeze(-1) + einsum_6 = None + matmul_1 = torch.matmul(squeeze_1, relation_forward_fir2_weights) + squeeze_1 = relation_forward_fir2_weights = None + to_1 = matmul_1.to(dtype=torch.float32) + matmul_1 = None + view_1 = to_1.view(size_2, 1, size_3) + to_1 = size_2 = size_3 = None + add_3 = view_1 + view + view_1 = view = None + getitem_6 = kwargs["x_t"] + kwargs = None relation_forward_sample_part8_w = self.all_constants.SamplePart8 - einsum_7 = torch.functional.einsum('bij,ki->bkj', getitem_6, relation_forward_sample_part8_w); getitem_6 = relation_forward_sample_part8_w = None - return ({'F_PID': add_2, 'x_n': add_3}, {'SamplePart8': einsum_7, 'Add6': add_3}, {'Sub13_int13': add}, {'Sub13_int14': update_state}) - + einsum_7 = torch.functional.einsum( + "bij,ki->bkj", getitem_6, relation_forward_sample_part8_w + ) + getitem_6 = relation_forward_sample_part8_w = None + return ( + {"F_PID": add_2, "x_n": add_3}, + {"SamplePart8": einsum_7, "Add6": add_3}, + {"Sub13_int13": add}, + {"Sub13_int14": update_state}, + ) + + class RecurrentModel(torch.nn.Module): def __init__(self): super().__init__() self.Cell = TracerModel() - self.inputs = ['x_m', 'x_t', 'F', 'x', ] + self.inputs = [ + "x_m", + "x_t", + "F", + "x", + ] self.states = dict() def forward(self, kwargs): n_samples = min([kwargs[key].size(0) for key in self.inputs]) - self.states['Sub13_int14'] = kwargs['Sub13_int14'] - self.states['Sub13_int13'] = kwargs['Sub13_int13'] - results = {'F_PID':[], 'x_n':[], } + self.states["Sub13_int14"] = kwargs["Sub13_int14"] + self.states["Sub13_int13"] = kwargs["Sub13_int13"] + results = { + "F_PID": [], + "x_n": [], + } X = dict() for idx in range(n_samples): for key in self.inputs: @@ -113,7 +247,9 @@ def forward(self, kwargs): results[key].append(out[key]) for key, val in closed_loop.items(): self.states[key] = nnodely_basic_model_timeshift(self.states[key]) - self.states[key] = nnodely_basic_model_update_state(self.states[key], val) + self.states[key] = nnodely_basic_model_update_state( + self.states[key], val + ) for key, val in connect.items(): self.states[key] = nnodely_basic_model_timeshift(val) return results diff --git a/case-studies/mass_spring_damper/mass_spring_damper.py b/case-studies/mass_spring_damper/mass_spring_damper.py index 60122de7..7261091d 100644 --- a/case-studies/mass_spring_damper/mass_spring_damper.py +++ b/case-studies/mass_spring_damper/mass_spring_damper.py @@ -12,94 +12,117 @@ T_x = 0.1 # Define the neural model -x = Input('x') # MSNN input (Mass position) -F = Input('F') # MSNN input (Force) -x_free = Fir(W_init = 'init_negexp', W_init_params = {'first_value':0.1,'size_index':0,'lambda':3})(x.tw(T_x)) +x = Input("x") # MSNN input (Mass position) +F = Input("F") # MSNN input (Force) +x_free = Fir( + W_init="init_negexp", + W_init_params={"first_value": 0.1, "size_index": 0, "lambda": 3}, +)(x.tw(T_x)) x_force = Fir(F.tw(T_x)) -x_n = Output('x_n', x_free + x_force) +x_n = Output("x_n", x_free + x_force) # Add the neural models to the nnodely structure -msd = Modely(seed = 42, workspace = 'saved') -msd.addModel('neural_msd', x_n) +msd = Modely(seed=42, workspace="saved") +msd.addModel("neural_msd", x_n) # These functions are used to impose the minimization objectives. # Here it is minimized the error between the future position of x get from the dataset # and the estimator designed using the neural network. # The minimization is imposed via MSE error. -x_t = Input('x_t') # Real position -msd.addMinimize('x[t]', x_t.next(), x_n) +x_t = Input("x_t") # Real position +msd.addMinimize("x[t]", x_t.next(), x_n) # Nauralize the model and getting the neural network. # The sampling time depends on the datasets. msd.neuralizeModel(0.01) # Data load carica i file CSV e costruisce automaticamente il dataset compatibile con la struttura della rete. -data_struct = ['time', ('x','x_t'), '', 'F'] -msd.loadData(name = 'simulations', - source = 'msd-data/data', - format = data_struct, delimiter = ';') +data_struct = ["time", ("x", "x_t"), "", "F"] +msd.loadData( + name="simulations", source="msd-data/data", format=data_struct, delimiter=";" +) # Neural network train -default_par = {'num_of_epochs' : 80, - 'train_batch_size' : 128, - 'lr' : 0.0005, - 'splits' : [70,20,10]} -msd.trainModel(training_params = default_par) +default_par = { + "num_of_epochs": 80, + "train_batch_size": 128, + "lr": 0.0005, + "splits": [70, 20, 10], +} +msd.trainModel(training_params=default_par) # Save the neural model in json format -msd.saveModel(name = 'msd_preliminary') +msd.saveModel(name="msd_preliminary") # Show the network performance on the test dataset -msd.analyzeModel(splits = [70,20,10]) +msd.analyzeModel(splits=[70, 20, 10]) vis = MPLVisualizer() vis.setModely(msd) vis.showResult("simulations_test") # Refine weights with recurrent train and analyze the model showing the performance (mse, FVU, AIC). -msd.trainAndAnalyze(num_of_epochs = 10, prediction_samples = 1500, step = 500, lr=0.00001, - closed_loop={'x':'x_n'}, training_params = default_par) +msd.trainAndAnalyze( + num_of_epochs=10, + prediction_samples=1500, + step=500, + lr=0.00001, + closed_loop={"x": "x_n"}, + training_params=default_par, +) # Show the network performance on the test dataset on recurrent vis.showResult("simulations_test") # Save the neural model in json format -msd.saveModel(name = 'msd_final') +msd.saveModel(name="msd_final") # Definition of the PID controller network -x_m = Input('x_m') # measured position -kp = Parameter('P', values=0.5) -ki = Parameter('I', values=0.5) -kd = Parameter('D', values=0.5) -e = x_t.next()-x_m.next() -c = e*kp+Integrate(e)*ki+Differentiate(e)*kd -controlForce = Output('F_PID', c) -msd.addModel('PID',controlForce) +x_m = Input("x_m") # measured position +kp = Parameter("P", values=0.5) +ki = Parameter("I", values=0.5) +kd = Parameter("D", values=0.5) +e = x_t.next() - x_m.next() +c = e * kp + Integrate(e) * ki + Differentiate(e) * kd +controlForce = Output("F_PID", c) +msd.addModel("PID", controlForce) # Neuralization of the whole models msd.neuralizeModel() # Train the PID controller -msd.trainModel(models = 'PID', - closed_loop = {'x' : 'x_n','x_m' : 'x_n'}, - connect = {'F' : 'F_PID'}, - prediction_samples = 500, - step = 500, num_of_epochs = 20, - lr = 0.05, - training_params = default_par) +msd.trainModel( + models="PID", + closed_loop={"x": "x_n", "x_m": "x_n"}, + connect={"F": "F_PID"}, + prediction_samples=500, + step=500, + num_of_epochs=20, + lr=0.05, + training_params=default_par, +) # Print the parameter of the PID -print(f"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}") +print( + f"The PID controller kp = {msd.parameters['P']} ki = {msd.parameters['I']} kd = {msd.parameters['D']}" +) # Test the controller on step and triangular signal import numpy as np -tt = np.linspace(0, 5, int(5/0.01), endpoint=False) -data_target = np.concatenate([np.ones(511,dtype=np.float32)*1.0, # Step - -2*np.ones(500,dtype=np.float32)*1.0, - 2 * np.abs( 2 * (tt*1/5 - np.floor(tt*1/5 + 0.5)) ) - 1, - 2 * np.abs( 2 * (tt*1/2.5 - np.floor(tt*1/2.5 + 0.5)) ) - 1]) -msd.loadData('test_control', {'x_t': data_target}) -msd.analyzeModel('test_control', - prediction_samples = 2000, - closed_loop = {'x' : 'x_n','x_m' : 'x_n'}, - connect = {'F' : 'F_PID'}, - batch_size = 1) -vis.showResult('test_control') \ No newline at end of file + +tt = np.linspace(0, 5, int(5 / 0.01), endpoint=False) +data_target = np.concatenate( + [ + np.ones(511, dtype=np.float32) * 1.0, # Step + -2 * np.ones(500, dtype=np.float32) * 1.0, + 2 * np.abs(2 * (tt * 1 / 5 - np.floor(tt * 1 / 5 + 0.5))) - 1, + 2 * np.abs(2 * (tt * 1 / 2.5 - np.floor(tt * 1 / 2.5 + 0.5))) - 1, + ] +) +msd.loadData("test_control", {"x_t": data_target}) +msd.analyzeModel( + "test_control", + prediction_samples=2000, + closed_loop={"x": "x_n", "x_m": "x_n"}, + connect={"F": "F_PID"}, + batch_size=1, +) +vis.showResult("test_control") diff --git a/case-studies/neuralODE/neuralODE_msd.ipynb b/case-studies/neuralODE/neuralODE_msd.ipynb index 3743ea1b..0ad1497e 100644 --- a/case-studies/neuralODE/neuralODE_msd.ipynb +++ b/case-studies/neuralODE/neuralODE_msd.ipynb @@ -12,6 +12,7 @@ }, { "cell_type": "code", + "execution_count": 1, "id": "67c44ae2", "metadata": { "ExecuteTime": { @@ -19,6 +20,15 @@ "start_time": "2026-02-27T15:24:10.484610Z" } }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>-- nnodely_v1.5.2 --<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n" + ] + } + ], "source": [ "import os\n", "from nnodely import *\n", @@ -27,22 +37,16 @@ "from nnodely.support import earlystopping\n", "from nnodely.support.odeint.adjoint import odeint_adjoint\n", "\n", - "def init_random_range(indexes, params_size, dict_param={'min_value': 0.0, 'max_value': 1.0}):\n", + "\n", + "def init_random_range(\n", + " indexes, params_size, dict_param={\"min_value\": 0.0, \"max_value\": 1.0}\n", + "):\n", " import numpy as np\n", - " min_val = dict_param.get('min_value', 0.0)\n", - " max_val = dict_param.get('max_value', 1.0)\n", + "\n", + " min_val = dict_param.get(\"min_value\", 0.0)\n", + " max_val = dict_param.get(\"max_value\", 1.0)\n", " return np.random.uniform(low=min_val, high=max_val)" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>-- nnodely_v1.5.2 --<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n" - ] - } - ], - "execution_count": 1 + ] }, { "cell_type": "markdown", @@ -58,6 +62,7 @@ }, { "cell_type": "code", + "execution_count": 2, "id": "dcc7da2c", "metadata": { "ExecuteTime": { @@ -65,6 +70,7 @@ "start_time": "2026-02-27T15:24:11.102685Z" } }, + "outputs": [], "source": [ "def ode_func_torch(t, state, weight_fir):\n", " import torch\n", @@ -98,9 +104,7 @@ " out = torch.cat((state[:, 1:, :], out), dim=1)\n", "\n", " return out" - ], - "outputs": [], - "execution_count": 2 + ] }, { "cell_type": "markdown", @@ -117,6 +121,7 @@ }, { "cell_type": "code", + "execution_count": 3, "id": "616f6513", "metadata": { "ExecuteTime": { @@ -124,38 +129,14 @@ "start_time": "2026-02-27T15:24:11.275795Z" } }, - "source": [ - "model_name = 'neuralODE_msd'\n", - "msd = Modely(seed=42, workspace='saved')\n", - "\n", - "x = Input('x') # MSNN input (Mass position)\n", - "F = Input('F') # MSNN input (Force)\n", - "T_x = 0.1\n", - "init_value = 0.1\n", - "weight_fir = Parameter('weight_fir', dimensions=(2, 1, 10), init=init_random_range, init_params={'min_value': -init_value, 'max_value': init_value})\n", - "paramFun = ParamFun(ode_func_torch)\n", - "neuOde = NeuralODE(func=paramFun, dt=0.01, rtol=1e-7, atol=1e-9, method='dopri5')\n", - "state = Concatenate(x.tw(T_x), F.tw(T_x))\n", - "ans = neuOde(state, weight_fir)\n", - "x_est = TimePart(Select(ans, 0), 0.09, 0.1)\n", - "x_est.closedLoop(x)\n", - "\n", - "# Output and loss\n", - "x_n = Output('x_n', x_est)\n", - "\n", - "x_t = Input('x_t')\n", - "msd.addModel('msd', [x_n])\n", - "msd.addMinimize('x[t]', x_t.next(), x_n, loss_function='mse')\n", - "msd.neuralizeModel(sample_time = 0.01)" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n", - "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n", - "\u001B[32m{'Constants': {},\n", + "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n", + "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n", + "\u001b[32m{'Constants': {},\n", " 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n", " 'code': 'def FNeuralODE5(state, *weights):\\n'\n", " ' from '\n", @@ -288,12 +269,40 @@ " 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n", " 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n", " 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n", - " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n" + " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n" ] } ], - "execution_count": 3 + "source": [ + "model_name = \"neuralODE_msd\"\n", + "msd = Modely(seed=42, workspace=\"saved\")\n", + "\n", + "x = Input(\"x\") # MSNN input (Mass position)\n", + "F = Input(\"F\") # MSNN input (Force)\n", + "T_x = 0.1\n", + "init_value = 0.1\n", + "weight_fir = Parameter(\n", + " \"weight_fir\",\n", + " dimensions=(2, 1, 10),\n", + " init=init_random_range,\n", + " init_params={\"min_value\": -init_value, \"max_value\": init_value},\n", + ")\n", + "paramFun = ParamFun(ode_func_torch)\n", + "neuOde = NeuralODE(func=paramFun, dt=0.01, rtol=1e-7, atol=1e-9, method=\"dopri5\")\n", + "state = Concatenate(x.tw(T_x), F.tw(T_x))\n", + "ans = neuOde(state, weight_fir)\n", + "x_est = TimePart(Select(ans, 0), 0.09, 0.1)\n", + "x_est.closedLoop(x)\n", + "\n", + "# Output and loss\n", + "x_n = Output(\"x_n\", x_est)\n", + "\n", + "x_t = Input(\"x_t\")\n", + "msd.addModel(\"msd\", [x_n])\n", + "msd.addMinimize(\"x[t]\", x_t.next(), x_n, loss_function=\"mse\")\n", + "msd.neuralizeModel(sample_time=0.01)" + ] }, { "cell_type": "markdown", @@ -310,6 +319,7 @@ }, { "cell_type": "code", + "execution_count": 4, "id": "f0f3f7be", "metadata": { "ExecuteTime": { @@ -317,29 +327,26 @@ "start_time": "2026-02-27T15:24:13.191582Z" } }, - "source": [ - "data_struct = ['time', ('x_t', 'x'), '', 'F']\n", - "msd.loadData(name = 'simulations',\n", - " source = 'data',\n", - " format = data_struct, delimiter = ';')" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001B[1;32m============================ nnodely Model Dataset =============================\u001B[0m\n", - "\u001B[32mDataset Name: simulations\u001B[0m\n", - "\u001B[32mNumber of files: 100\u001B[0m\n", - "\u001B[32mTotal number of samples: 199100\u001B[0m\n", - "\u001B[32mShape of x: (199100, 10, 1)\u001B[0m\n", - "\u001B[32mShape of F: (199100, 10, 1)\u001B[0m\n", - "\u001B[32mShape of x_t: (199100, 1, 1)\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n" + "\u001b[1;32m============================ nnodely Model Dataset =============================\u001b[0m\n", + "\u001b[32mDataset Name: simulations\u001b[0m\n", + "\u001b[32mNumber of files: 100\u001b[0m\n", + "\u001b[32mTotal number of samples: 199100\u001b[0m\n", + "\u001b[32mShape of x: (199100, 10, 1)\u001b[0m\n", + "\u001b[32mShape of F: (199100, 10, 1)\u001b[0m\n", + "\u001b[32mShape of x_t: (199100, 1, 1)\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n" ] } ], - "execution_count": 4 + "source": [ + "data_struct = [\"time\", (\"x_t\", \"x\"), \"\", \"F\"]\n", + "msd.loadData(name=\"simulations\", source=\"data\", format=data_struct, delimiter=\";\")" + ] }, { "cell_type": "markdown", @@ -354,6 +361,7 @@ }, { "cell_type": "code", + "execution_count": 6, "id": "7fab61e7", "metadata": { "ExecuteTime": { @@ -361,36 +369,23 @@ "start_time": "2026-02-27T15:24:39.965159Z" } }, - "source": [ - "msd.exportPythonModel(name = model_name)\n", - "msd.importPythonModel(name = model_name)\n", - "msd.neuralizeModel(sample_time=0.01)\n", - "param_model = {'num_of_epochs' : 20,\n", - " 'train_batch_size' : 128,\n", - " 'lr' : 1e-3,\n", - " 'splits' : [70, 20, 10]}\n", - "msd.trainModel(training_params = param_model, minimize_gain=None)\n", - "\n", - "# Save the neural model\n", - "msd.exportPythonModel(name = model_name)" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001B[1;32m=============================== Save JSON Model ================================\u001B[0m\n", - "\u001B[32mModel saved in: saved/neuralODE_msd.json\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m========================== Export Python Torch Model ===========================\u001B[0m\n", - "\u001B[32mModel exported in: saved/neuralODE_msd.py\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m=============================== Load JSON Model ================================\u001B[0m\n", - "\u001B[32mModel loaded from: saved/neuralODE_msd.json\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n", - "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n", - "\u001B[32m{'Constants': {},\n", + "\u001b[1;32m=============================== Save JSON Model ================================\u001b[0m\n", + "\u001b[32mModel saved in: saved/neuralODE_msd.json\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m========================== Export Python Torch Model ===========================\u001b[0m\n", + "\u001b[32mModel exported in: saved/neuralODE_msd.py\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m=============================== Load JSON Model ================================\u001b[0m\n", + "\u001b[32mModel loaded from: saved/neuralODE_msd.json\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n", + "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n", + "\u001b[32m{'Constants': {},\n", " 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n", " 'code': 'def FNeuralODE5(state, *weights):\\n'\n", " ' from '\n", @@ -523,14 +518,14 @@ " 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n", " 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n", " 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n", - " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m========================== Import Python Torch Model ===========================\u001B[0m\n", - "\u001B[32mModel imported from: saved/neuralODE_msd.py\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n", - "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n", - "\u001B[32m{'Constants': {},\n", + " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m========================== Import Python Torch Model ===========================\u001b[0m\n", + "\u001b[32mModel imported from: saved/neuralODE_msd.py\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n", + "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n", + "\u001b[32m{'Constants': {},\n", " 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n", " 'code': 'def FNeuralODE5(state, *weights):\\n'\n", " ' from '\n", @@ -663,76 +658,90 @@ " 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n", " 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n", " 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n", - " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m======================== nnodely Model Train Parameters ========================\u001B[0m\n", - "\u001B[32mmodels: ['msd']\u001B[0m\n", - "\u001B[32mnum of epochs: 20\u001B[0m\n", - "\u001B[32mupdate per epochs: 1088\u001B[0m\n", - "\u001B[34mâ””>len(train_indexes)//(batch_size+step)\u001B[0m\n", - "\u001B[32mshuffle data: True\u001B[0m\n", - "\u001B[32mprediction samples: 0\u001B[0m\n", - "\u001B[32mstep: 0\u001B[0m\n", - "\u001B[32mclosed loop: {}\u001B[0m\n", - "\u001B[32mconnect: {}\u001B[0m\n", - "\u001B[32mtrain dataset: simulations_train\u001B[0m\n", - "\u001B[32m\t- batch size: 128\u001B[0m\n", - "\u001B[32m\t- num of samples: 139370\u001B[0m\n", - "\u001B[32m\t- num of first samples: 139370\u001B[0m\n", - "\u001B[32mvalidation dataset: simulations_val\u001B[0m\n", - "\u001B[32m\t- batch size: 128\u001B[0m\n", - "\u001B[32m\t- num of samples: 39820\u001B[0m\n", - "\u001B[32m\t- num of first samples: 39820\u001B[0m\n", - "\u001B[32mtest dataset: simulations_test\u001B[0m\n", - "\u001B[32m\t- num of samples: 19910\u001B[0m\n", - "\u001B[32m\t- num of first samples: 19910\u001B[0m\n", - "\u001B[32mminimizers: {'x[t]': {'A': 'SamplePart10',\n", + " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m======================== nnodely Model Train Parameters ========================\u001b[0m\n", + "\u001b[32mmodels: ['msd']\u001b[0m\n", + "\u001b[32mnum of epochs: 20\u001b[0m\n", + "\u001b[32mupdate per epochs: 1088\u001b[0m\n", + "\u001b[34mâ””>len(train_indexes)//(batch_size+step)\u001b[0m\n", + "\u001b[32mshuffle data: True\u001b[0m\n", + "\u001b[32mprediction samples: 0\u001b[0m\n", + "\u001b[32mstep: 0\u001b[0m\n", + "\u001b[32mclosed loop: {}\u001b[0m\n", + "\u001b[32mconnect: {}\u001b[0m\n", + "\u001b[32mtrain dataset: simulations_train\u001b[0m\n", + "\u001b[32m\t- batch size: 128\u001b[0m\n", + "\u001b[32m\t- num of samples: 139370\u001b[0m\n", + "\u001b[32m\t- num of first samples: 139370\u001b[0m\n", + "\u001b[32mvalidation dataset: simulations_val\u001b[0m\n", + "\u001b[32m\t- batch size: 128\u001b[0m\n", + "\u001b[32m\t- num of samples: 39820\u001b[0m\n", + "\u001b[32m\t- num of first samples: 39820\u001b[0m\n", + "\u001b[32mtest dataset: simulations_test\u001b[0m\n", + "\u001b[32m\t- num of samples: 19910\u001b[0m\n", + "\u001b[32m\t- num of first samples: 19910\u001b[0m\n", + "\u001b[32mminimizers: {'x[t]': {'A': 'SamplePart10',\n", " 'B': 'TimePart8',\n", - " 'loss': 'mse'}}\u001B[0m\n", - "\u001B[32moptimizer: Adam\u001B[0m\n", - "\u001B[32moptimizer defaults: {'lr': 0.001}\u001B[0m\n", - "\u001B[32moptimizer params: [{'params': 'weight_fir'}]\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m================= nnodely Training =================\u001B[0m\n", - "\u001B[32m| Epoch |\u001B[0m\u001B[32m x[t] |\u001B[0m\u001B[32m Total |\u001B[0m\n", - "\u001B[32m| |\u001B[0m\u001B[32m Loss |\u001B[0m\u001B[32m Loss |\u001B[0m\n", - "\u001B[32m| |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m val |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m val |\u001B[0m\n", - "\u001B[32m|--------------------------------------------------|\u001B[0m\n", - "\u001B[32m| 1/20 |\u001B[0m\u001B[32m2.946e-05|\u001B[0m\u001B[32m3.003e-05|\u001B[0m\u001B[32m2.946e-05|\u001B[0m\u001B[32m3.003e-05|\u001B[0m\n", - "\u001B[32m| 2/20 |\u001B[0m\u001B[32m2.333e-05|\u001B[0m\u001B[32m2.359e-05|\u001B[0m\u001B[32m2.333e-05|\u001B[0m\u001B[32m2.359e-05|\u001B[0m\n", - "\u001B[32m| 3/20 |\u001B[0m\u001B[32m1.811e-05|\u001B[0m\u001B[32m1.807e-05|\u001B[0m\u001B[32m1.811e-05|\u001B[0m\u001B[32m1.807e-05|\u001B[0m\n", - "\u001B[32m| 4/20 |\u001B[0m\u001B[32m1.361e-05|\u001B[0m\u001B[32m1.329e-05|\u001B[0m\u001B[32m1.361e-05|\u001B[0m\u001B[32m1.329e-05|\u001B[0m\n", - "\u001B[32m| 5/20 |\u001B[0m\u001B[32m9.822e-06|\u001B[0m\u001B[32m9.341e-06|\u001B[0m\u001B[32m9.822e-06|\u001B[0m\u001B[32m9.341e-06|\u001B[0m\n", - "\u001B[32m| 6/20 |\u001B[0m\u001B[32m6.726e-06|\u001B[0m\u001B[32m6.203e-06|\u001B[0m\u001B[32m6.726e-06|\u001B[0m\u001B[32m6.203e-06|\u001B[0m\n", - "\u001B[32m| 7/20 |\u001B[0m\u001B[32m4.319e-06|\u001B[0m\u001B[32m3.798e-06|\u001B[0m\u001B[32m4.319e-06|\u001B[0m\u001B[32m3.798e-06|\u001B[0m\n", - "\u001B[32m| 8/20 |\u001B[0m\u001B[32m2.513e-06|\u001B[0m\u001B[32m2.064e-06|\u001B[0m\u001B[32m2.513e-06|\u001B[0m\u001B[32m2.064e-06|\u001B[0m\n", - "\u001B[32m| 9/20 |\u001B[0m\u001B[32m1.273e-06|\u001B[0m\u001B[32m9.422e-07|\u001B[0m\u001B[32m1.273e-06|\u001B[0m\u001B[32m9.422e-07|\u001B[0m\n", - "\u001B[32m| 10/20 |\u001B[0m\u001B[32m5.204e-07|\u001B[0m\u001B[32m3.270e-07|\u001B[0m\u001B[32m5.204e-07|\u001B[0m\u001B[32m3.270e-07|\u001B[0m\n", - "\u001B[32m| 11/20 |\u001B[0m\u001B[32m1.508e-07|\u001B[0m\u001B[32m6.744e-08|\u001B[0m\u001B[32m1.508e-07|\u001B[0m\u001B[32m6.744e-08|\u001B[0m\n", - "\u001B[32m| 12/20 |\u001B[0m\u001B[32m2.458e-08|\u001B[0m\u001B[32m5.847e-09|\u001B[0m\u001B[32m2.458e-08|\u001B[0m\u001B[32m5.847e-09|\u001B[0m\n", - "\u001B[32m| 13/20 |\u001B[0m\u001B[32m1.724e-09|\u001B[0m\u001B[32m1.689e-10|\u001B[0m\u001B[32m1.724e-09|\u001B[0m\u001B[32m1.689e-10|\u001B[0m\n", - "\u001B[32m| 14/20 |\u001B[0m\u001B[32m1.613e-10|\u001B[0m\u001B[32m 2.83e-11|\u001B[0m\u001B[32m1.613e-10|\u001B[0m\u001B[32m 2.83e-11|\u001B[0m\n", - "\u001B[32m| 15/20 |\u001B[0m\u001B[32m4.815e-11|\u001B[0m\u001B[32m6.983e-12|\u001B[0m\u001B[32m4.815e-11|\u001B[0m\u001B[32m6.983e-12|\u001B[0m\n", - "\u001B[32m| 16/20 |\u001B[0m\u001B[32m1.415e-11|\u001B[0m\u001B[32m1.566e-12|\u001B[0m\u001B[32m1.415e-11|\u001B[0m\u001B[32m1.566e-12|\u001B[0m\n", - "\u001B[32m| 17/20 |\u001B[0m\u001B[32m1.537e-11|\u001B[0m\u001B[32m2.816e-12|\u001B[0m\u001B[32m1.537e-11|\u001B[0m\u001B[32m2.816e-12|\u001B[0m\n", - "\u001B[32m| 18/20 |\u001B[0m\u001B[32m2.932e-11|\u001B[0m\u001B[32m3.224e-12|\u001B[0m\u001B[32m2.932e-11|\u001B[0m\u001B[32m3.224e-12|\u001B[0m\n", - "\u001B[32m| 19/20 |\u001B[0m\u001B[32m2.718e-11|\u001B[0m\u001B[32m7.241e-11|\u001B[0m\u001B[32m2.718e-11|\u001B[0m\u001B[32m7.241e-11|\u001B[0m\n", - "\u001B[32m| 20/20 |\u001B[0m\u001B[32m2.717e-11|\u001B[0m\u001B[32m1.739e-12|\u001B[0m\u001B[32m2.717e-11|\u001B[0m\u001B[32m1.739e-12|\u001B[0m\n", - "\u001B[32m|--------------------------------------------------|\u001B[0m\n", - "\u001B[1;32m============================ nnodely Training Time =============================\u001B[0m\n", - "\u001B[32mTotal time of Training: 685.4026439189911\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[34mThe selected model is the LAST model of the training.\u001B[0m\n", - "\u001B[1;32m=============================== Save JSON Model ================================\u001B[0m\n", - "\u001B[32mModel saved in: saved/neuralODE_msd.json\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m========================== Export Python Torch Model ===========================\u001B[0m\n", - "\u001B[32mModel exported in: saved/neuralODE_msd.py\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n" + " 'loss': 'mse'}}\u001b[0m\n", + "\u001b[32moptimizer: Adam\u001b[0m\n", + "\u001b[32moptimizer defaults: {'lr': 0.001}\u001b[0m\n", + "\u001b[32moptimizer params: [{'params': 'weight_fir'}]\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m================= nnodely Training =================\u001b[0m\n", + "\u001b[32m| Epoch |\u001b[0m\u001b[32m x[t] |\u001b[0m\u001b[32m Total |\u001b[0m\n", + "\u001b[32m| |\u001b[0m\u001b[32m Loss |\u001b[0m\u001b[32m Loss |\u001b[0m\n", + "\u001b[32m| |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m val |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m val |\u001b[0m\n", + "\u001b[32m|--------------------------------------------------|\u001b[0m\n", + "\u001b[32m| 1/20 |\u001b[0m\u001b[32m2.946e-05|\u001b[0m\u001b[32m3.003e-05|\u001b[0m\u001b[32m2.946e-05|\u001b[0m\u001b[32m3.003e-05|\u001b[0m\n", + "\u001b[32m| 2/20 |\u001b[0m\u001b[32m2.333e-05|\u001b[0m\u001b[32m2.359e-05|\u001b[0m\u001b[32m2.333e-05|\u001b[0m\u001b[32m2.359e-05|\u001b[0m\n", + "\u001b[32m| 3/20 |\u001b[0m\u001b[32m1.811e-05|\u001b[0m\u001b[32m1.807e-05|\u001b[0m\u001b[32m1.811e-05|\u001b[0m\u001b[32m1.807e-05|\u001b[0m\n", + "\u001b[32m| 4/20 |\u001b[0m\u001b[32m1.361e-05|\u001b[0m\u001b[32m1.329e-05|\u001b[0m\u001b[32m1.361e-05|\u001b[0m\u001b[32m1.329e-05|\u001b[0m\n", + "\u001b[32m| 5/20 |\u001b[0m\u001b[32m9.822e-06|\u001b[0m\u001b[32m9.341e-06|\u001b[0m\u001b[32m9.822e-06|\u001b[0m\u001b[32m9.341e-06|\u001b[0m\n", + "\u001b[32m| 6/20 |\u001b[0m\u001b[32m6.726e-06|\u001b[0m\u001b[32m6.203e-06|\u001b[0m\u001b[32m6.726e-06|\u001b[0m\u001b[32m6.203e-06|\u001b[0m\n", + "\u001b[32m| 7/20 |\u001b[0m\u001b[32m4.319e-06|\u001b[0m\u001b[32m3.798e-06|\u001b[0m\u001b[32m4.319e-06|\u001b[0m\u001b[32m3.798e-06|\u001b[0m\n", + "\u001b[32m| 8/20 |\u001b[0m\u001b[32m2.513e-06|\u001b[0m\u001b[32m2.064e-06|\u001b[0m\u001b[32m2.513e-06|\u001b[0m\u001b[32m2.064e-06|\u001b[0m\n", + "\u001b[32m| 9/20 |\u001b[0m\u001b[32m1.273e-06|\u001b[0m\u001b[32m9.422e-07|\u001b[0m\u001b[32m1.273e-06|\u001b[0m\u001b[32m9.422e-07|\u001b[0m\n", + "\u001b[32m| 10/20 |\u001b[0m\u001b[32m5.204e-07|\u001b[0m\u001b[32m3.270e-07|\u001b[0m\u001b[32m5.204e-07|\u001b[0m\u001b[32m3.270e-07|\u001b[0m\n", + "\u001b[32m| 11/20 |\u001b[0m\u001b[32m1.508e-07|\u001b[0m\u001b[32m6.744e-08|\u001b[0m\u001b[32m1.508e-07|\u001b[0m\u001b[32m6.744e-08|\u001b[0m\n", + "\u001b[32m| 12/20 |\u001b[0m\u001b[32m2.458e-08|\u001b[0m\u001b[32m5.847e-09|\u001b[0m\u001b[32m2.458e-08|\u001b[0m\u001b[32m5.847e-09|\u001b[0m\n", + "\u001b[32m| 13/20 |\u001b[0m\u001b[32m1.724e-09|\u001b[0m\u001b[32m1.689e-10|\u001b[0m\u001b[32m1.724e-09|\u001b[0m\u001b[32m1.689e-10|\u001b[0m\n", + "\u001b[32m| 14/20 |\u001b[0m\u001b[32m1.613e-10|\u001b[0m\u001b[32m 2.83e-11|\u001b[0m\u001b[32m1.613e-10|\u001b[0m\u001b[32m 2.83e-11|\u001b[0m\n", + "\u001b[32m| 15/20 |\u001b[0m\u001b[32m4.815e-11|\u001b[0m\u001b[32m6.983e-12|\u001b[0m\u001b[32m4.815e-11|\u001b[0m\u001b[32m6.983e-12|\u001b[0m\n", + "\u001b[32m| 16/20 |\u001b[0m\u001b[32m1.415e-11|\u001b[0m\u001b[32m1.566e-12|\u001b[0m\u001b[32m1.415e-11|\u001b[0m\u001b[32m1.566e-12|\u001b[0m\n", + "\u001b[32m| 17/20 |\u001b[0m\u001b[32m1.537e-11|\u001b[0m\u001b[32m2.816e-12|\u001b[0m\u001b[32m1.537e-11|\u001b[0m\u001b[32m2.816e-12|\u001b[0m\n", + "\u001b[32m| 18/20 |\u001b[0m\u001b[32m2.932e-11|\u001b[0m\u001b[32m3.224e-12|\u001b[0m\u001b[32m2.932e-11|\u001b[0m\u001b[32m3.224e-12|\u001b[0m\n", + "\u001b[32m| 19/20 |\u001b[0m\u001b[32m2.718e-11|\u001b[0m\u001b[32m7.241e-11|\u001b[0m\u001b[32m2.718e-11|\u001b[0m\u001b[32m7.241e-11|\u001b[0m\n", + "\u001b[32m| 20/20 |\u001b[0m\u001b[32m2.717e-11|\u001b[0m\u001b[32m1.739e-12|\u001b[0m\u001b[32m2.717e-11|\u001b[0m\u001b[32m1.739e-12|\u001b[0m\n", + "\u001b[32m|--------------------------------------------------|\u001b[0m\n", + "\u001b[1;32m============================ nnodely Training Time =============================\u001b[0m\n", + "\u001b[32mTotal time of Training: 685.4026439189911\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[34mThe selected model is the LAST model of the training.\u001b[0m\n", + "\u001b[1;32m=============================== Save JSON Model ================================\u001b[0m\n", + "\u001b[32mModel saved in: saved/neuralODE_msd.json\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m========================== Export Python Torch Model ===========================\u001b[0m\n", + "\u001b[32mModel exported in: saved/neuralODE_msd.py\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n" ] } ], - "execution_count": 6 + "source": [ + "msd.exportPythonModel(name=model_name)\n", + "msd.importPythonModel(name=model_name)\n", + "msd.neuralizeModel(sample_time=0.01)\n", + "param_model = {\n", + " \"num_of_epochs\": 20,\n", + " \"train_batch_size\": 128,\n", + " \"lr\": 1e-3,\n", + " \"splits\": [70, 20, 10],\n", + "}\n", + "msd.trainModel(training_params=param_model, minimize_gain=None)\n", + "\n", + "# Save the neural model\n", + "msd.exportPythonModel(name=model_name)" + ] }, { "cell_type": "markdown", @@ -744,6 +753,7 @@ }, { "cell_type": "code", + "execution_count": 7, "id": "5dd1d858", "metadata": { "ExecuteTime": { @@ -751,47 +761,17 @@ "start_time": "2026-02-27T15:39:15.286428Z" } }, - "source": [ - "# Recursive plot using getSamples\n", - "msd.importPythonModel(name = model_name)\n", - "msd.neuralizeModel(sample_time=0.01)\n", - "samples = msd.getSamples(dataset='simulations', window=2000-10, index=0)\n", - "result = msd(samples, sampled=True, prediction_samples=2000-10)\n", - "\n", - "#region Plotting\n", - "t = np.arange(len(result['x_n'])) * 0.01\n", - "plt.figure(figsize=(12, 7))\n", - "plt.subplot(2, 1, 1)\n", - "plt.plot(t, result['x_n'], label=r'Estimated $x$', linewidth=2)\n", - "plt.plot(t, np.array(samples['x_t']).squeeze(-1).squeeze(-1), label=r'True $x$', linestyle='--', linewidth=2)\n", - "plt.title('Position estimation')\n", - "plt.xlabel('Time [s]')\n", - "plt.ylabel(r'$x$ [m]')\n", - "plt.grid()\n", - "plt.legend()\n", - "plt.subplot(2, 1, 2)\n", - "plt.plot(t, np.array(samples['F'])[:, 0, 0], label=r'Applied force $F$')\n", - "plt.title('Applied force')\n", - "plt.xlabel('Time [s]')\n", - "plt.ylabel('Force [N]')\n", - "plt.grid()\n", - "plt.legend()\n", - "plt.tight_layout()\n", - "#endregion\n", - "\n", - "plt.show()" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001B[1;32m=============================== Load JSON Model ================================\u001B[0m\n", - "\u001B[32mModel loaded from: saved/neuralODE_msd.json\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n", - "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n", - "\u001B[32m{'Constants': {},\n", + "\u001b[1;32m=============================== Load JSON Model ================================\u001b[0m\n", + "\u001b[32mModel loaded from: saved/neuralODE_msd.json\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n", + "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n", + "\u001b[32m{'Constants': {},\n", " 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n", " 'code': 'def FNeuralODE5(state, *weights):\\n'\n", " ' from '\n", @@ -924,14 +904,14 @@ " 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n", " 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n", " 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n", - " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m========================== Import Python Torch Model ===========================\u001B[0m\n", - "\u001B[32mModel imported from: saved/neuralODE_msd.py\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[33m[neuralizeModel] Closed loop on x with sample in the future.\u001B[0m\n", - "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n", - "\u001B[32m{'Constants': {},\n", + " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m========================== Import Python Torch Model ===========================\u001b[0m\n", + "\u001b[32mModel imported from: saved/neuralODE_msd.py\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[33m[neuralizeModel] Closed loop on x with sample in the future.\u001b[0m\n", + "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n", + "\u001b[32m{'Constants': {},\n", " 'Functions': {'FNeuralODE5': {'atol': 1e-09,\n", " 'code': 'def FNeuralODE5(state, *weights):\\n'\n", " ' from '\n", @@ -1064,33 +1044,68 @@ " 'Select7': ['Select', ['NeuralODE5'], 2, 0],\n", " 'TimePart1': ['TimePart', ['x'], -1, [-0.1, 0]],\n", " 'TimePart3': ['TimePart', ['F'], -1, [-0.1, 0]],\n", - " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n" + " 'TimePart8': ['TimePart', ['Select7'], 0.1, [0.09, 0.1]]}}\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n" ] }, { "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAKyCAYAAAAEvm1SAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3XV0FFcbx/HvbjwhQRIgaAjuGqy4e3F3KS1eKLRQtFhxKVCgUCAUL+5Wgru7B4dAIO7JzvvHwlJeKJrkZpPnc849nZnM7Pw2Cdvss1d0gIYQQgghhBBCCCGEEPFIrzqAEEIIIYQQQgghhEh6pCglhBBCCCGEEEIIIeKdFKWEEEIIIYQQQgghRLyTopQQQgghhBBCCCGEiHdSlBJCCCGEEEIIIYQQ8U6KUkIIIYQQQgghhBAi3klRSgghhBBCCCGEEELEOylKCSGEEEIIIYQQQoh4J0UpIYQQQgghhBBCCBHvpCglhBBCiATPy8sLTdPeaBEREdy9e5eZM2eSIkWKWL1fhQoV0DSNChUqAJAhQwY2b96Mm5ub6Rxvb28WLlwYq/eNS6qfQ7169fD09DTt///3WAghhBBJjw7QVIcQQgghhHgfLy8vnJyc6N69u+mYtbU1xYoVY+zYsZw5c4YyZcrE2v0cHR3Jmzcvly9fJigoiPbt27No0SKyZMnC3bt3AShcuDCBgYHcvn071u4bl1Q/By8vLwAqVaoEvP09FkIIIUTSY6k6gBBCCCHExwgMDOTYsWNvHDtw4ADJkiVj1KhRlCxZ8q2vf66goKAPPtbZs2dj5V4qqXwOH/M9FkIIIUTiJsP3hBBCCGHWTp48CfDGsLRmzZpx4sQJgoKCePz4MbNnz35jiJ+trS2zZs3i/v37hIeHc+XKFX744QfT1/89tOxVDyOAO3fumIa7/f/QNycnJyZPnszNmzcJCwvjwoULdOzY8Y2s3t7ejBgxggkTJvDkyRNCQ0PZtm0b2bNnf+9z1Ol0/PTTT9y4cYPw8HCuXbtGz5493zgna9asbNiwAV9fX0JCQjh8+DC1atUC+Kjn4ObmhqZpNG7cmHXr1hEcHMyTJ08YPHgwjo6OzJ8/H39/f548ecK4cePeuLebmxuenp48fPiQyMhIfHx88PT0JFWqVICxl1TFihWpWLGi6fv6ruF7xYoVY9u2bfj6+hIQEMDGjRvJmzfvWz+XypUrs2PHDkJCQnj8+DHjxo1Dr5c/a4UQQghzpEmTJk2aNGnSpCXk5uXlpXl5eb3za3369NE0TdOKFSumAdrgwYO1mJgYbcaMGVr16tW17777Tnv27Jl29uxZzdbWVgO0OXPmaLdv39aaN2+uVahQQRs3bpymaZrWoUMHDdAqVKigaZqmVahQQXNxcdFGjhypaZqmNWjQQMuaNasGaN7e3trChQs1QLO1tdXOnz+vPXnyROvatatWvXp17ffff9c0TdMGDRpkyurt7a35+flpmzZt0mrWrKm1atVKe/bsmXb48OH3Pv85c+ZoERER2vDhw7Vq1appo0eP1qKjo7UhQ4ZogKbT6bTLly9ru3fv1mrVqqVVrVpV27RpkxYVFaVly5bto56Dm5ubpmma5ufnp40cOVKrVKmStmTJEk3TNO3KlSvajBkztMqVK2uzZs3SNE3TmjRpogGanZ2d5u3trR0/flxr0KCBVrFiRe3nn3/WIiMjtdmzZ2uAlidPHu3UqVPaqVOntJIlS2qOjo5vfI8BrWLFilpERIS2fft2rV69elrTpk21M2fOaP7+/lquXLne+Lk8fvxYGzJkiFapUiVt8uTJmqZpWteuXZX/nkqTJk2aNGnSPrkpDyBNmjRp0qRJk/be5uXlpe3du1ezsLAwtdSpU2tNmjTRnj17ph06dEgDtBQpUmhhYWGmYsirVrZsWU3TNK1bt24aoF25ckWbO3fuG+cMGTJEq127tga8VTBp3769pmma5ubmZjr/3wWd7777TtM0TStVqtQbjzlv3jwtNDRUS5kypema27dva3q93nTO0KFDNU3TtFSpUr3zuefIkUOLiYnRfvzxxzeOjxw5UgsNDdVSpUqlpU2bVtM0TWvZsqXp605OTtrkyZO1vHnzftRzeFWUWr58uenradKk0TRN0/bt2/fGvf39/bWpU6dqgFaoUCFt//79mru7+xvnbNiwQbty5cobP8N/Fxb//3t89OhR7eLFi298b5InT675+vpqK1eufOOakSNHvnGvW7duaRs3blT+eypNmjRp0qRJ+7Qm/ZyFEEIIYRYqVKhAdHS0qT19+pQVK1Zw6tQpWrZsCUCpUqWwtbVl+fLlb1x78OBB7ty5Q8WKFQHjcLKuXbuyZcsWevToQZYsWRg9ejRbt279rGwVK1bE29ubo0ePvnF8yZIl2NnZUapUKdOxEydOYDAYTPsPHjwAwMHB4Z2PXblyZfR6PZs2bcLCwsLUNm7ciJ2dHeXKlcPHx4dLly4xb948Fi1aRMuWLdHr9fzwww9cvnz5k57L4cOHTdtPnz4FeGvuJz8/P9NwyHPnzlG+fHnu3LlD9uzZqVWrFj/88AN58uTBxsbmo+5pb29P8eLFWbVq1Rvfm4CAADZt2mT6ub1y5MiRN/YfPHjwn98/IYQQQiRcUpQSQgghhFk4deoUHh4eeHh4UKxYMfLly0eKFCmoWbMm9+7dAzDNYfTkyZO3rn/y5ImpkPL9998zePBg3N3dmTlzJt7e3hw6dIiCBQt+VrZUqVL95z2BN+azCg0NfeOcV0WY/5oTydnZGYDLly+/UZQ7ceIEAOnTpwegWrVqeHp6UqNGDZYtW4aPjw8rVqx4494fIzAw8K1jISEh772mb9++PH36lBs3brBgwQIqVqxISEgIOp3uo+6ZIkUK9Hr9B39ur7zreyhzSgkhhBDmR1bfE0IIIYRZCAoK4tSpU+8958WLFwC4urpy/fr1N76WLl06bt++DUBkZCRjx45l7NixZMqUiXr16jF06FCWLVtG/vz5Pznbixcv3jlZebp06QDw9fX95Md8xd/fH4BKlSoRFBT01tdfFeQeP35Mjx496NGjB4UKFaJJkyYMHDgQX1/ftyZFj00tW7ZkypQpDBgwgIULF/L8+XMAVq5cSYkSJT7qMfz9/TEYDLi6ur71tXTp0n3R908IIYQQCZd8pCSEEEKIROPYsWOEh4ebhvO9UrZsWdzc3Dh48CC2trZcu3aNfv36AXD//n1+//13li9f/sYKfv8WExPz3vvu27cPd3f3N4bpAbRp04aIiAiOHz/+2c9p//79ALi4uHDq1ClTS506NaNGjcLZ2ZlSpUrx5MkTPDw8AOOQuqFDh3LhwgXTc/rQc/hcZcuWxc/Pj0mTJpkKUg4ODpQtW/aN3kvvu39oaCgnT56kWbNmb1zj5ORE3bp1OXjwYJxkF0IIIYRa0lNKCCGEEImGn58f48aNY9iwYURFRbFp0ybc3d0ZNWoUly5dwtPTk/DwcE6dOsXw4cOJjIzk/Pnz5MqViw4dOrB69ep3Pu6r3kqNGjVi69atXLt27Y2vL1q0iB49erB+/XqGDRuGt7c3X3/9NZ07d2bEiBEEBAR89nO6ePEif/31F/PmzSNLliycPHmSXLlyMXbsWLy9vbl+/TpWVlaEhoby119/MWLECJ48eULVqlUpUqQI06dP/6jn8LmOHz9O9+7dmTRpEps2bSJ9+vQMGDAAV1dX/Pz8TOf5+/tTunRpKlWqxJkzZ956nEGDBrFjxw62bt3KrFmzsLa2ZtCgQdjY2DBy5MhYySqEEEKIhEf5bOvSpEmTJk2aNGnva/+/ctuH2rfffqtdvHhRCw8P1x4+fKjNnDlTS5EihenryZIl06ZNm6bduXNHCw8P1+7du6dNmDBBs7W11eDtleEcHBy0nTt3auHh4drmzZs1eHPlOkBzdnbW5s2bp/n4+GhhYWHamTNntI4dO76R6/+vgXevivf/zcLCQhsyZIh28+ZNLSIiQrt37542a9Ys06p+gJY9e3Zt9erV2pMnT7Tw8HDtwoUL2jfffGP6+oeew6vV99q3b//GvTVN04YPH/7e5zFixAjt3r17WmhoqHbjxg1t2rRpWpcuXTRN07TcuXNrgFaxYkXT97tly5ZvfY9ffd/37dunhYSEaC9evNDWr19vWj3wXT+Xz/39kCZNmjRp0qQljKZ7uSGEEEIIIYQQQgghRLyROaWEEEIIIYQQQgghRLyTopQQQgghhBBCCCGEiHdSlBJCCCGEEEIIIYQQ8c6silI2NjbMnz8fPz8/Hj16ZFrK+X3c3NwICgqiQoUK8ZBQCCGEEEIIIYQQQnwMS9UBPsXEiRPx8PCgcuXKuLm54enpyd27d1mzZs1/XjN79mySJUsWjymFEEIIIYQQQgghxIeYTU8pe3t7unTpQp8+fThz5gzr169nwoQJ9OzZ8z+vadWqFY6OjvGYUgghhBBCCCGEEEJ8DLPpKVWoUCGsrKw4fPiw6djBgwcZPHgwOp0OTdPeOD9VqlRMmDCB6tWrc+nSpc+6Z/r06QkKCvqi3EIIIYQQQgghhBBJjaOjI48ePXrvOWZTlEqXLh2+vr5ERUWZjvn4+GBnZ4ezszO+vr5vnD9lyhQ8PT25fPnyZ90vffr0PHz48IsyCyGEEEIIIYQQQiRVGTJkeG9hymyKUvb29kRERLxx7NW+jY3NG8erVKlC2bJlyZ8//2ff71UPqQwZMkhvKSGEEEIIIYQQQoiP5OjoyMOHDz9YTzGbolR4ePhbxadX+6GhoaZjtra2zJ07l+7duxMeHv7F9w0KCpKilBBCCCGEEEIIIUQsM5uJzh8+fIiLiwsWFhamY66uroSGhuLv7286VqJECbJly8aaNWveKCht27aN2bNnx3dsIYQQQgghhBBCCPEOZtNT6uzZs0RFRVGqVCkOHToEQNmyZTlx4sQbk5wfP36c7Nmzv3HtzZs36dKlC7t27YrXzEIIIYQQQgghhBDi3cymKBUWFoanpydz5syhY8eOZMiQgf79+9OxY0cA0qZNS0BAAOHh4dy6deut6x8+fMizZ8/iO7YQQgghhBBCCCGEeAezKUoB9OvXj9mzZ+Pl5UVAQADDhw9n3bp1ADx58oQOHTrg6empOKUQQgghhBBCCJE02dvb4+Ligk6nUx1FxBFN0/D19X1jfu/PpQO0D56VBDk6OhIYGIiTk5NMdC6EEEIIIYQQQryHTqejY8eOVKxYUXUUEU/27t3LwoUL35hS6ZWPramYVU8pIYQQQgghhBBCJDwdO3akQoUKrFy5kqtXrxIdHa06kogjlpaW5M6dm2bNmgGwYMGCz3+s2AolhBBCCCGEEEKIpMfBwYGKFSuycuVKtmzZojqOiAev5vJu3rw5K1as+OyhfPrYDCWEEOZGp9djbWenOoYQQgghhBBmy9nZGYCrV68qTiLi06uft4uLy2c/hvSUEkIkOY7OqWjUvQ21vy5FLndnbC0hNFJjhdd9jmzcxYkNWwgPDlEdUwghhBBCCLPwalJzGbKXtLz6eX/JpPZSlBJCJBl6Sws6D+/NDz2qkzPlq/9hRgIQEaPDNW8+GuTNT43uXdg8dRbH1mx856R9QgghhBBCCCG+nAzfE0IkCSnSpuHvY38x6+fK/ypIvXbneTQaxgq/nZMjTYcP5JtZE2VonxBCCCGEEELEESlKCSESvXTZ3dl9ah4Nizpi+fJV70VgBKPHraVUyR9wz9KZKkU6MaF+S05u3AaAjd7A6I55WX10IQ4pkitML4QQQgghhIhL3t7eaJr2zlahQoUPXl+oUCFKly4NgJubG5qm4ebmFus5/32fTzV8+HC8vLxiOdGXk6KUECJRc8mUnl2HfqNohtcvd38s2kfGtK0YNmghx49f5+7dpzx69AKf23dYPngkC3v+QO30frjYxlC3oAN/75+Djb29wmchhBBCCCGEiEt9+vTB1dX1rXb48OEPXrtu3Tpy5swJwP3793F1deX+/fuxnvHf90kspCglhEi07JM70XnWFAIxDsGLjtFo23EG33WcRHh45H9ed3HfYZbP32jar1kgGX9un4FOLy+ZQgghhBBCJEYBAQH4+Pi81aKioj547b8n+jYYDPj4+GAwGGI945dMKJ5QyTssIUSipNPpaDVuBGnc3Tjy1IG15yJo3HQ8Sxft/KjrBw/4g+Fj1pn2W5RNQ9+J/eMqrhBCCCGEECIB69WrF3fu3CEsLIwTJ05QpkwZALy8vMiSJQuLFi1i4cKFbwzfe7Vdu3ZtvL29CQoKYtq0aeTLl48TJ04QHBzMpk2bSJYsGQBfffUVBw4cICQkhODgYLZs2YKrq+s77wOQMWNGNmzYQEhICN7e3gwbNgz9yw/S8+TJY3qsf/75BxcXl/c+vxYtWhAaGmq6H8CCBQs4d+4cTk5Osf79/DdN2tvN0dFR0zRNc3R0VJ5FmjRpn96qdGmvTb5wRJt84Yg2Yu8WLWV61896nLmeP2kGbZNm0DZpgZGbtVK1Kip/btKkSZMmTZo0adKkJaTm5uamLV68WHNzc1Oe5XOat7e31r59+//8euHChbXw8HCtdu3ampubmzZlyhTt0aNHmk6n01KmTKndu3dP6927t+bk5KS5ublpmqZpbm5upu39+/drBQoU0Fq0aKFpmqZdv35dq1q1qvbVV19pvr6+2vfff685OTlpfn5+2tChQ7UsWbJoX331lXbt2jVt+vTpGvDWfQDt+PHj2rx587ScOXNqFSpU0K5evaoNGTJEs7a21m7fvq15enpquXLl0rp166ZFRkZqXl5e7/0+nD17Vvvtt980QBsxYoR27949LX369J/1c//YmoolQgiRyOQqVoCmfTsSYjB2n106cAR+j5581mN16ziRYsVzUDRPWpJZacxf2JdiWY8TERoay6mFEEIIIYRIXApWr0zNHt9g4xB/87NGhISyfeYfnN/l9UnXzZkzh5kzZ75x7O7du+TPn58sWbKgaRp3797l7t27DBkyhM2bN6PX6/Hz8yMmJoaAgAACAwNJmTLlW489atQoLly4wIULF5g2bRrLly9n9+7dAOzevZvcuXNjZ2fHqFGjmDJlCgB37txhzZo1lChRAuCt+1SuXBk3NzdKliyJpmlcv36d/v37s2jRIk6fPo2zszPdunUjNDSUa9euUbFiRdKkSfPe78HgwYNZvXo1T548oVevXpQrV45Hjx590vfxU0lRSgiRqOgtLViwchjFsgRxyMfApHEruHH0xGc/nsFgoE6lAVz3XoijnQV50+oZu2A4P7T4KRZTCyGEEEIIkfhU6tCatFmzxPt9K3Zo/clFqWHDhrF27do3jr2aT2rHjh1cuHCBixcvcvr0aTZs2MC8efOIiYn5qMe+ffu2aTssLIw7d+68sW9jY4OPjw+enp707duXwoULkzdvXgoVKsShQ4fe+Zh58uTB2dmZwMBA0zG9Xo+9vT358+fnxo0bhP7rg/QTJ05Qp06d9+bcsmULly9fZtiwYVSvXp3Lly9/1PP7ElKUEkIkKn3G9qF0NlsAvkoTzKWNG774MX18/Oj9/XwWzv0WgG8b52N+8YJcOXH+ix9bCCGEEEKIxMpr4RJq9uwa7z2l9i5a+snXPX36lFu3br3za2FhYZQsWZIKFSpQr149OnbsSLdu3ShWrNhH9SSKjo5+Y/9dk6CnT5+ekydPcurUKXbt2sW8efOoU6cOpUqVeudjWlpacvXqVerXr//W1xo2bPjWpOiRkf+90NMrNWrUIHfu3FhYWODj4/PB82ODFKWEEImGa5aMDOlVGePwZBgzfgP37sbOi6nnH5vp0KUWFYpn5mGIFV/368aVlt1i5bGFEEIIIYRIjM7v8vrkHksJUalSpahcuTJjx45l7969DBo0CB8fH8qWLcuqVavQNO2L79GwYUNevHhBvXr1TMd69er1RnHp3/e5du0amTNn5tmzZ6beUlWrVqVDhw4sW7aMnDlz4uTkZPpakSJF3nv/IkWKsGrVKjp37kyHDh0YNWoUzZo1++Ln9SGy+p4QItGYsnAIKW2NL9SXvf0YM3RBrD5+h2YjmXckgs33nUiTvzAFqlSI1ccXQgghhBBCqJE8eXLSpk37VrO3tycsLIzhw4fTuXNn3NzcaNGiBcmSJeP8eePIiZCQEHLnzv3O+aQ+1vPnz8mcOTOVK1fG3d2dH3/8kcaNG2NjY2M659/32blzJ3fv3mXJkiXkz5+fsmXL8scffxAaGsrOnTu5d+8ef/75J7lz56Z9+/Y0b978P+/t5ubGli1bGDt2LCtWrGDYsGE0btz4g4Ws2KJ8pvuE2GT1PWnSzKsVrVhSC4verBm0TVq0YZPmUTJPnNwnX8WyplX9Bm39W9NbWih/7tKkSZMmTZo0adKkqWyJYfW9/zJ48GAN0Fq3bq1dvXpVCwsL065evao1b97cdH23bt20oKAgbc2aNe9cfe/f35f/X+lv4cKF2sKFCzW9Xq/9/vvv2osXL7Tnz59r27dv1/r06aP5+/tr1tbWb90H0Nzd3bXNmzdrISEhmo+PjzZz5kzN1tZWA7QsWbJou3fv1kJDQ7WjR49qEydOfOfqeylTptSuXLmizZ49+43jmzdv1rZt2/bZP/dPqKmo/wVIiE2KUtKkmVc7cH2FZtA2aQZtk7Zu16Q4vdd382aYClMlGtZT/tylSZMmTZo0adKkSVPZzL0oJS32f+4fW1OR4XtCCLNXs1VdSmd3ACAiSqNbu7Fxer9tM+eiQyNn8gimTu2E3tIiTu8nhBBCCCGEEImRFKWEEGbvlzHt0b+c/++Pvw7y5PGLOL3f3XMXqep0jzqZgijtZknbvu3i9H5CCCGEEEIIkRhJUUoIYdaKVa9Ivox2AIREGBjcZ0a83Hfpop2m7YH966O3kN5SQgghhBBCCPEppCglhDBrZTu2ZeH1lBzysee3P/YQHBwWL/edMnoxz4OiAciVxoL6HRvGy32FEEIIIYQQIrGQopQQwmzlLleazPnzEmnQs+7AAwb3nh5v946Kiub3ebtN+wN+ahJv9xZCCCGEEEKIxMCsilI2NjbMnz8fPz8/Hj16RL9+/f7z3FatWnHt2jVCQ0M5dOgQxYsXj8ekQoj4ULlzW9P2rjkL4v3+E4b9SWikBoBHVgeKlJfXGSGEEEIIIYT4WGZVlJo4cSIeHh5UrlyZ7t27M3z4cBo3bvzWeWXLluXPP/9k5MiR5MuXj8OHD7Nt2zYcHBwUpBZCxIV8pYqQt3ghAHxu3+Hinv3xniEkJJy1W88DYKmHwWO6xHsGIYQQQgghhDBXZlOUsre3p0uXLvTp04czZ86wfv16JkyYQM+ePd8619XVlVGjRrF06VK8vb0ZOXIkzs7O5M2bV0FyIURcGDW5G11yvaBiumAubFyPpmlKcgzr/zuGl7euWTIDzunTKskhhBBCCCGEEObGbIpShQoVwsrKisOHD5uOHTx4kJIlS6LT6d44d/Xq1YwdOxYAW1tb+vbti4+PD5cvX47XzEKIuJEuS0aql0iPtQUUTBnOZa/47yX1yp1bjzhw+iEA9lYa3w+X3lJCCCGEEEII8THMpiiVLl06fH19iYqKMh3z8fHBzs4OZ2fnd15TuXJlgoODGT58ON9//z0hISHxFVcIEYd+ntADe0tj96R9J+9z9/ZjpXkm/7rctF2+msdbhXIhhBBCCCGEEG8zm6KUvb09ERERbxx7tW9jY/POay5evEixYsUYNmwYixYtomTJknGeUwgRt2zs7WlRp4Bpf/jAPxSmMdq8Zh9rj/qy8HpKToa4kqOUTHguhBBCCCGEOVi4cCGapv1nq1ChguqIiZrZFKXCw8PfKj692g8NDX3nNU+fPuXcuXOMGTOGXbt28d1338V5TiFE3Oo6sD3O9sZeUpfuBHDI66zaQC/9MmQR/pEWAJRqUl9xGiGEEEIIIcTH6NOnD66urri6utKnTx/u379v2nd1dX1jCiER+yxVB/hYDx8+xMXFBQsLC2JiYgDjhOahoaH4+/u/ca6HhwcxMTGcOXPGdOzy5csy0bkQiUDnjlVM29OnrlOY5E2X9h0k0Pc5Ti7O5K9UnmSpUhL8wk91LCGEEEIIIcR7BAYGEhgYCEBAQAAxMTH4+PgoTpV0mE1PqbNnzxIVFUWpUqVMx8qWLcuJEyfeWnWrc+fO/Prrr28cK1asGFeuXImXrEKIuOFRqST5Mxh7SPqHxrDo9/VqA/2LITqGkxu2AGBhZUHZRrUUJxJCCCGEEEJ8KTc3NzRNY8iQIbx48YIZM2aYjrm5uZnOGz58OF5eXgBkzJiRDRs2EBISgre3N8OGDUOv/+/yS4sWLQgNDcXV1dV0bMGCBZw7dw4nJ6e4e3IJgNkUpcLCwvD09GTOnDl4eHhQv359+vfvz/Tp0wFImzYttra2APzxxx9UrlyZ3r17kz17dkaMGEGJEiWYNm2awmcghPhSA4a1Q/9yDvHVm84SHR2jNtD/ubR7D6XThNAxpx/jhjdSHUcIIYQQQggRS8qUKYOHh4epBvE+a9eu5enTpxQpUoQOHTrQqlUrfv755/88f8WKFVy/ft10zogRI6hatSq1atUy9eJKrMxm+B5Av379mD17Nl5eXgQEBDB8+HDWrTMO33ny5AkdOnTA09OTM2fO0LBhQ8aOHcu4ceO4ePEiNWrU4NGjR4qfgRDiczmkcKLGV26AhkGDcUPmqY70lnuXb5DHMYTk1noc09uQq0herp25rDqWEEIIIYQQyvTtW5++/Rp88LzTp2/RoP7oN46t3zCEokWzffDaqVPWM3Xqhs+N+FGmTZvG7du3Ad7oIfX/KleujJubGyVLlkTTNK5fv07//v1ZtGgRo0eP/s/rBg8ezOrVq3ny5Am9evWiXLlySaKGYVZFqbCwMDp06ECHDh3e+tr/L8G+ZcsWtmzZEk/JhBBxrWT9Wpz3s6NAynCu3XrK7ZsPVUd6i8FgYLvXVZrXzouFHnoNbEPP5v/9iYgQQgghhBCJnZOTPRkzunzwvPv3fd86ljp18o+61snJ/rOyfYo7d+581Hl58uTB2dn5jR5Oer0ee3t7UqVKxYsXL9553ZYtW7h8+TLDhg2jevXqXL6cND7cNquilBAi6Spavx4nntlz8pkd8zsPUh3nP/02fhnNaxs/AalbPR+9dLq35r0TQgghhBAiqQgMDOXBg7cLTv/v2bOAdx77mGsDA0M/K9unCA8PN22/6+97S0tL03+vXr1K/fpvr8gdEPD2c3ylRo0a5M6dGwsLiyQ10boUpYQQCZ5bofyky2Hstnv79HkuHz+vONF/O7L/HA+eR5DR2YbMKfRUbFAZr3X/qI4lhBBCCCGEElOnbvjsoXX/P5wvoYiMjATA0dHRdCxr1qwAXLt2jcyZM/Ps2TNTb6mqVavSoUMH2rVr987HK1KkCKtWraJz58506NCBUaNG0axZszh+FgmD2Ux0LoRIuorXr2PaPrpmo8IkH2f1+hOm7W69ZcJzEf90Oh1ps7lTqGoFqrZpROHqlUiXMzu696z6IoQQQgghPo6Pjw/37t1jwIABuLu70759e+rUMb5n2blzJ3fv3mXJkiXkz5+fsmXL8scffxAaGorBYHjrsdzc3NiyZQtjx45lxYoVDBs2jMaNG1OkSJH4flpKyF+nQogEzcrWlu86VSS5dQwRoaGc3+WlOtIHTR+7mFc9eiuWyIze0kJtIJFkpHF3o+Ggfgz32szifQs4s6s/O//qyOkd/bh2cTrnfTYxaeVE3PLnVh1VCCGEEMJsaZpG586dKVGiBJcvX6Zp06aMGTMGMM4z+/XXX6PX6zl27Bhr1qxh69at9O7d+63HSZkyJdu3b2fDhg2MHz8egOPHj7Nt2zbGjh0br89JFRm+J4RI0Nr1akFlt2jAj81HHhEZFqY60gfdvf2Y6w+CyZUpGS72UKt5TbYslYUXRNyxc3Kidu9vKdW0AfqXvaEeh0a/cU4yKwP5XCBfs9x803Ayy3beZlCXUfg/eaoishBCCCFEguPp6Ymnp+cbx+7evfvWwmoAu3fvJk+ePG8cmzhxIgDe3t7UrVv3g/fz8/N76zGAj7o2sZCeUkKIBK1j52qm7d2bDylM8mnWbjxu2m7XpbbCJCKxK1CuOMeuL6FD1zqmglRkWDj7tx/h1GUfjp59wKVbzwmLjDFd42hl4Ns6WTh7dQFVmiedP3qEEEIIIUTCIj2lhBAJVnr3jHhkdwIgPEpj3tSVihN9vLmTlzOwe2WiDDrSZXVDb2GBISbmwxcK8Qkad2vFH1Nbk9LGQJaoEK48iWL73L848vc6wgKDWPCvcy0s9NSoVZyff2nPV0UzAZA5ucbav7rRJk06Ns2Yp+ZJCCGEEEKIJEt6SgkhEqxeQzpg/XI6pj1H7xAWFqE20Ce45/2EcX/fYs7VVJwIdCabR9KYqFDEn04/f4vnb61IaWOcMFNniGLToIHs+XMxYYFBb50fE2Ng6+ZjlC3Wnbr1x+IbGIlBgz2PHKjYtRONBveP76cghBBCCCGSOClKCSESrKZfe5i2p09coTDJ51m1YBMxmnH8ecFqlRSnEYlJ0+6tmDmyHvaWxhn1vR8HUzBvNw78c+qjrt+68Qg53doxZNo+bgfZAFCmRWPq9O0eZ5mFEEIIIYT4f1KUEkIkSGVqlSOri7Gb1BP/KHZtOqw40ae7sv8QkWHhABSoWhGdXl5yxZcr93UV5k1tha2FsSB18eZziuTujPftx5/0OP7+IYzrN4mlg0aYlieu3KktDXp3jvXMQgghhBBCvIu8QxJCJEi9BjQ3ba/Z+HG9PxKayLBwrhwwFtNSuKSkQLkSihMJc5cxexZWLeuLk7WxIHXrYRClC39HYGDoZz/m6c07WDNqAgCFUoWxbEpDaraSyc+FEEII8fFefcBlY2OjOImIT69+3jFfMHeuTHQuhEhwLCwtqVbKDQBNg8kjFypO9PlCbpynXhcP3JJFkuLHZnTYd1R1JGGmLKysWLVjImkdjAUpH/8Iynr0ICQk/Isf++jqDdSqmo/K35YGwHPedxQ/dYV712598WMLIYQQIvF7/Pgx4eHhfPfdd6xatYqnT59+UaFCJGwWFhakSZOGZs2aER4ezpMnTz77sXSAFnvREg9HR0cCAwNxcnIiKOjtCWOFEHGn+XdNWT67HQAX7wRS0L214kSfL0/BbFw6Nw2Ae34GsqSqrzaQMFs/z/2F0V2LAhAZrVGixA+cP3Mj1h7fwcGWK3f+IqOLLQBn70dSPFsLYqKiYu0eQgghhEi8UqdOzTfffEPu3LlVRxHx5OrVq8ybN49nz5699bWPralITykhRILjkLMgW+8nI3fyCJYs3ac6zhe5cv4WD19EkiGVNZlS6MlXPD+XTlxUHUuYmewliuH8VTX+eRhOhXQhDBi8PFYLUgAhIeHUrPwTJ09Nw9ZKR+FM1oxfOJz+bYbE6n2EEEIIkTg9e/aMX3/9leTJk+Pk5IROp1MdScQRTdMIDAwkICAATfuyfk7SU+o/SE8pIdSwtrNlxN6t2NjbERoQyIiKdYiJjlYd64ssWj+WdvULADB50QkGdBypOJEwJ1a2NvRfuwSXTBkBOPTHHNbO8Iyz+3Xq0ZD5MzsBEBEDlWuP58jOg3F2PyGEEEIIkfh8bE1FJjoXQiQoeSuUxcbeDoBzu/aYfUEKYMn8LabtGlULKEwizFGN7t+YClK3T51l3czFcXq/BbPWsevIHQBsLGDRkv5YyqSlQgghhBAiDkhRSgiRoBSpXc20fWbrLoVJYs8/Ww4TGGZckSRXelvSZEqrOJEwF3k88tP02yYAREVEsGrEr1/cRfpjNK/zM/4hxslJc6S2YNTsgXF+TyGEEEIIkfRIUUoIkWAkd0lFn5aFyJIskqCnz7h96qzqSLFC0zQOnb4PgJUe2nZrojiRMBczFw2kTY4gqqYP4sSyZTy7cy9e7uvvF0SP3n+Y9nu0KY573uzxcm8hhBBCCJF0SFFKCJFgfPNDKwq6RNEwSyClHB6iGQyqI8WaVcv3mLbrf11SYRJhLuq2/ZoKeZOj00Eup3BOrN0Yr/dfvmArR86/Wt5XR6dfvo/X+wshhBBCiMRPilJCiASjWdOypu2lC3coTBL7Vv65mUjjaCiK5EyFtZ2d2kAiQdNbWjBxUkf0Lxet+WPJYe7defL+i+JAuya/cOiuAc8bKbDPXYxcX0lBVQghhBBCxB4pSgkhEgRXt/QUcncAICRSY/mfmxQnil3h4ZGcvf4cgEiDjrJ1KihOJBKyrgM7kyuNJQAvgqP5sdsUJTlu3XhAr+9mEGkw/rlQp293Wd5ZCCGEEELEGilKCSEShG4/tcXq5SuS17G7REfHqA0UB8ZO3MiC6ylZdCMVyXPmUx1HJFAWlpb82LeOaX/0r2sJD49Ulufcjn+4f+kKABly56RInerKsgghhBBCiMRFilJCiASh0dfFTdvzZq5TmCTu7Fi1nRchxpXTcpctpTiNSKi6DelCllTG/z0/9ovkt3FLlebRNI0tU38HwMEyhnFTumFhZaU0kxBCCCGESBzMqihlY2PD/Pnz8fPz49GjR/Tr1+8/z61duzZnzpwhKCiIc+fOUa9evXhMKoT4FDkK5CR3ehsA/EJj2Py3l+JEcSMiJBTvM+cAcMmUERe3TIoTiYTGwsqK/n1qmfbHjP0bQwKY8P/GsZOkeXGZjjn9qJrDgkGTvlcdSQghhBBCJAJmVZSaOHEiHh4eVK5cme7duzN8+HAaN2781nkFChRg7dq1LFiwgMKFCzN37lxWr15NwYIFFaQWQnxI9x9bY/Fympqd+66jaZraQHHo6sEjpu185UorTCISot4jviNzCuP/mh8+j2D25JWKE722Y+kG0xDbnp3KY2VrqzaQEEIIIYRIFDRzaPb29lpoaKhWoUIF07HBgwdrXl5eb53766+/alu3bn3j2Pbt27XRo0d/9P0cHR01TdM0R0dH5c9dmrTE3m4+W6cZtE2aQduklataXHmeuGzZCufVjj7drT0J3aodub5MeR5pCafp9Hrt3KP1pn8LXfs0VZ7p/9u1BytM+X6e0k95HmnSpEmTJk2aNGkJs31sTcVsekoVKlQIKysrDh8+bDp28OBBSpYs+dZKQJ6engwcOPCtx0iePHmc5xRCfJoiZYuS1cW4ytgT/ygO7D6hOFHcunPhKgVShJLGLoaC7o5Y29mpjiQSiAJVKrDneRq23nfkxM0g5v22WnWkt/wy/PX8Vj26VJS5pYQQQgghxBcxm6JUunTp8PX1JSoqynTMx8cHOzs7nJ2d3zj36tWrnD9/3rSfN29eqlSpwj///BNveYUQHyddsZIsvZmCU762LFt7SnWcOBcTY+Dc9ecA2FlCw3a1FScSCUXFDq0xoONagA1t201NkMNYl/25CW+fMADSOer4/pdvFScSQgghhBDmzGyKUvb29kRERLxx7NW+jY3Nf17n7OzMmjVrOHToEBs2bIjTjEKIT1e4ZlWehlvi9cCWEQN+Vx0nXmzb9rr41qhZBYVJRELhXqQgbgXzAfDo2g2uH0m4PQZHj349z1WfbtXQW1ooTCOSEhsHe+p/05y5m6dz5PYabvht4X7wVu4EbuX0g/WsObKAXmP6kNY9s+qoQgghhPhIZlOUCg8Pf6v49Go/NDT0ndekSZOGPXv2oNfradKkSYL81FmIpMytUH6cM6YH4OaxkwS/8FOcKH4smbvetF26qJu6ICLBqP1tG9P23kXLFCb5sIUz13Df1/ihUMYUerr0b6c4kUjsUqZzpfHQH1l0eiPr/mjDN3WyUtLdmmwpDGRwiCGzYwyFM1jQsFRqpv9clVvXf2fmzgXkLF1CdXQhhBBCfIDZFKUePnyIi4sLFhavP5F1dXUlNDQUf3//t85Pnz49+/fvx8bGhooVK+Lr6xuPaYUQH6No7eqm7TPbdilMEr+8bz7gwYtIwPimPneRPIoTCZWy5c/J5C55qJspkGRhTzm7fbfqSB80cfJ603bvXnXUBRGJmpWtDfV+6MXALav4qllDfKLsCY/RffA6e0sNO7ecfPvHdLov+h3X7FnjIa0QQgghPofZFKXOnj1LVFQUpUqVMh0rW7YsJ06ceKsHlL29Pdu3b8dgMFChQgUeP34c33GFEB+gt7BgVN9KFHEOw8YQxoV/9qmOFK8OHL1l2m7RSd7UJ2UDx3TF2gJyJI/EOeAmMdHRqiN90OyJy3gREgNAljS2FKpQUnEikdgUKl+c2V5LqNihFZYvJ9QPCQplzT+3mDhjJ+Ur/Exql9ZYWdYnmUMT8uXtTu/+f7H3xD1uPovGP9L4IWa2YkXou3Ihtbt3Qm8hQ02FEEKIhMZSdYCPFRYWhqenJ3PmzKFjx45kyJCB/v3707FjRwDSpk1LQEAA4eHh/Pzzz2TLlo2KFSuavvbqMQIDA1U9BSHEvzRoV4e8qSEvIaSJCiY8OER1pHi1/u+9tKxt7CFVpXJBRqiNIxSxsrWlYbVcpv3RP89TmObjxcQYmDL7Hyq2bMhlf1uKNmrIuX3HVMcSiUTbHzowY2wTbC3gr5sxPA+OZp/ncvZ6LifsHX/HhYZGcOXKfa5cuc/MyavQ6/UUqFaJmj2+IY27G1bWVsyb2JSLXSvSuFIvgv0CFDwrIYQQQvwXzVyanZ2dtmjRIi0oKEh78OCB1qdPH9PXNE3T2rdvrwHalStXtHdZuHDhR9/L0dFR0zRNc3R0VP68pUlLjG3T4bmaQdukGbRN2vDJvZTnie9mY2OlhUcbn39AxCZNb2GhPJO0+G89hnU1/Ts4f3uJ8jyf0iwsLbWhuzdoky8c0SaeO6S5ZM6oPJM0829jFo7UImI2mf5dnLqzSkvj7vZZj2Vpba3V7tNN2/twt+nxrvqs17Lmz6H8eUqTJk2aNGmJvX1sTcVshu+BsadThw4dcHR0JGPGjEyfPt30NZ1Oh6enJwB58uRBp9O91V71qhJCqGVta0v5osYJzmMMMGvCUsWJ4l9ERBRX7xk/8be3glLVSn3gCpEYde1SzbQ9a8ZmhUk+XUx0NAeX/Q2AXq+nXJvmihMJczdt1QR+al8Eq5d/nV6960+9Mt156n33sx4vOjKSrdNns3PRSmIMxmM501hw4PAU8hTLF0uphRBCCPElzKooJYRIHFp1a4Ljy8U0z970w9fnhdpAisxdtJ8Ndx2ZfSUV9plzqI4j4plHpRLky2gLQECYgfm/rVKc6NMdXb2BiNAwAMo1qkmy5I6KEwlz9dvqifRqkgf9y3nMt+27QeGcHXn08MsXqhk7eB71m0wgOMJYmUrnCHv3jaNA6cJf/NhCCCGE+DJSlBJCxLt2Haqatpct3asuiGIr5m/gdpANUQa9LF2eBP00vL3pDfjfm84S86orhxkJCwzC5/h+amYMomeRcL7/5RvVkYQZGjJzEN0b5Ub38t/Dyk3nqVOxH5GRsTfp/9Z1Byhf9kcCQo0T9Kd2gB07RuKW2z3W7iGEEEKITydFKSFEvEqW3JGSeZ0BiIyBedNWKE6kjt+jJ/jeewCAW6H8WNvZKU4k4outgz3VSmUGwKDB2EF/KE70+R4cP0qeFBFY6qFD63Kq4wgz07Zfe4Z2+8pUoF2+8Rwtvx4cJ/c6e/IapUv2Na0c6eqo45+DU3FOlzpO7ieEEEKID5OilBAiXnXp1xq7l+t+Hr3gQ3BgqNpAil0/chwASysrsnoUVhtGxJvOP7TG6eUQ1tPXX3Dn9kO1gb7Amr+288Q/CoCsLpbUaFrtA1cIYVS4nAe/j2tqmkNqz7G7tGkwNE7vefWiN5XL/0hwhAZAVmcLth2cgd7SIk7vK4QQQoh3k6KUECJetWj5uieF54IdCpMkDA/OniFPinBqZgzihwFNVMcR8SRnqRL4hBnfBP/5xzbFab7c4hWHTds/DGqpMIkwF3ZOTtQd/DPn/WzRNLjk7U/t8t+jaVqc3/v86evUrz+GyGjjvbJlcKTN8B/i/L5CCCGEeJsUpYQQ8SZFGmcKZnUCICRSY8mcdYoTqffsxnVqZgwmT4oIqpbJqjqOiAcp07kSnSk/y26l5Lf94SyYsVp1pC82Yeh8Io0joihfMDXp3NKpDSQSNJ1OR6uxw0iZIR1Hnzow+5/nVCndO1bnkPoQrx3H6DVgCTcDLFl+KwWFGjSkeP3a8XZ/IYQQQhhJUUoIEW/yVCzPwuup2P0wGUu3XCEqKv7egCRUt6/d47G/8fuQKaUlmbJlUpxIxDWPf73x3bRkc6L4d/DC15/dR+8BYG0BP435TnEikZCVb9uCvBXKABD8wo8hbX7kqY9fvOeYN20VbTv9ToTB+Odw46E/4pojW7znEEIIIZIyKUoJIeJNkdrVCI/Rc8HPlpGD5quOk2AcO2t8M6/XQfNO9RSnEXFJp9OZemMYDAZObTT/oXuvjBuxyLTd7OvC6F4tpSbEv+Qplo82P3YBjP8Glg4cgb/PU2V5jq3dxOFVxl67VjY2tBk3AmtbW2V5hBBCiKRGilJCiHiRIm0ashUrAoDP7Ts8unZDcaKEY8v61/PxVKtRVGESEdfK1KlA3hxpAI0bR08ofTMe2w7uPsGtJ2EAuDrq+bptHcWJREKjt7BgyZoRtM8TiodLKAeXrjQt9qDShvHTeHzjFsksY+hVPTVz14xVHUkIIYRIMqQoJYSIF4VrVjVtn966U2GShOdvz63EGIzbRfOkURtGxKkff25Jq2wBtMvuT+AF9W/GY9uS5QdN2z36NFSYRCREw2cNpIibLZZ6KJYqhBPLV6iOBEB0ZCQbR4+jdTZ/MiWLok2tHDTqWF91LCGEECJJkKKUECJeTBvdiCrpg8lgH8WZrbtUx0lQAv2DuO1j7GHibK+jQMkCihOJuGBtZ0eFYukBSGEdg9faxLf65LRRC4mMgefhFsSkyoStYzLVkUQCkatIHn7oXNq0/9PQ5Ty6n3B6Cl4/fZG1284DYKGDWb91JllyR8WphBBCiMTPUnUAIUTiV6h0QQpksEGnCyetZTDP7z9QHSnBOXryNjnq5QOgcesaXDh2QXEiEdva9miCo7Vx+/SNF/g8eq42UBwI8Aui9+TD5KpdD9BRpGY1jvwtq2wKmLdsCPaWGgCHzj5h1oTlihO9rXOTYZR/tILMLjakTabjj9VjaFWtt+pYIolxzpSB3IVykTVnJpySJyMyKgo/3wAunLrG3Ss3CA8OUR1RCCFilRSlhBBxrnv/lrya83jLrktqwyRQW9cfpO3LolTFivkVpxFxoU27KqbtpX95KUwStzYvWE2u2l8DUKJhXSlKCVr3bkXZ3E4AhEdptG40VHGid4uKiqZd6wns2T4UvQ6aVnZnYaMq7Fr7j+poIhGzsrWhZqu6tOtUkyJ5XcngpMPGQnvrvGgDTL+YkkdXb3Lj6AlOb90p83MKIRIFHfD2q57A0dGRwMBAnJycCAoKUh1HCLN2328DGVIYRwsXLTmQs8elMPX/7OxtCQj6G0s9+IWBs72swpeYOKZ0wufpUmwtISIaXFI2IyQ4THWsONN35SIy5s0FwMRGbXhy45biREIVu2QO3Hi4nPROxk8mxs/6h0E9p6kN9QGeG36l7dfGDwfuvYghR/rmREVEKE4lEhun1C6Ua9OMIf3rUjJ9zAfPfxFhgeeNlG8+RoA3+9dsY+lvyzDEfPgxhBAiPn1sTUXmlBJCxKkqDSqZClJ3n0VIQeo/hIWGs/vscw48sWfbo+SkzZpFdSQRi7r0a43ty77JRy88SdQFKYDj6zYB4GITTbeB7RWnESpNWjjEVJC67xvOkD6/KU70Yd+2GIFPYDQAmVNZMO6PgYoTicTE2s6Wmj278vO21VTu1JZ7EQ5vfN0vOJrjl3xYv+sqnqtPsXTDObYf9ObAibs8vHL9X2dqNCiWAs8pzbjyeA1NujaN3ycihBCxRIbvCSHiVI++TUzbf69PfKuNxabx03fw9QDj/CXZS3rgc/uO2kAi1rRoUc607fnndoVJ4sf5nXtYOa8T6ZMZCMxchNHdrImKjFQdS8SzTDmy0L5+AV51yv/umxnEvFpqNAELD4ugW7fZrF3aC4DvWnnw29gs3L12R20wYfaqNq1JzV7foUuZ1nTsjp/GvsBnHNl3hvm/reb2rcfvfQyHFMkpWL0yjTo2wDl/KgBypLZgxZx2HOhdnw5NhnL3qnecPg8hhIhN0lNKCBFnrGxsqFIyMwAxBpg+2lNxooTt5olTpu3sxYsqTCJiU5qMaSmc1biKV0ikxpK569UGigdBL/x48uQFAE420KlfK8WJhAqtfvyWKM3YS2rf6UdsW79fcaKPt37ZTvaf9QHAzhJG/DZAcSJhziytrZm5dhKbl/egaVEbQCM6Kop9i5czukZjKhXuxM99ZnywIAUQ4h/AkVXrGFy/M4PHbOBZYBQAeh1UyJecs2d/o9OPHeL2CQkhRCySopQQIs50+r4FjjbG7TM3/Xh4z0dtoATu0bWbhAYGApCteFF0r2aHF2at249tsXr5f9v9J+4THZ005v1Y+Ocu03a7f03yLpKG9Lly4FKiPJ43UrLb24JuHcarjvTJOjYfyZNgWH/XiRfpi+JetJDqSMIMOadPy8HLS+jeMBfWFpDJIZq0fleZ2LA1Gyf+RpDv563EGhkZza9D5pPRpRlT/9hHRLSxR2JyG/hjXGPWHJiLtZ1dbD4VIYSIE1KUEkLEmfYdq5m2PRfues+ZAkAzGPA+dYa0tlGUz25FsQoeqiOJWOCQNTdX/a2JMsC839erjhNv/py+kjDjB/gUy5kCl3Sp1QYS8apu3+7o9XpiNB2jRyzm6oXbqiN9Mu/r92jU6U+8g6wBaDioH3oLC8WphDkpUt6DU5fmUSLb6+LQpj3X+bZWd3zv3o+Ve0RFRfPDt5MoWKAnV+4ZP9jS66Bh2fScuLYY18zpYuU+QggRV6QoJYSIE47OKcmR2bhKTGiUxvxpKxUnMg9FU4XRKnsA5V1Dad6uhuo44gs5OqdClykP2x44MXJbMBuW71YdKd6Eh0Ww96TxTZe1BfQc1E5xIhFfcpYuQa4ypQB4/uARh5avUZzo8x1bs5H7l68CkCF3TorXr604kTAXFetX5p+dI8icwtjrOTJGo1vfxdSv8gMhIeGxfr8bV+9RwL0tMxcdxvBybfUCmWwZv342rjmyxfr9hBAitkhRSggRJwrVqMLiWyn529uJ2SvPEREukxx/jM3rXs+5Uq5sboVJRGzIX6UCer3xf7WntnuhaZriRPHrj5nrTdtNG5dSF0TEq19nfk9ya+Mw1W0z5hITFaU40efTDAY2jJv6ao9uQ74hWfJkSjOJhK9mi9psWNWPFDbG13y/kBiqVBvG3Gl/x+l9DQYDvTv+SuvOcwiL0jjkY4+vVVq6L5hFxrzyN4UQImGSopQQIk54fF0b0PEgxJpJIxaojmM2dm/YT7hxJXLyZUmOTi8v0+asYLVKpu1zO/coTKLGhmU7eR5iXG0tVzpb8haVN0WJXdNvGtOwqCMdcvhR0PI+Z7eZ/9Bt7zPneX7mKI2zBNKuiI5xc39SHUkkYHXb1OXvxd1xtDYWpB69iKBwvm855HU23jKsXLiFokX7snrPHcC4Yt9382eQpVCBeMsghBAfS97tCCFiXdps7mTKlweA+5eu4HNLlib+WNHRMVy9a5wTwsEKKtUtpziR+FxpMqShZpV8WOo0fO894PH1m6ojKbF1j3Hok14HvQa2UZxGxLVhw1sCxp/34W0HEk3vwKMrV5M5mbHHV8dGhUnvll5xIpEQZSlUgPHTe+BgZfy9v/s0jGL5unL/bvwv9HLt4i3mdOnFrZNnALBzTMaYFZOp3KDSB64UQoj4ZVZFKRsbG+bPn4+fnx+PHj2iX79+H7ymTJky3Lp1Kx7SCSFeqdC8vmn75MatCpOYp/2Hrpi26zeVPx7NVefvW1LfPZTv8jwnXXDSLEgBzBi/zLRdr3p+hUlEXGv6TWPyZTAuueoXGsOYgbMVJ4o9e7YcYv9ZY2HBzgpmeP6sOJFIaFyzZ6Xz75PY/Tg5twOt8H4SSrG8XfB58kJZpojQUOZ168u1Q0fJ5hhB05wRrFvRj+KViivLJIQQ/8+silITJ07Ew8ODypUr0717d4YPH07jxo3/8/z8+fOzevVq03weQoi4Z21rw9xhVWjq7k92hxDObDX/oRvxbcPK18O8yn6VS2ES8SUaNigJgJUeDu88rDiNOicPnePGk3BuBFhz0j8FGfPK73RiNXxES9P2jLn/EBkZrTBN7OvecQLRxtGo1CvnRsESUmQVRo4uznwzZyr2Tk5Eazomr7pGiYLf8uJ5oOpoRIVHsLDPQArYPcNSD442sG3LMPIWy6s6mhBCAKADzKJftb29Pb6+vtSqVYt9+/YBMHjwYKpWrUqlSm/3JOjatSuTJk3i9u3bJE+eHHd390+6n6OjI4GBgTg5OREUFBQrz0GIpKDn0C78NtLYU+r0TX88crRVnMj86HQ6giI2Ym8FYdHgZN+QmKjE9eYusUuZJhWPH3libQEhkRop7BsQE2NQHUuZUk2+punwQQDs+2sFGydMV5xIxLZmXRuzYm4HwNhLyjVFE6IS4evWyp1TaFotBwB7zjylatHOihMJ1SysrOizeBYZ8hvna7p34TJzuvQiIjRUcbI3pc+UlpMX5uCa3BKARwEGCufpgu/jZ4qTicTOwsoKt4L5KF3ZgyoV8+Pu5kIyBxvsbCwBAyFh0QQGR3Dn7lMunPVm344jnD98JtEM/07KPramYjZdiAoVKoSVlRWHD7/+tPngwYOULFkSnU731vm1atWiffv2TJ069a2vCSHiTpfOVU3bc37fojCJ+dI0jUu3/QCws4TqDWUIn7np1Ls51hbG7cNnHyXpghTAuZ1eREcaV+AsUqsaegsLxYlEbBsytIVpe8bcfxJlQQqgT6dfTYtRVCicBo/yRdUGEsr9tX0aP9XPiJNVDH6Pn/Bnz/4JriAF8Oi+D2VLfs+Ll4tPpE+uZ9eRmVhZWytOJhIjGwd7SjSsR5fZUxh1cAc9Fs2mXZ/WdKidjQr5klMsiy1501mSN501xbPaU6VgSjrXy8W0oTU5c/AXph3bSrvJYyhYrRKW8jua6JlNUSpdunT4+voS9a9lhX18fLCzs8PZ2fmt8xs2bMi6deviM6IQSV5+j3zkz+wAgH+YgYW/rVKcyHztP3DZtF23UXmFScTnaNT4K9P2ssUyhDUsMIjL+w4B4OTiTI6SHooTidhUo1l18me0BcA/1MCYnxLPXFL/78mDZ6zaegkACx1MntlbcSKh0o8T+9K8UmZS28XQIqs/a4YMJ/iFn+pY/+n2tbvUrT3CVFgt5GbPGq/f1IYSiUruYvlZ7vU7j3xX0XXcT+QpWxobezsA/CI+7gOpgEg90XbJKVS9Mu2njGWE12b6zhhOjgI54zK6UMhsilL29vZERES8cezVvo2NjYpIQoj/8+MvndC/7Li4YcflJN875Ets/NuLoCg9V/xtiHBIrTqO+ASOKZwomiMFAOFRsHzeRrWBEohTm3dgodPI7hRBv59bqY4jYtHQX9qZtv9ceijR9pJ6pX/XCYS9/Iy0TH5nylQrpTaQUKJC3Qr80rcyrwZsLFp2iCvHz6sN9RGO7j9D5+7zMbwcGVX3qwxM+HOI2lDC7OUrWYgtJxZy9tg4mlfMREpbjbwpwgEI8HnGiQ1bmTtsGj8M+ZtKVYeTPds3uKZti2vatuTO3Y3qdcYwaPQGVm69zI5jDwkNeD3Uy87JkR86luLimclsPfEnBUsVVPU0RRyxVB3gY4WHh79VfHq1H5oAu8gKkdRYWVtTt7LxEwyDBuOH/ak4kXk7sPMYUw4acErtSHjq7OgtLDDExKiOJT5Cu15NsXn5YeDRi0+IjIx6/wVJxI0jx+iY4zmO1hCaLit97O0IDw1THUt8oUx5cpDHPSVgIDRKY+SAWaojxTlfnxcsWX+Wb5oWRq+DPsM6cWjXUdWxRDxyck7BkiX9TK/1B88+pleHX9WG+gTL520gd74sDO1jnHKhb4eSHN1fj7WemxQnE+bGyTklUz2H0bJmDmwtNF5NV61pEHr/NtPaT+X+pderSnu94zGePvXn+rUH7N76+nVUp9eTvUQxitWtSYV6FUjvYPywo6ZHGqocGsNarzv0bj+aZw994vLpiXhiNj2lHj58iIuLCxb/mofC1dWV0NBQ/P391QUTQgDQ+YdWpDCO3uD87UCuXripNlAicOvkGQBsHRzIkFu6LJuLho3KmLZXLd+rLkgCExEWzvnrzwGwt4IOvZspTiRiQ5nWzVl8IyXb7ifj98XHCAoIUR0pXvzUbRJXn8Gq28m5n6IA6XPlUB1JxKMV26eQIbnxbdTTgCjqVuyrONGnG/79dNZ53QaMZYRWP3bFKbWL2lDCrLTt25YbdxfTsU72lwUpiDZobPjnOnny9qCqR9c3ClKfQjMYuHH0BCuGjGJcow6s++cGUS8/m7XSQ/MqWbhxaz4Dfu0ZW09HKGQ2RamzZ88SFRVFqVKvu0iXLVuWEydOyMz8QiQA3bvVMm3PnbtVYZLE41VRCiCbRxGFScTHsrCypERe43DLqBj4a85axYkSlmVL/jFtt2otE/ibuxRp01C0dg0M6Dh9L5IhvaaojhRv/J8H0PV7Tx6GWgFQo0cXxYlEfOk3thc1PYyv89EGaNZkLIFmWoxtUbM/x28Gsco7OXcNLrSbNBq9pSxEId4vRepUrDs8n4WTm5Ha4fXxbQduky3rNzSs+gPXr96LtfvdvfWAxlX74e7eiTW7b/BqdhAnGxg/sAbHby4ne77ssXY/Ef/MpigVFhaGp6cnc+bMwcPDg/r169O/f3+mTzcuK502bVpsbW0VpxQiaSpTvRT5M9kD4BdqYP6UFYoTJQ63T50FwEqvUb6GzFliDnKXLMYlfzt8wy244O1PSJAMT/u3hTNWmSbYLZ7HGaeUTmoDiS9Srm1zLKyMM0EcWrGGyLCk9ft+dPVG/J8Yh47kr1SedDnlTVFil694fkb2r2HanzhrN/t3n1SY6MtERUZRpXg3rt429mJ1L1qIOt93V5xKJGSZC+Tl5zULqVHC1TSP7O3HIVSqPpI65ftw/27cDad7dP8ZTav1o8RXP3H2pr/puEe2ZJw+PZXO/dvG2b1F3DKbohRAv379OHXqFF5eXsyaNYvhw4ebVth78uQJzZs3V5xQiKSpZIM63A40flq8bO0pYmTuo1jhc8ubeul96Z7nOT82cUenN6uX7CQp21elOPzUgb9upqRn/6Wq4yQ44aERHLlg/IPVxgI6922hOJH4XCnTpKJ2m3oARIVHcHDZ34oTxb/oyEi8Fi55uafRftC3SvOIuKW3sGDZ2uHYWxlHaBy/4svg3tMVp/pyIf4BLP5hMNEvVziv2L4VRaqWU5xKJEQlG9Wjx6LZWKRMw55HyYgxwMxFh8iVqRX7dp2Itxxnjl2maI629P9lPaGRxn+PyayhYe9ONBzUD0tr63jLImKHWb3DCQsLo0OHDjg6OpIxY0ZTLykAnU6Hp6fnW9d4enri7u4enzGFSFIcUqYgTYlybLiXnNmnrfil/0zVkRKVYP9A9DrjHDwVa5f58AVCqfyVjH/Ix0RFc87rkOI0CdPihTtN282bl1eYRHyJn8f1oGvBCOpmCuTR4T0Ev/BTHUmJo2s2kcbwnJZZAxjdPjfFKxRTHUnEkUodWxFlmwKAoAiNBtUGqA0Ui+5duMymSTPQo/FVmhD2bPqRzDkyqY4lEgi9hQWNBven2S8/mwo+2/ZcoWjJH+ndcZyy1banjPiTvLm/49ztQM6/sOVagC1lWzWl24KZODqnUpJJfB6zKkoJIRKeUk3qY/VyJczty7bg6/NCcaLE5fCR66btuo0rKEwiPiRtNnecM2YAjEMvwwKDPnBF0rR0zlpCIo3bRbInxzmt/OFobvQWFrRpUhydDnIkj+TSrn8+fFEiFR0RQfiNc7jaR6PXwa9TZehTYpQ2mzvVu3Vh35NkrLyZjJ59F/Hkoa/qWLHq4LK/yRN9g5JpwkhuC+t3TECn06mOJRSztrNl64k/Gfh9TdOxg8v+Zk6XXlw4+XmTmMeme96PKJq9DT17zCYqIgKALIUK0Gf5n+T2KKA4nfhYUpQSQnw2C0tLyjRvDIAhJoZDK1YrTpT4bFq737Rd5qvcCpOID2nQ/muSWxuHrl702v+Bs5Ou6KhoDpx+CBhX0Pm2f2vFicSn6tCnJWkdjW9Wbz4O45/NSbtX4M+9ppjmSqtQKA35PfKqDSRild7CgpZjhpp6iCz9fTV/zU6ci1j0bD2CUOMoPgq7J2Pin4PVBhJKJXdJxcFLnlQv4oyHSxiFUwazfPAo1v06hZjoaNXxTDRN4+CKtcxo09U0z1+JXCk5dWQsHfvK3xjmQIpSQojP1n1IF4rndEKHxqW9B3nx8LHqSInOPxv3E/Hy//t5sySXTy0TsB+6lqdTTj/aZffj0bnTquMkaAvnvV6hs0njrxQmEZ+jV+96pu3fZ29TmCRhePb4OWt3XgPAQg/jfuutOJGITRXaNidTvjwA+Ny+w/ZZ8xQnijs3L3vzw+CVpv1e7UtSvpa8RidFqTOm5djF+Xi4GxcyMmiwe9EqTm5MuCtsP7x6nWktOhF97yo1MgZhZwl/TG7B4El9VEcTHyBFKSHEZ9Hp9QzsW4s6mYLokMOPc+sT56eGqkVHx3D9QTBgnMTxq6olFScS75I5eyaypjF+im6IiuT2pVuKEyVsaxZt5o6fxrGndpwNT4d9clmFz1yUrFyCgpmNb1ICwgzMGveX4kQJw8AeU4h8ucZH9ZIZyJY3q9pAIla458nK37NbUThVGJohhhVDRxMdGak6VpyaO3EJmw/eA4y9Wf9aMgArGyvFqUR8Sp0xLYdOzyFnWuPPPSoGOnWfz69DEn5BNuj5C35t24eLtwMAsNDByH5VmbxoqOJk4n2kKCWE+CzdB3cknZPxJeRFQBhn9h5XnCjxOnzshmm7XtNKCpOI/9KhZxPT0sj7j99RmsUcGAwGeo/eyeGnDvjF2FCgisyXZi4Gj+7Mqw6bKzecISoq4QzhUOnBnUds3n8bAEs9TJjVV3EiERsW/v0LqWw1KqUPIYvfWe6dv6Q6UrxoWftHfIKMVdZMqSz5Y9VoxYlEfHlVkMqe2hKA8CiN+o0nsHjOBsXJPp7/i0BK5G7PjqMPANDpoG/7EizaPElxMvFfpCglhPgsP/SuY9qeOGm9uiBJwNb1B03bZcvmUZhE/Je6tT1M28sX73zPmeKVs9t3m7YL16qmMIn4WGkypKFq8fQARBvglwG/K06UsPzUcyrRLxehqlMuC5ncM6gNJL5Iuz6tKJ8vBQAR0Ro/fjNObaB4FBIUQpdvZmLQjPtt6ualagP58CCxS5nGmYOn/r8gNZ7tGw4oTvbpYmJiqFW6G8u3XTMda1cnF6v3/KYwlfgvn1SUio6O/uQWFRUVV9mFEIq06d6ULC7GLr2P/aP5Y9JSxYkStx3r9pqGheTPmlJtGPGWZMmTUTCrcfhZaKTGxmU7FCcyDw8uX8X3nvFTzOzFi8ryzWbg5/E9sDW+V2HvyYc8fvBUbaAE5tblO+w6ZvydtraAsTO/VxtIfDbHlE5MGNPCtD9ljhe3bzxQmCj+bVm5m1U7jCsAW+hh0cK+2NrbKU4l4oqtgz17TswhR5o3C1K7Npn3Qhata/fn96UnTPuNKrmzcvc0dYHEO31SUUqn09GkSRMqV678Ua1p06bo9dIZS4jEZuiw5qbtGXN2ommawjSJX2REJDcehgAQoVmQvWAuxYnEv7X6thHWFsbtE5efEh0dozaQGTmzfRfJrWIomTaC3kM6qo4j3sPCypKWDYqY9kcNWaAwTcL1U+/fTL1LGlbLjVNKmS/NHP2+bCRpHIzjVG/7hDG0z3TFidTo3Hgwj/yNQ3StbK1p+kNXxYlEXNBbWtBnzq9kT2csOkZGQ4PGE8y+IPVKzzYjGTfn9arITatkY/HWyQoTif9n+Skn79ixgz179hAYGPhR5ydPnpwdO+QTYyESk8792pAjrQ0Az4JjmDQs4U96mBiMn72X0u3aEh6jxyVnbm6ev/bhi0S8aNSknGl77eqD7zlT/L/Aq+fplMsPgLQtSjFGpuFJsPJXKseJF44U0YUT8CKAA7tkHsF3uXjyCocu+pLOLT0nn9lRoGZ1Di1frTqW+ARFyxalWfVsgHHFsY5tJ2IwGBSnUiMsNJwO7afyy9whnHiejIKNm5Jp7U7uX7qiOpqIJTqdjua/DCZN4RKs9I7h64z+9OzxOzs3Ja6/Z37uNhG9Xs+PXcsSbQCHPB7U7NWV7TP+UB1NADrgs7o42Nvb06VLF3Lnzo2Njc1bX+/cufOXZlPK0dGRwMBAnJycCAoKUh1HiARBp9Ph7buWzKmM9ewfR29i0lB5MY8PmQvmo8/S+QCc3rKDpQNHqA0kAOO/iRehG0lua5xjJ3XqNgS8CFAdy6w8CVxPGkcLNA3yFOrH9Qs3PnyRiHdd50wlV5lSACz5vj9n/kkcn6DHhfS5svPDauOqhM8fPGJc3WYYYqQHpbk4fnM5HtmSAbD2nxs0qdpPcSL1qnbtQK1e3wLw6NoNprboiEF6BScKtft0o0qXdgBERUTwZ48fuHHslOJUcWfC/EFkrVKTu8HGFZO3TPudPX/KKrJx5WNrKp89tm7FihUMHTqUlClTotPp3mpCiMSnz4hvTAWph35RTJZeUvHmweWrRISGAZDVo8gHzhbxpWqDiiS3NW5fuR8sBanPsHO/cc4SnQ66D2ipOI14l1QZ0pkKUr73H3B2z2HFiRK2R9ducuWA8XvknDE9BavJqqnmon2fVqaCVEikxretRilOlDDs+fMvHl4xvlanz5WDiu1bK04kYkOjbi2p0qUtAIaYGJb8ODxRF6QAfuzyK1MHvZ7svM733SnZqJ7CRAK+oChVqVIl6tWrR8uWLenUqdNbTQiRuOgtLajVrIZproxRY1bLXFLxyBAdw91zFwBIkTYNzhllVaeEIIdHYW4HWhFlgJ27z6uOY5b+mL7GtF2vZiGFScR/KfGvP9iPrdkkr/0fwWvBEtN2pY5tFCYRH8vS2prBQ17PmTlp5k6eP/VTmCjhMMTEsGrEWAwxMTjbRLNwagtKVfb48IUiwarWqApLZ7SiTqYgLHUa68dP4+KefapjxYtDK9awZdrr1WNHT+9D3xHfKEwkPrsodeXKFezsZAUGIZKKMs0bczE6PYtvpGDbWX/+mLxMdaQkx/D4FlXTB9Ehxwt69m+mOo4ALLPkY8O95My+4szUMdL9+3Mc3HWMxwHGYSDuqa3J75FXcSLxbxaWlgwbUJcM9lHEREVxYv1m1ZHMwq2TZ3hw8TI5k0fwY700dOjd/MMXCaUqtGvJzqcunPa1xftpBKMG/P7hi5KQB5evEXRsN62z+5PJMYbFS3+SBa3MVI6COVmx5HtsLCBn8kgy+J5JcnPf7fnzL/Z6LqN0mhCqZAxj/NCvadO9sepYSdZnv5J06NCB2bNnM2zYMNq3b0/btm3faEKIxMMhZQpqdO8CgF+kJd+1/VVxoqQp8P5dCqSKIKWNgfIV8quOk+QlS5WSTPnzAHD/2i0eeSet5cJj03av15Pmdv+hxXvOFPGtQ58WlMqko1nWAIpbehP0/IXqSGYj+vJh6mQKIq1dDAMGNFIdR7yHU5rUVPmmHREGPV4P7ahQcVCSndz8faYPmkJAmLGnZHZXW8bM6q84kfhUTqmSs/2f8aR82bfknm8EHeoNUBtKkc2TZxL++B4AlnqY91sHajWprDhV0vTZRalvvvmGHDly0K1bN4YPH84vv/xiaiNGjIjFiEII1Wr27IqdkyMAx9dv5v7Fy4oTJU2bVuwi+uXfyIVyplYbRpDrq5KmT4mvHpA5dr7EnKmrTNu1q0nBNSH59rtapu01K/YoTGJ+fv/VE98Q44t2noz21Gwkb3YSqrp9u2Njbw/A4VXreHBFFlx4l4AXgfT76fXQ1D5dypEzfzaFicSn0Ol0bNo/E3cX4/ywgeEGqpTtR3BQqOJkamiaRo2S33LmtnECbhsLWLW0L8XLylQC8e2zi1KdO3emZcuWpEuXjqxZs77RsmWTFychEouKdcrSq1t1LHQa4cEhbJ02W3WkJCs0OBRvn3AAUtnryFskt+JESVvl+hXR64yfGF85cERxGvN2Yv8ZHvhFA5DZ2YqiZQoqTiQAcuTPQZFsTgAERWjMnSTDtj9FTHQ0fy55/dowbEwHdWHEfypV/SvK1q8OQGhAIDtmySIu77N45iq8zj4FwNYSlq37RXEi8bEmLx5BuXwpAIiKgUZNxnPr2j2lmVSLioiiQtFvufEkAgAHa9iyfSSZ3NMrTpa0WH7uhb6+vly6dCk2s4gExDaZA2kypiV3HjecU6fAOXUKbO1s0FvosdDr0VvoiIqKISgghKCAEA4evkJIQBBBvs8JDw5RHV/EEr2FBfMW9CVbmjAKpIrg+xGbZOiGYifO3CVHulwANGxVnctnripOlDRZWFoyrF0+LC1ecPW5jgEvJ6EXn2/bP5f4ponx08lufZvzzSGZOF61ASM7YfFyQeUtXteJioxSG8gMjf5xJj06liGZNZTIlZKiXxXk9GH53U5I5s7/npzp/Tjpa8ewiQsIDQhUHSnBa9dgCFeu/0EyayiaPTl9R3zD1BFSzEvImnRpRM9WRU37g0auZc8W6eUNEBwQRPni3Tl96Q/SOVng4qDH6+hvFMrenpAgeV8bHz67KNWjRw9mzZrFyJEj8fb2Jjo6+o2v379//4vDibhjYWlJ4ZL5KFWhCIWK5iBbVld2Xw0j2j4VKVzTYOvgQHanCOplDvqox5t+0RkDxr9co8IjyGPvR4E0Bh4/C8bb+xlXr97jwunrHD9wlsf3nsTlUxOxaNKCwWRLYw2AITqatTMXK04kdmw5SqvaxqJUxUoFGKM4T1JVp3l1HKwBNKIC/DBEx6iOZPZ+n7ySKjU8uB5gg7c+k+o4SZ7ewoKGNfKZ9scNW6AwjfkKCQxm1ZaLdGqYH70ORk/uRu3S3VTHEi+17tmcApnsAI38TsEcW7tRdSSz8PDuY0ZN2sr4n2sD8MvAeiyfv4EnD54qTibeJUeBHMyf2RHLl2Ok1u2+wZSRC9WGSmB8HjylRpWfOXhwPE42kDWNDf+cmM1XeTvI/HLxQAd81rq+MTGv/wD/99LAOp0OTdOwtPzseleC4OjoSGBgIE5OTgQFfVxhJqHS6/WUquRB9a/LUKpUbvJkT4NrCkus/m/w5to7TtwNtjbtZ3aIpLH7hz8tijHAb5dd3jhWKV0whZ3D33m+X5iG98MgLl55wN4951j91zaCn8uSuwlN4dIFOXpgDNYWxv1W38xnxfwNakMJnFI68tx3GRZ6eBZsIK1jfdWRkqS/tk6mda2cAIyZuZehvSYrTpQ49Fn2J5kLGFff+7VuM3zvygdcqnTo05IF01oBcPleMPndWipOZL7SZXbl9u152FhAlAFy5uzG3VuyMIJqOr2e28/W4JbK+J5l+KQdjBowU3Eq83Lq1jKKZDXOObr75GOqF++qOJH4f5Y2Nuy/vIRSWW0BuP4olAJZWhMVFf2BK5OmGo0qs35VX2xevv9ZseMGrWr2UxvKjH1sTeWzK0fu7u6fe6mIR1/368GycTWxt/rwucmsDESGheP/xIcAn2c81EJwLJyKwKAwAgNCCQ2LwBBjwGDQQNOwtLbEwd4GKxtrDuzfg52TI04uzji6OKNzSfGf90lppyNldieKZs/LVxUKU6BzL57duYf3mfPcPn2W0Ad3uHRShoaqpNPpWPr3EFNBasexh1KQSiAC/YK46xtB1jQ2pE6mJ0e+bNy4dEt1rCSnfMmspu3Fc9YpTJK4nN2+21SUKlyjCrv/WKQ2UBL2Xfc6pu15f+5SmMT8Pb73hG2H7tKgvBtWehg9vQ9t6ybN1a4SkgFju5sKUo/9oxjz0++KE5mfdk1HcfL4BGwsoHKxdJSvU4H9W/apjiX+pfHg/pwKT0/qwGBSW0dQs8IPUpB6jx1r99Dn54z8Pq4podF6orMVp2Sjehxbu0l1tETtk3pKbdmyhZYtWxIY+HFjrZMnT86yZcuoU6fOh09OYBJDT6lkzin5Ze9W2mTzI7Xdv3u2gW+IgXtPgrh+04cL525z8shFzh6/jO/jZ7F2fzsHO4qWLkDhEnnJk8+d7NnTkc0tFRldbEzV5yM+9hx9Zm+6RodG9zwvCI8ycPraM3ZsO8XyeRu4d/thrOUSH/bb0l9M484DwjSyZ+nA86cyl1RCsXzHFJpXzwHAwF+3MuFnmXw+PmXLl40bF6cB8MAvisypZKn32JLCNS1Dd60H4PGNW0xq1EZtoCTKPU9Wrl+cjoUegiM1UidvSkR4hOpYZi1f0TycPTkBCx2ERUGGdG3wfx6gOlaSZetgzz2fFbg4GKee+KbvEv6ctlJxKvM0ZdEw2jQvxe6HyTh9/h5Tm3UgJlqKHglB8QZ1aDFqCAARoaGs+fEnTu07qTiVeRgy7Qcy1WxIcJSFcdGKHv25dviY6lhmJ056StWsWZOKFSvi5/dxQ61SpUpFjRo1PuUWIhYFP/fjyOr1FGlTkuS6ME6dvcPe3afYsXZvvBQYwkLCOLT7OId2H3/juF6vp1i5wlSsVoJgy+RYpM9Kpvx5sLSyIrVtDNYWGtYWOioWSkPFQrUY+1MtHvhFcejEHVb+tZNNy3fK2N44VLtZNbq1eD0RYr+BS6UglcDs2nbcVJSqWLkwExTnSWradn09ZPLgMW+FSRIf/yc+3Dt3gXJlclG9YhqOVS/FgZ1HVcdKcnoOaofFyyH+2/belIJULLh0+gqHLvpSLr8L90OtKd+0HhvnLFEdK8kaM6u/qSB17WGoFKS+wIAuY3lo/wfp8+QhXY5sVOzQmn/me6qOleSlcXej4aAfTPt//zKeM1KQ+mijv5/M1z9GU6FtCywsLWk3ZQwz233L4+syOiEufFJPqX/PI/WxzHV+qcTQU8qcWNrYkClfbmo0qEiP9qXJmcEBK4t3nxsUoXHswhN+HrKUM3sOExMlqwHFlrQZ03Dh6jxcHIzvRtbuuU2TKn0UpxL/L1XqlKw/t4pHYTacu/SAX79upTpSknL02l+UyJkCgMbtfmfdX9vUBkpkJi8cQt8OJQFYtvUaber0V5woadHpdPy8bQ35c7iQP0UYHeoN4JSsFhcrPCoWp9vcqfhHWuB7/wHj67XA8Bl/W4sv45IuNbfvLCDZy2lU6zadytbVe9SGMnMZcuekz/I/sbC0JCoigkmN2uB7T+ZNU8XWwZ7tZzy5GpOOkGgLjqxez+pfxquOZXZ0ej3tJo2mYLVK6NAo5vCM7o1/5NKZa6qjmY2Prano//Mr72BhYfHJLTYLUjY2NsyfPx8/Pz8ePXpEv37/PelY4cKFOXr0KCEhIRw/fpyiRYv+57lCveiICLxPn2POsOkUcGtBqpQtaN9zMWv33Oah/5tdgB1tdJQunJ5Wk35l5IFttJ00mqJ1qmPn5KgofeKgt7Dg99UTTAWpu76RtK77o+JU4l1ePPNjtdddHoZa4eLuTrJUKVVHSjJsbG0omDUFYByCs2mFzLUT2+ZMXsGr9VOqlc+hNkwSlLN0CVJlSMejUCumr7woBalYdHLvCY7uOQGAS6aMFKhaUW2gJGrqgkGmgtSxK8+lIBULHl69zv6/jL3NrG2sGbdoJDqdTnGqpGvp1omUz2FLm+z+2PvdYcP4aaojmSXNYGDZz7/w+PJlvnYLpJy7nl1e40iTzuXDF4tP8klFKdUmTpyIh4cHlStXpnv37gwfPpzGjRu/dZ69vT1bt27lwIEDFCtWjMOHD7Nlyxbs7e3f8agiIQoJCuGvWX/TpEofMqVsSM78fRg9cy9nbgcRZYDbgdZo6LB1cKBwjSq0HvcLF+6v4vzdFYyd/SNZcmZW/RTMTt1+PbjjkJOjT+0IjYKGdYcTESZDNhKqWydPm7azehRRmCRpadiuNrYvP2s5c/050TJZaKy7cfEmN54YV29NnUxP1frlFSdKWko1eT089ehqWeAitnktfD1kr1InmTMtvjmnd6V2RePKqQYNenaZojhR4rFz9nwsAx7TPGsAnSqlZtxs6eWqwreDOtKwvPF9kI1eY/3UOUTJEOzPFhUewcqfh+OojwTANbklXsdnYmNrozhZ4mI2RSl7e3u6dOlCnz59OHPmDOvXr2fChAn07NnzrXObN29OWFgYAwYM4OrVq3z//fcEBQXRtGlTBclFbLh56TbDek2mWLZWpE3Tht5dp3JiwxZC/PwBsLUwkMUxmvyZHRj4XTluXZ3Ftcd/M81zKPmK5lYb3gyUa92MCu1aAjoOPrShXI0xnD12UXUs8R63TrwuSmWTolS8yVeyMAGRxv91bt0qczPElY1bz5i2u/RooC5IEuPk4ky+iuUACHzmy+X9hxQnSnxuHj/F/UtX0KNRrXQWWn4rCyXEp6rdOrP0dkqOPLVj/V5v6QkYiyLDwjmxYgXp7I0f1vTuXJ6c+bN+4CoRm/IUyc2kX16/psxafIQd62Q1xC91/9YD6tb5hZCXM8bkyejAlkMz1YZKZMymKFWoUCGsrKw4fPiw6djBgwcpWbLkW91DS5UqxcGDB984dujQIUqXLh0vWUXc8n8ewL4121gxZDQjKtVlVsfueO/eQWD46+nRdDrI4WpL73YluHBqInd81zJv9RhKVCymMHnCVK7Z1zQY2Ne0v2bMRM54ycTCCZ33mfOksoqkcKow+naU17b4EpgqOwuup2ThVSf+/O1v1XESrd8nLMXw8iW9aplsasMkIb/O/J7WuYLJlzKcc1u2YYiW+Y7iwtUtG+mY049amYIZPqyF6jhJhmuObHh8XZsog549N2No22CI6kiJzpKZq/A65wuAjSUsWTNCbaAkxMramnVbx+BgZdw/eyuAvp1+VRsqETmx7xQdvp1H9Mu1tioXdWXe6tFqQyUin12USpYsWWzm+KB06dLh6+tL1L8mtfbx8cHOzg5nZ+e3zn306NEbx3x8fMiYMWO8ZBXxxxATw+2TZ5jUeySp7L+mTtNprNx1k2fBb67Ol9nZis6NC3LUawQjtiylRvcupMuZXVHqhGPg+B5sW9qVDPbGf1c7fp/PsTUbFacSHyMiJJSKLs+plD6EstltcMueSXWkRC9leldcs2cFdJw5cRWfBz6qIyVad67f5dqjMABS2eup06yK4kSJn06no3GdgqSzj6Z6hmAenTyiOlKitXPZJsIjjb1Jcqa3o27zqooTJQ21e3+HXm9867Nn/mLCAgMVJ0qc2jcaQrBxpBMeOVPSc1A7tYGSiAUbxpPT1ThZWkCYRt3KP6BpH72emfgIaxZuZPCE7ab9zo0L8dPY7xQmSjw+uyh14MAB0qZNG5tZ3sve3p6IiDfHw77at7Gx+ahz//88kfhsW/0PLav3Ja1jfSrUGMOiDZd46Pd6zpdHoZY4Zs5K9W6d6b/mLwZt/Ztf5v9Cg7a1TX+oJBVjZv/ImAE1sbfUaJQlgAde29g5+0/VscQnOHXuvmm7YZuaCpMkDbnLvu6RduXA4fecKWLDuo2vh0d2+u5rhUmShubfNCRNMuP/B68/CuP4vjMfuEJ8LkNMDPP/ej00ctjI9grTJA2VG1ahZFXja7j/Ex8OLFutOFHi9eD2Q36d/noRkNHDmuCSVhZkiUttezajZQ3jh+0GDTp3nc2je/LBWVyYOGgW89a+nuJk9E91aNqhjsJEicNnvws/c+YMx44dI1euXG8cL1SoEFu2bPniYP8vPDz8raLSq/3Q0NCPOvf/zxOJ24GdR+nUYCCZUjWkWJnB/L78NDtPPX3jHJdMGejd1oO1i7vhF7qeLUfm0KZbE6ysrRSljnt6CwtW7JrGoO/K8Wrk697Tj/mt7xi1wcQn27n99Zv2qtVlhdG4VqRyWdP21QPSiySuzZm4lJiXH/JWLuUuKznFsR69X09wvmjxXnVBkoixP80i8OXnp8VypKBEeZkbMC7N/r0bHXP6USZtCF7zFhIdIRM/x6Vff/yNC/eMvV2dbHV4rpVhTnHFPbc7Mye1Rf/yf5EL1pxl7ZJtakMlct82HsTu08b3lBZ6WPTHd/Ia/oU+uyjVqVMnFi1axMGDBylTpgw5cuRg5cqVnDp1ipiY2J+D4OHDh7i4uGBhYWE65urqSmhoKP7+/m+d6+rq+sYxV1dXHj9+HOu5hHk4c/g8PVsNp1P59vxSuR5rRk/k+tETuFhFkNzaONTP0UZHrVIZWPx7e/yC17LnzJ9892M77JMlnlUbM7ln4NTNJTSr+nqOlnV7valevGuc/LsVcWvdkm2meXeK5E2nNkwi5+DowJQuOWmVzZ881s94ePW66kiJ3gPvh1y+HwJAcjsdX7eqoThR4pXRPQMl86QCICwKfhu9UHGixC80KIQVm4yTbOt0MHqSDAGJK90GdSCHqw2WeshqF8yx9VtVR0oS2rUYTeTLPy1rfZWZBq2qqw2UCOn0ekbPH47jy74Yl++H8F3z4WpDJRF1v+rGpfvGwqt/lCV1Bv2EnZOj4lTm64vGK40YMYIpU6awa9cuLl68iKOjI6VLl+brr2O/m/3Zs2eJioqiVKlSpmNly5blxIkTb42XPXr0KF999dUbx8qUKcPRozJ5szCuKHR45VrmftObn6q1YORvezh7O8g0cR2AvRVULJyG38c35bnfSg5d9qRC0zrY2Jtvgapzv1ZcvDKHQlmM88EZNPht8XEaV+qtOJn4XM8e+/LghXE+sHTJLcicNYPiRIlXs85fY20Bae2isQl5pjpOkvHXsoOc9rVl+a3kGNLlUB0n0fpx1DdYvvyLcOdhb0JDwtQGSiKGff8b4S9nGKhYLD3ZcrupDZQIWVpbMXRgQ9P+mIkbiYqMes8VIracO3Ke+avPmfZn/94NG1trhYkSn6pdO/AkeS623HPEN0SjbpUBGAyGD18ovlhkRCSVS/Vg741IVnsnJ1lGN9pPGYuFpaXqaGbps4tSadKkYdq0aQwZMoTLly8TFRXFokWLOHHiRGzmMwkLC8PT05M5c+bg4eFB/fr16d+/P9OnTwcgbdq02NraArB69WpSpEjBtGnTyJMnD9OmTcPBwYFVq1bFSTZhvp7c92FEn6kUzdaKdOnaM3DsVo5d9TN9sgPG1UOK5HSmwfDBjDq4g56L51KzZ1fK1qmIg6ODuvAfKUf+HBy5+hfzJrc0fZISGgWdei/m+/aj1IYTX+zUhYem7UZtaylMkrg1bFLOtL15k3zAEV9m/bqYPfdteRJmRaHqlWUIXxxp+vXrYQcTRy1WmCRpefrQhy0H7gBgqYex0+VDotg2bMr3uDoZ3+7ceRbB9JELFCdKWvq2G8m9F9FoGjyKTka5lo1VR0o0snoUofp3nQC46mfJV5WGc+fG/Q9cJWLTs0fPaFK5F36+fgDkKOlB0xGDFKcyT59dlPL29qZ8+fI0bdoUDw8PGjduzB9//EH//v1jM98b+vXrx6lTp/Dy8mLWrFkMHz6cdevWAfDkyROaN28OQFBQEHXr1qVcuXKcOnWKUqVKUbt2bZlTSrzX86cvmDB4NqXztMPFuRU9Bq1m37lnhEXB7UBrDJoOCytL3IsUpNq3HVmyYiB+/iu48nAVS7ZO4tsBbRNUT5WU6VxpNLg/3Rb/Qf6sryeYvHgvhCJFvmfxTFnOPjHYue31BwGVq8m8UnGlVGHjv22DBkvnrlOcJukICwzk+tHjgPE1za1gfsWJEp/Gnb4mraPxz8GbT8I5/M/JD1whYtPQvjNNPbW/rpwT5zQyIXRscUrpRK/O5U37Awd6KkyTNEVFRvLNN7NYcTs5ex8no/J3XUjhGn8LZSVWDimS03rcCPQvp7XZMftPbp6QxSlUePHgEQt6DSAq3DhPXdmGNZm2dKTiVOZHB3zWWpHNmzdn5cqVbxwrUqQImzdvZt26dfTs2TM28inj6OhIYGAgTk5OBAUFqY4jFLK2taZwhVJkKFacHCU9SJs1CxY6je55npuGO/zb8xAD1+74ceHifc6cvMqOTYe5e807XrKmcE5O625NiMiQj1ylS5j+Z5U3RThl0wQzfd4+hvScFC9ZRPxIkyE1j+4vQK+DxwHRZEjR8MMXiU9SvEJRju39BYDrj8PJnb6p4kRJi8fXtWk5ZigA+5esZMP4aWoDJTJ7zy+kfAEXAIZO2smYATMUJ0p6dp9ZQOXCqTFoMHjKHsb3n6o6UqIwd/VYvmlcAICztwMpmq214kRJV+MhA/iqeSMALu09yIJeAxQnMm8HLi8hMFkGrgbYcuPYSeZ27YMmw/aUKlClAj1njKKRezDOtjFMnLufn76bqDqWch9bU/nsotR/cXNzY9u2beTNmzc2HzbeSVFK/BenNKkpXq0Mg/vXo3Cu1KSwe/9wkrV3nLj6OBKfW9743n+AfVQQ7i4WXL96j+uXbvPg9iMe3XvyyWPArW2tKVAsD19VLoZHidyULOZO1rS2WOrhr5sp8A03jmmOCA3l8Mo1HF/5N08fylw4idGd52vJnMq4YmRm9295cOeR4kSJy7TFw+jdtjgAc1eeoVuLYYoTJS22yRyYdHgzeV000lsFkz9dQ6KjZWGG2GCf3IkTt1eSK2U0EdGQ1qUlwYHBqmMlOWWql2LGslGc9rXjtrcP4+o2wyCLj3yRjO4ZuHZ9DnaWoGlQqdYY9u+Qodeq2Dom46cNy3FKbSyAL/tpKKe27lacyjxNXjSUvu1LAHD0AdQo0oEg3+eKUwmAsXN/ZmDX0oDxdaf3oJXMGr9EcSq1PramEuszcd29e/etScaFSEwCnz7jn6Xr+WfpegDye+SlRoPylC2Xn/y5XcnsYoPVv3pQPQ+3wM4xGVkKFyBL4QIUSBlG1QwhbzymQTOueBQSaSAyykBUtIHHfpEs2vMYLcaAhZUlFlZWtC3ngltqG5zsLLC3Mq7Y8y55U4Sz/lQQx9dv5tDy1YQGBMbRd0MkBPuO3aHkV/l4EGJFpgL5pCgVy6pWymfaXrZQVm2Kb+HBIZSwfYRHOidAR8uuDfjr9zWqYyUKxerVYvujFBx4GsOjA7ulIKXIoZ1H+f3vC+QpWxrnjOkpWK0SZ7fLG/Yv8dvCQdi9fJez96yPFKQUCw8KZt2vU+gwZQyFncPpvKYPRQpew1vmQPok1RtVplfbEqb9xTNWSUEqAfn527HkzDWLRhUyo9PBtLHNef7MnxULNquOluDFek+pxEJ6SonPZWtnS/lapSnxVQHyFXRn940YXLNnJVX6dACUSRtCidQfXtnIJ8ySZbdSvHGsubs/6R2i//Oa5yEGTl32YcEfW/n7zw1vrUwpEqd8FcvSaYaxi/D+v1ayYcI0tYESkZQuKfDx+QtLPfiHaaSyj/3VZcWHDRjbjfGDagOw++RjqhfvqjhR4jBg3VJcs2cFYPzXLXjqfVdxoqQrW/GidF8wC4CHV64zpVl7xYnMV5osGbl8ZS6pbA1EGaBQkX5cPX9DdSwBrD2ygAalUgNw9NIzvsrfSXEi8+GcNhWXby4kdTLjJ98b99+hQYVeilOJ/6e3sGDv+UWUzZsCgMgYqN9kIjvW71cbTBFlPaWESOrCw8LZudaLnWu93jhubWdLynSufFWxMKW/ykNWd1fSpHEihZMdjg7WONpZYm+tw0oPFnqIecdovhhNR7QBgiM0/EOi8PUL48YtH06fvM4/Ww5x9ujFeHqWIiG5ffocBoMBvV5PNo8iH75AfLRWXRuY5o47dv6x2jBJ2LzJyxn1Y22sLaBMoXRY21gTGRGpOpZZy1K4oKkgdevUGSlIKXbrxGnuXbxM5vx5yZQ3BxUbVmXvOukt9TmqfdeFJbdSUjhVGN6nTktBKgHp12EU1S78hoMVlMqXmq59m/PH1JUfvlCwbvcUU0Hq/vMoWtSKu8XFxOczxMRQvURXjlxeRKHM9lhbwJpV/alaNYij+2Uy+v8iRSkh4klkWDg+t++w7vYd1i1Y/95z9Xo9tg52GNBhYWFBdFQUMVHR9I+OlokMxVvCAoN4dPUGGfPmIl2u7Ng5OREWKEM2Y0Px8kUwaKDXwcb1h1XHSbL8n/tz+oY/pXKnwM4K2nZrxJ/TVqiOZdZa9myBHg0DOo6t3qg6jgAOeC5jyoKfKeoSztezvyWbFKU+WYbcOSlapwYxGuy/GcnYliNURxL/cueaN5Pm7md4T+OqiOPHtGLtsp34+vgpTpawjZjej7L5nQGIiIFG9UcQHhqhOJX4L+EhYVQs+i3HLy0gR1or7K10bNs+grJf9efSWSmSv8s71g4TQqhmMBgIDQohPCiYEP8AIkJCiY6MlIKU+E+3Tp3BQqeR2TGaqo0qq46TaNzUZ2b2lVSsv2XL8nnrVcdJ0laueN31vU37qgqTmL+0GdMwokMBuuR6QWFHP87t8vrwRSLOndnpRQ6HYJJZGXBPa0uLTvVURzI7dX94vfr3rrkLCQ8Oec/ZQoWRfSZz+aGxoJLcTs/KreMVJ0rYvqpakoE9Kpn2h4/fwqlD5xUmEh8j4Lk/FUr04IG/cdGK5HZ69h6YSOasGRQnS5ikKCWEEImAY6gP3fM8p6l7IF06V1MdJ1FIm82dlOlciTTo2b77Iv7PA1RHStL+nLac8JdT6pXMnxY7B1u1gcxY/1++wcoCHKw0wh7dJTpCPnFPCDSDgTkL9pr2h/3SWl0YM9Sg/deUKF8UgOcPHnJ41TrFicS7aAYDrZuOJOLl63mlouno/qP8rr+LYwpHVq0ehLWFcf+fU4+ZMHiO2lDioz2595hKZfvzLMQ4x+/DSAcajx2FnZOj4mQJjxSlhBAiEfhnw170L1djLJY/vdowiUSecq9Xkr1y4IjCJAIgOCCYk9deAGBrCe17NFGcyHy1bFTctD3l12UKk4j/N27g7/i+fAOTO6MDDVpVV5zIPOgtLPhtakfa5fCjYrpg9syZT0xUlOpY4j+cO3KeSfMOmvbHj2pOZvd0ChMlTN/8+hNO9lYA+ATG0LjqD4oTiU9169JNqlQezN47Ov555EDGfHn4dt5v2Dk5qY6WoEhRSgghEoEn95/wyN/4sWO65Jaky5xWcSLzl6dcadP2lQMyn1RCsHzZXtN2qzYyTPVz1GtZnfQpjB+733kWyZ5NBxQnEv8WFRHB3MWv36yPHNNBXRgzMmh8DzKmtMRCB6n1QRzftFN1JPEBw3pM4Mwd4/BKB2sda7aNU5woYSnZqB4ZylZh6a0U3PTX06zprwT6y4rw5uji8Qu0rNKTQF/jB2uZ8ubmOylMvUGKUkIIkUicuvAQAJ0OGretrTiNeUudzplxbbJQPUMQKUIf4nvvgepIAlj02yrCXg758MiTGhtbG7WBzFDv/s1M20tWHlKYRPyX0f1n8CLMuJ0/iyO1Gld6/wVJnINTMvp1fz3P3M9DlmKQOTgTPE3TaF5vCMEvF1LN5u5Cufoy/QBAxry5aPizsVdUaLSe5q2mcWDnMcWpxJfwuX2H2Z16EOj7HIAiRbNx7Y4nhTxyK06WMEhRSgghEondO0+ZtqvWKKYwiflr820jHK018qWMIHXUU9VxxEthwSGs3X+ffx46sOhGKtyLF1Udyay4uDpTrpCxF2VENEwZ8afiROJdIkLDmL/sqGl/zPjOCtMkfBP/+JGUdsbx6xfuBrPk9zWKE4mPdfPiTUZM2s75F7YsuZmSyn1645AiuepYSjm7pqb95LFY2Rg/dDmwdBVntu1SnErEhqfed5ndqQd2Yb40yRJAxpSWHDg4gYo1SqiOppwUpYQQIpFY+9c2NONUJBTLL6t7fIm6X5c0ba9fc/A9Z4r4Nm70cs772REWo6dwDVmF71P0H/mNacJcr1MPZfL+BGxUv+n4hxu3C2dLTpW6ZdQGSqAyuKWjfeMiAGga9Ok+U3Ei8ammDJ7FjBXniDLocErtQvNRQ1RHUkav17P7yG90KuOArYWBO+cusGnSDNWxRCx66n2X33v+jH+YsTdnMhsd2zYPoVGbGoqTqSVFKSGESCQe33vCw5fzSqVPIfNKfS6dTodH3jQARMXAyj83Kk4k/u3qwaOEBQUDkL9yeSytrRUnMh+tm7wutk4bv1JhEvEhIYHBLPr7de/X0RO6KkyTcM3w/Bk7S+P23rNP2btVhqSao1XDfyXouXG+nXwVy1K+TbMPXJE4zVszhkJZkuHuGEX9jC9Y/MNgYqKjVccSsezi8QuULt4H7+cxANhY6ljh2ZPvB7dTnEwdKUoJIUQicvriI8A4r1SjNrUUpzFP1RpUwNHGOBTk8r1gQgKDFScS/xYTFcXFPfsBsHNMRq6vpNv7x6jbvDoZUhrfvd99HsXOdV6KE4kPGd57Cs/D4LSvLWcMWUiVUVZW/bfCpfJTt1wWwPgBQs9O49UGEp8tyPc5ywePAsDJKoZlczrQqks9xaniV/dBHejYID9g7PU3ZMgSAnyeKU4l4srda96ULPANFx9GAGCphymjm7Jw9Uh0Op3idPFPilJCCJGI7N5x0rRdrYaHwiTmq3nb112od++5oDCJ+C/ndu4mg30UldIFM2O6zLfzMTKVLM3JZ3aERev4a7msJmkOgvwDadN/PfueJCNMs6Zql/aqIyUoM+f3x/LlO5m1/9zkytnragOJL3Lt0FGubVpH6+z+pHeIYc7Mb3DLmjQKsRVqfcXkUY1N+3+uu8Ci36Q3a2Ln+/gZpfJ0YP/l10Pp2zcuwuELf+KU3EFhsvgnRSkhhEhE1v61HYMGLyIs0Cd3Vh3HLFUok8O0vWT+JoVJxH+5efQUNTMEUNg5nHL5XcjgJkNV38c2mQOZylTggI8Dv520ZsKQOaojiY+076+VhAUal4H3qF8bl8wZFSdKGHJ4FCJnNuMw65Ao6NNxjOJEIjYsGD6VW0+Mk6kls9GxZc8k9PrE/XY1k3t61qz+CZuX8/0dvvyCro1/VhtKxJvQoGAqF2rP/I3XMbycF7ZkvtScub6IZKlSqg0XjxL3v3IhhEhiHt17zMitgXjeSMk1MmGbLGl90vKlMmZJj3tq44o3PoExnDsqPaUSoqjISP45dg8ACz30GdxBbaAErmidGtjY2wFwYvNOggNkSKq5CA8KZu/i5QBYWFrydR+ZWwqg5vc9WHIzBXseOfDbgkM8feSrOpKIBdFRUTSq9TOBxhFN5HVzZPGGsWpDxSFrW2t2HZpOKnvjW/J7L6KoVaan4lQivhmiY+ha/wf6jt5K+MspxC6Hp6b1uBFKc8UnKUoJIUQic/6gcQif3sKCbB5FFKcxL22+bcCrofyHT99XG0a81+xpq03bTRoUV5gk4SvdtIFp+8jf65XlEJ/nwF8rCfd/gYdLKDN6F6VBq+qqIylVtE51shQqgIaOHSeeMLTnRNWRRCy6ceE6vQeuMPUaaVU3H31+TpwTQG85MJOc6WwBCIqAWpUHEuQfpDiVUGXGsNnUaTqVnTfheqANtsmSqY4Ub6QoJYQQicyNY6/nlcpeUuaV+hRVqhc1ba/7e7/CJOJDdq/z4mGAcUnlLKltKF62kOJECVPtZtWoXzYD9pYG7p67yOPrN1VHEp8oIjSUmIsHKOcaip2lxviJnVRHUsbazpY6fXuY9jdOmI4hOkZhIhEXFk9bytw1F037E0Y2pVzVxPX3zNh5Q6jikQ6AaAO06zyLK+dkXrSkzmv9Hhp5tGbJT8OZ3+MH1XHijRSlhBAikbl18gyGGOMf6YXLSQ+Sj6XT6TgU5MqyW8nZ/8CK1Ys2q44kPmDTjtfDK3sPbK0wScL1488tKZs2lC65XhB59bjqOOIzje43nachxq4jOdI70K5bQ8WJ1BgzdxA53FIBcHnfIa4dPqY4kYgrPZsP5uCVQACsLGDd+iGky5hacarYUaxuTexK1+ayn3G6gGGTdrJh6XbFqURCERoQyJmtOwnx81cdJd5IUUoIIRKZ8KBg3KLu0ja7H8PrOJG7QHbVkcxChjy5cHR2xifMCs+1ZwkPDVMdSXzA9LGLTUM86lTKlSSXUX4f10xpKV3A+CYuMgaWzlyhOJH4XJFhYUybs8e0P2pk6yT3+17AIy99WheldTZ/KqYNZOOk31RHEnFIMxioV74X9/yMPWJTOVjgdXQG1tZWipN9mVxflaT5yMEYNB07Hibj+wlejPtphupYQiglRSkhhEiEIp49wcXW2FuqWcfaitOYhzzlvzJtXzlwRGES8bGunbvO5YfG4mEKez1N2svv+r/9NLorVi//0ttz/L7MVWLmJgycYRqymsnFhr5DO6gNFM/++GsglnrQ6eDO5es8u3NPdSQRxwJ8X9Dg6xEERRoLsE8tUlOj93eKU30+twJ5aT91LBZWlgAcWrGW336aojiVEOpJUUoIIRKhLesPmrarVi2sLogZyV22lGn7qhSlzMbKvw+btrv2+FphkoSnRYNipu0Jo5YoTCJigyE6hrETN5r2B//UADt7W4WJ4k/rbo0pmdu4PHpwJHRvM0pxIhFfzh48Q4fv5rP1rj0nfO2p2L4VZVs1VR3rk9VtVpWTx8aTLqU1AOd3ebHuVylICQFmVpT69ddfefr0Kc+fP2f8+PEf1W05W7ZshIaGxkM6IYRIODYs3UHky7lfC+V0URvGDLjnzMwP9TNRzCWUqMfe+Ps8VR1JfKRZv3oS8fJ3vWCe9KZPoJO6ph3rkdbJAoCbPhEc2H74A1cIczBn7AIuP4oAIKW9nkl/DFCcKO7Z2dsyZXxb0/6MPw/g80Beo5OSdQvXM6rfNNN+/Z++p0ht81mFslqDCqxa2gdnO2jqHkDQ9QssHfQLmsGgOpoQCYLZFKX69etHq1ataNiwIY0bN6Z169b069fvvddkzJiRzZs3Y2dnF08phRAiYQgLCeX6I2NB3tFGx1dVZMLz92n7XQMyJ4umvGsobviojiM+gf8zP/7afZ/V3k4s9XYmZ+mSqiMlCH1+aGza9lx6QGESEZs0TaNfn7mmudQ6NS9BZvd0akPFsZlLhpHa0Vhg9X4WydBekxUnEiocW7uJXXMXAqDX6xk2cyBT5/+kONWH1WxUkfWr+mP78vOSe8/CmfrNAKIjItQGEyIBMZuiVJ8+fRg2bBiHDh1i7969/PTTT/Ts2fM/z69fvz6nTp0iQv7BCyGSqAOHXy8t3Lh1NYVJEr5atV4Pc1q9wkthEvE5pv26jPsh1mjoKF5f5pXKlicLJfM6AxASBdNH/qk4kYhNO1fvYu/55wDYWMLUPwcqThR3ipQqQNv6BQAwaND1m99Mq8uKpGf7zD84tGIN2RwjqJslhD6dyzJ1fsLtLdjym/qsX/UDdi/nZr/pE0mZQt8Q8CJAbTAhEhizKEqlS5eOzJkzs3//ftOxgwcPkiVLFlxdXd95TZ06dRg6dCh9+vSJr5hCCJGgbFi117RdoVxedUESOCtrKwplNy4xHhYNG5ZsU5xIfKqrh44Q9PwFAPkqlcMhZQq1gRQbMu47LF7OcLB1322CA4LVBhKxrkeH8YRE6Tj21I47TgVIkTaN6khxYtHyQVi+fLey6cA9/tmwT20g8T/27jwuqur/4/h72EHBDWVxQcS9RStNS8q0zTTTstJWs2y3b2WWlpXftm+lLdpqZZlaWflV61tW1s9wTzMTMzcQFUUWBVF2ZDm/P3CmCFBUZi4Dr+fjcR7N3Dlz7/s2DMN8POdcyy38z6vK2r7J8TvuwTsu1FuzHrc2VCXGPDFSs6aPlk/ZID/Fpx3R+WfdrcwDmdYGA2ohtylKSVJycrJjW1pa2fSKVq1aVfqcu+66S++//77zwwFALfV/Xy9VblHZ7a5tG8nLy9PaQLXUNbdc4RhWv3HHIRUfKbI2EE5YaXGJfvv6O0mSl7e3oq+70uJE1vHw9NQV/cuK0KVGen4ifwvVRdtjt2rUpP/T6v0NJF9/DXr4Pqsj1biHnr5DZ7QNlCQdLpBGX/+UxYlQGxhjNHLAvzT357+uvnjfredrwU+vyNOzdny1nTrzCU17/lpHQTV2d656dr1d6Snp1gYDaqna8c6V5Ofnp6ioqEpbw4YNJancVDz7bV9fX0vyAkBtV1pSos27yoaI+3lJA4b1szhR7XT9TRc7bn/33W8WJsGp+PWrb9WxUaGubXtYc1+/Th4eteZPHJc67aJofbknWEv2NdCPGzK06dfNVkeCkyx6+0PlHir7HX/2oMsV1fNsixPVHL+GDdRuwGCl5JX9i8FzryxSRtpBi1OhtjDG6OZLH9CcH3c5tg29pJNWbfxAgUENLMvl4empH9a8q3/ddp7s1+Na8edBndf1NmUxZQ+oUq35i61Xr17asWNHpe3cc8+VVL4AZb/NlfUAoGpLl5d9IS0plc6/mAWgKxPdM0JS2aiSWW/PtzgNTtb+XYlqo/1q3bBIjQM8dPM9Q62OZIk+N1yrolKb/sj010Nj3rE6DpwoPytb370x3XH/rhcfU7PgRhYmqjlXjh2josAQfbGzkd7/KVWvPTX9+E9CvWJKSzXy8n9pyqzfVXL0InbnntZC2xNn65xeXVyeJ7BZU9357msyLTs7tn2xZLf6db9dhfkFLs8DuJNaU5RatmyZbDZbpe3TTz+VpHLrR9lvp6SkWJIXANzB7Pe/0YLdQXpnazPtLGlmdZxa59wLuqt5w7JpjTv3F2rvziSLE+FUzJq91HH73vuvsi6IRUKiItWhVw9JZUW6+DXrLE4EZ1v736+VtOlP9Wqep4f7NdCHXz5jdaRT1qFXD5133VBJZVeSfXwk0/ZQtfG3TdJDz3+vgqPr34c29tGqlS/r1vuucVmGrn2j9cj8Oep43rnacshPa1J9NfHVn3XDJQ+wMD9QDbWmKHUsKSkpSkxMVHR0tGNbdHS0EhMTlZqaamEyAKjdtqz7U3/sPKxiY1O7s7vL08vL6ki1yq13D3HcXrJ8u4VJUBOmvzxLOUfKbvfo3Eyt24ZZG8jF+t1yneP2qs/nyxhjYRq4gjFGy997Xz2C8+TlIQ2+KEoXDzrP6lgnrWnzJhrxzF+LVi96/R1lpvC3Po7t7UnvaNC1U5Vqv6aDzUPnj3lIg8c9IC8nLvVyVq+uWrByuu54a7ICm5VdMCXrQLpuGvSoXhz3utOOC9Q1blGUkqR3331XL7/8svr27au+ffvqpZde0rRp0xyPBwcHq0ED6+YQA0BtFb+2bJ0k3wB/tTnzNIvT1C6X9Pvr/8cnM761MAlqQl52rn5as1eS5OkhPfrM7RYncp2WEWH6aOIFurRlthopR7/97zurI8FF1i9bp0+/j5ck2WzSRx8/Im9v9/wHiPk/TtG/+vorxL9IO9b9rl++XGh1JLiJmK+W6KzT79Yv8blakRag3BJvXTTyRj321afqPuASeXrW3MVewls119xFL+vX1S9raJ+W6hhU9q8hf/68TK8Mu0W7ft9YY8cC6gO3KUpNmTJFX3zxhRYuXKh58+Zpzpw5ev31vyrQ69at07hx4yxMCAC10461fy3e3bF3TwuT1C6+DQL0U0YLLU5qqLV7SrTqx1+sjoQa8PqLnzluXze0h4VJXOvJl++Rn5d0epNCBR+KV0FOrtWR4EL/unmSUrPLRsa1DvbV23MmWpzoxI179k717R6iZn4lujrisBa9/Bqj/XBC0hKTFd35Rr3wxAwVHb0oVrNWLXXXa88o9dB8vTFzgkLDmp70/rucHqm5303Wrt0favjArrJf7O/Mxjn6ctJ/NPPBCcrNPFQDZwLULzZJ/LavRGBgoLKyshQUFKTs7Gyr4wDASWsc0kJzf/tCbQOPyJafrc5h11odqVY489J+GvnafyRJKz+bp4UvvmZxItSUuLQFat/CW5J0zU1v6qvPfrQ4kXP5+Poo7dB/1cjPJmOk8y95Vmt/Zj2p+mbY6GH6/P3b5Gkru3DDxZc/q2U/ucfPwRnndNava6bI9+gAr39PW6pnH3rV2lBway0iIzR0wsPqdH4vXRiao3OCyxYbLy41Wrc5Tf/9fJm+nP2D9iWlV7kPm82mrmdE6qY7r9JVg3uqa0RQuceLS6VvViXpnhv+rQP70px6PoA7qm5NhaJUFShKAahL9hz8Sq2alA1dj+p4v3bF77E4kfVGPP+Ueg4ZKEl6/+6HtH31WosToaZMevNRTRpzoSRp9Z8HFH1G3Z7G9/hL9+qF8WU/y7G7c3V25AiLE8Eq85a9q2EXtpIk7TtYqKiwETpypNjiVMfm7e2lrXvnql2InyRp9dZDiu56i8WpUFd0PO9cffzJo+oV6SebreLjh3KLlZSWo9T9h7V4RYLW/ZmigEZBahwSoosv7Kw7Lgiq8JziUmlJbLqeeOgtbVix3gVnAbin6tZU3Gb6HgDg5K3+PdFx+6a76t9Vyf7J5uGhLheULQZckJurHet+tzgRatKrT013LHje+7TmatehlbWBnOy+uy513J780jwLk8Bqo4ZOUMrRaXwtm/rqk69fsDjR8X04/3lHQepgvtG1lz9qcSLUJXG//Krzo67TJde+oe/WZyi3qHxlqnEDL53errEu6R2hq265UkMefVCX3jVKPYcMVEnziHJ9M/JtmrN4p7r3fkJXnDOKghRQQyhKAUA98NW85Y7blw84x8IktcPQmwboqtO81bbhEe1cu04lRUVWR0INyjmUrR9WlRViE3O8dd6QARYncp6b775aLZuUTVVMPFisz9+bb3EiWCkn87Duvv8DFZeW3b92QFfdMeYaa0Mdw12P3KCbB5ddcKLESHeN+VCpe7naHmpezIKfdGWP2xQeerPumvi1vlmbpp0HpYKSSoZPHZVdKK3fW6RPFu/UkFEfKqzxNRo54EFtWbfJhcmBuo/pe1Vg+h6AusTX30+ZWfPk5yXlFRkF+Q1VaWmp1bEssyDmDQ29KFKS9OQrP+k/j75hcSLUtK49z9Ajn7yrQ0c8tX9XoiYPuaFOLpq8cddnOqNtoCRpwsuLNXnCWxYnQm3w9vyXde81XZVR4KlP1+Vp/BW3qjA3z+pY5fToc4ZWLP2PYx2pGV9t0V1Xj7c2FOqdhk2bqHWHtorq1EY5BSU6dDhfeVlZys7I1IHEvSo+umA6gBPHmlKniKIUgLpm3Y7PdE5U2ZfXQde+ru/n/2xxIuvsz/5KwQ09VWKktu3u0b7d+6yOBCe498O31P7cspGB7931L8X94h6LPlfXBZf20rIfn5QkZeYbhTa+VkVHjlicCrWBp5eXPls9W6kBbVVsbIpdvERzxj1pdSwH/wYB2rnvU4U0KqtIrduRrd6dbpapx/9YAgB1DWtKAQDK+TnmT8ft6266xMIk1urV9ywFNyxb9D0hrZCCVB226vO/prL1GTHMwiTO8e+XRjtuf7pwAwUpOJQUF+veqx9R9uEcSVL3yy/WJXePsjhVGZvNpmv//bg25jRWqZFSs40GXfggBSkAqKcoSgFAPTF35iLH7QvO72BhEmvdevcQx+0ly7ZZmATO9mfMch1OOyCbjAYPPle9+55ldaQa06BxI+U3iVBmoYfyi6Vnx71pdSTUMgf3pWjuxGcdU7UHP3CnHn72LotTSYMevl/dB1yizZl+mhfnq2uvfVHpKQesjgUAsAhFKQCoJ2JXb1RadtmXk8gWfgoJD7Y4kTUu63+a4/acD76xMAmcrbS4RKmrf9aojpkaGpmrl16/z+pINeaCm4dre26gPo5voken/aL0lHSrI6EW2rx0pb6b+o4aeJXo+naH9OpTg/XwxFssy3PBTder36ibJEmlJSV69q5ntPrHXyzLAwCwHkUpAKhHfv0jWZLkYZOG3z7Y4jSuF9mhjaKOXno8LbtUa5astTgRnG3+u5/K21a2fGZ091B1Oj3S4kSnzi+woaJvvE6SVFxUrM9e+dDiRKjNYmZ+Kp/EDQrxL5EkTXnuet398HUuz/Hp/17UUy/f67g//4VXtHX5KpfnAADULhSlAKAe+fSTn7U8NUCz4xtrb0kTq+O43J0PX++4vXTNLguTwFUyUg7om+Vlr7WHTXrh9QcsTnTqom+4Vv6BDSVJv339nQ6lplmcCLXdnYPHKmZzlqSy98Fbr9yqW+++ymXHn/vtS7ph8Om6pGWOTm9SoB/f/VBr5n3lsuMDAGovilIAUI98PWeRfk3xUUahlzr16W11HJcbPOgcx+1PPvrewiRwpafHvqWio2soX9mvg4JbuG9BNrx1iF597loF+xarpLhYP384x+pIcAMlRUUaeO5orY7PlSR5ekgfvXun06fy+fh46cdf3tbwQX9Nm96yfJUWvzPDqccFALgPilIAUI8U5uZpV+wfkqTmEa3VrFVLixO5Tkh4sDq3KhtdkpkvffflTxYngqvE/7Fdy/4oW3PJx1N69jX3HS318jsP64wWRrd0OKTGyRuUkcTVI1E9hXn5uvScO/TbrgJJZSOmXn3+er3x4WOy2Ww1frywlsH6PW6WLundRpJkjDT1s1g9fOOTNX4sAID7oigFAPXM9lVrHLe79u1jYRLXCj3tdP13dyOtT/fTohW7uPx4PfPM4x+qtGxpKd107Tny8/OxNtBJaB7aVNcOKBtxUlIqvfMCa0nhxORn5+qis27X0i3Zjm1jbr9Av/zxgZoFN6qx4wwdcbG2xn+orhFBkqSiUunfb6/U2JueqrFjAADqBopSAFDPbF66Us39itW7eZ5efGqo1XFcpmu/C5Wc563lqQ31wrOfWR0HLrbqh5XasDtPkhTo66GJf1tw2V289sF4+XqV3V72R7piV2+0NhDcUt7hbF121m36YlmyzNFC7bmnh+g/X05Vi8iIU9p3k6aBWvDTK5r/2UMK8i/7mpFzxKZRD32m5x54+VSjAwDqIIpSAFDPpCXs0iUhB3VeSJ7O7xSoiHbhVkdyOk8vL53WN1qSlJ+dox1rf7M4Eazwn+c/d9wec2d/+fp6W5jmxLSNaqnrr+gqSSoulcb/6y2LE8GdFR85ohv73aPxry1VXrFNO7O8ldO8kx6ZP0eDHr5PDZo0PuF9drngfH2+YoaGXtJJ9tmA8QdK1Oeiifrszbk1ewIAgDqDohQA1EMrf9vjuD3y/mEWJnGNqJ5nyz8oUJK0dfkqlRQXW5wIVlj40UJtSiqUJAX6eWjkgzdanKj63pg5Qd6eZbd/+i1V61estzYQ3J4xRq+Me1WXDn5Z/12fI8kmL29v9b/9Fj21eL5id8zWC6/dp86dW1W65lRQUIC6dOug6Buv09gvZ2n0O69qp0crFZdKR0psmvXDTp3Z9kZt+oURfQCAqtkkGatD1EaBgYHKyspSUFCQsrOzj/8EAHAj194xVF/OuEOS9Nv2TJ3b+VaLEznXrB+mqeUZZyghy0dv/+sp/fFTjNWRYJGrRw3V86/dpzX7A7T5jwS9fv1tMqZ2/yl0xjmd9Puvr8jTQzpSIp1x1ljFb4q3OhbqEG8/X11y1yhddNuN8vL2VmTgEQ2NyHI8nn+kVPsz85Wde0Q+3p5qHOir4EY+Ssjy1bd7g8rtKyRvt9566m2t+XGlq08DAFCLVLemQlGqChSlANRlXt7eOpizQA19yr7kNg68VgX5hVbHcgofH28dylkgP2+poFhq1mSE8nNyrY4FCz342Ydqc0bZVLiPH5qgTUuWWZzo2JbFztAF3UIkSV8s2a0bLnHfqweidmsU0lwX3XaTHvvXAJ0XXnTc/tlFHpqxvakkac+mLVoyY7Y2xyyv9YVeAIDzVbemwvQ9AKiHiouK9HvcQUmSj6d0wx2DLU7kPCNGXyW/o0sHxe44TEEKWvzuDMfty++/Ux6enhamObZB1/ZzFKTyiqRH7njR4kSoyw6nHdDXL09V/w7X6Jq75mjeihRt2V+qrCMeKv7bBUsLSmxKzfVQbEKWYmZ8rMlDbtC0G+/Qnz8voyAFADghXlYHAABY45v/rdWFp18hSRo24iLNfOu/FidyjltGXea4PX/BaguToLbYtuIXJW78UxHdTlerju101xN3aPpz71sdq1K9rh+mtHxPhfiX6MP/xio5MdnqSKgHCvPy9NUHX+qrD76UJDVo0lgNmzZR4+AmKsjLV1ZGpg7vP6DS4hKLkwIA3B3T96rA9D0AdV3zliHat2eGvDykQ3mlatpgiNWRapyvn48ys/4rP2+bCkuk0JBbdDjjkNWxUAtEnnWm3vjqDV0Qmit/z1Kd1vU+7Yjba3WscjpfcJ7ufOc12WQUXrpft51/gwpy862OBQAAcFxM3wMAHNOBfWnanlIgSWoc4KF+A3pbnKjm3XLP1fLzLrtq1Pr4QxSk4LBrwx/y3L9TQT6l8vaUpn/yhNWRyvHw8tSQRx+UJBnZ9PKEtylIAQCAOoeiFADUYz8u+dNxe9R9Q60L4iQ3j7zUcfvLL1ZYmAS10f23vai84rKiZf+erXTxgF4WJ/pLv9tuVIvICEnSrt83asP3P1mcCAAAoOa5VVHqxRdf1P79+5WRkaGXX35ZNputyr69evXSqlWrlJ2drW3btumOO+5wYVIAcA8z3pin7Yd99O2eQG3Ma2Z1nBrlH+CnXqeXLRBdUCx99PpcixOhttm1OV6fLtrmuP/h7Efk5WX9oueDhl2kT6eNUKh/kUpLSvTVy69bHQkAAMAp3KYoNXbsWN144426+uqrNWzYMN10000aO3ZspX1DQkL0/fffa+nSpTrrrLM0adIkvfnmmxo4cKCLUwNA7bZ1/RbNWLJf8Vm+Cj/tdDUKaW51pBozasy18j16OY912w4q5zDrA6KiR0Y9p5SjPxptmvvrpbcesjSPn5+PPpjxL7UIKNWIdod1cPWPStqy3dJMAAAAzuI2RakHH3xQTz/9tFatWqWlS5dq/PjxGjNmTKV9hw4dqtTUVE2cOFE7duzQF198odmzZ+vGG290cWoAqP02/RTjuH3GxRdZF6SG3XBzf8ftuXOXWZgEtVlO5mE99uTnKj162Zcxo/uqU9e2luV5e86TCm3sLUlKOlyq1x99xbIsAAAAzuYWRamwsDC1adNGy5cvd2xbuXKl2rZtq9DQ0Ar9f/jhB40aNarC9kaNGjk1JwC4o41/K0qdeWk/C5PUHA9PT63NDdWS5AaKz/TU7De/sDoSarFP3/hUMZsOSZJ8PG36/OtnjrlEgLMMvPpCjRx2liSpxEj3j3lfBbl5Ls8BAADgKm5TlJKk5ORkx7a0tDRJUqtWrSr0T0xM1Nq1ax33mzdvrhEjRmjJkiVOTgoA7ictYZcy9ySqfVCh7h8Spe7ndrE60inr0KuHbA2b6I+D/nr+w/XKy861OhJquTuue0pZR8oKUd3aN9ULUysfje0sTYODNHvOWHkcrYXNX7ZX3366yKUZAAAAXK3WFKX8/PwUFRVVaWvYsKEkqbCw0NHfftvX1/e4+50/f75SU1P13nvvOe8EAMCNtS7YrcFtstWlyRHd/dD1Vsc5ZecMHuC4zVXLUB174nbr36+V/awcKbGp4yWXqUGTxi47/tf/94qaNihbZH1XRolGDX7UZccGAACwSq0pSvXq1Us7duyotJ177rmSyheg7Lfz8qoe1t6gQQN9++236tixo6688krl5+c79yQAwE199PZCx+0rLj3DwiSnzjcgwLE2Vu6hw9q6fLW1geA2pj3xhj5dvl+fJDRWYmmwbnjhKdk8nP+n0rSPxqtPt7JR4QXFNt04/GXl5zC6DwAA1H21pii1bNky2Wy2Stunn34qSeXWj7LfTklJqXR/gYGBWrx4sU4//XT1799fO3bscP5JAICbWvvzWiUdLpUktQn2VdczoyxOdPKemnyvBrQrVph/kWJ/+D+VFBdbHQluwhij+655VEkphyVJXS44X1c8cLdTj3nrPVfpgVHRR48vPfNmjNYu+cWpxwQAAKgtak1R6lhSUlKUmJio6Ohox7bo6GglJiYqNTW1Qn+bzaYFCxaoXbt26tu3r7Zs2eLKuADglv5vxV/F+wcm3GxhklNzy4jz1a1ZgUZEHdaB2F+tjgM3k51xUJ9O+LejmHnx6Ft01+O3O+VYjUND1G/0KBWV1YP15bIkvTz2VaccCwAAoDZyi6KUJL377rt6+eWX1bdvX/Xt21cvvfSSpk2b5ng8ODhYDRo0kCTdcccd6tevn0aPHq1Dhw4pJCREISEhatKkiVXxAaDWe2/qPMftq67oZmGSk9en39lq3cxHkrQns0QrFi0/zjOAiuLXrNP/prwhD5vRJeE5mv6fq/XurAk1eoyGTZvo7ven6aBvqBbubqRfEvJ1y4CHavQYAAAAtZ3bFKWmTJmiL774QgsXLtS8efM0Z84cvf76647H161bp3HjxkmShg0bJk9PTy1atEipqamOtmDBAqviA0Ctt3bJGu3KKJEkhTX21gWX9LA40Yl7YPyNjttf/7DJwiRwdys/m6e8Dct1RtOyC6vcfWsfvTmjZhYfD2rRXPfMeFMtIiMkSbFbU3V5r7tV/LcLugAAANQHNknG6hC1UWBgoLKyshQUFKTs7Gyr4wCAS7z+ybN68KazJEnzFm/X8AHjLE5UfZ6eHsrIWaggPw+VlEqdu41Vwp/xVseCG/Pw9NSHP7ypkZe0dmxb9PN2Dbtigo4cObm1yu761zW6dexIrc1uLsmmQ6lpeuvWe5SZUnE5AgAAAHdV3ZqK24yUAgA431svzVHJ0fVtLruwgzxccOWxmnLDHYMV5FeW98+9+RSkcMpKS0p0xxUP6LOlyY5tg/p30sa4merctc0J7atBAz99ueg/mj5tlM6P8FDP4Hyl703SO7ePoSAFAADqLff5tgEAcLqEP+O1ZV+BJKmRv4cGXd/f4kTVd/+DQx23P/18hXVBUKeUFpfo1kvu05TPNjsWJO8U0VixG9/SWx+OU1BQwDGf7+XlqXsfGqbE1M907cAz/tqed1Bv3nKXMvYmOTM+AABArUZRCgBQzqzPVurXA/6aHd9YpWEdrY5TLRHtwtWzS7AkKfuITe+8MNPiRKhLSktKNP6mCbr3qW+VWVD2p5OPl0333d5Xe9M+U6+rB6lhs7KLqXh4eKh9+zANvSZaH33+lFIyvtTbr9+mpg29JUlFpdJ73ybootNvVU5GpmXnBAAAUBuwplQVWFMKQH3VsGkTPb3kf/L08tLhtAN6/vKrVVpSYnWsY3rn06d1z409JUnzl+3RdRfdb3Ei1FXtunXVh18+o+j2fvL0kLZk+mrxvkBJUnbGQSk/W08PaFDpc+PSjcY9+rG+/ZgLrwAAgLqNNaUAACcl52Cmtq5YLUlqFNJcnfr0tjjR8fXrd6YkyRjp1ec/sTgN6rKdG7eof5fhGnb3J/plV5H+OOjneCywWVM1bl1+rakSI21Pl8ZOWapuESMoSAEAAPyNl9UBAAC1z9r53+j0fhdKknoPG6yty1dZnKhq4Z066LuMcG0qKJJ/dorW/N8vVkdCHWdKS/W/GV/ofzO+UORZZ6rLhX0UedaZahwWIv/AhtqUJqWl52rT5j2aN+sHrftphUqKiqyODQAAUOtQlAIAVLBt5S8qzjyg3h0aaORdZ2jr/G5as3yj1bEq1fvaIZJs2pvro/8+/4XVcVDP7Nrwh3Zt+KPctictygIAAOBumL4HAKigtKREDVL/1AWheWrqV6rHnh5pdaRK+TVsoHMGD5AkFebl6/fvFlucCAAAAEB1UZQCAFTqlX9/qKLSstuXX9he/v6+1gaqxIDbrpNfg7JFpdd/+4MKc/MsTgQAAACguihKAQAqtXtLgtbvKLtShr+3Tf96/BaLE5UXGNRAn752ja6PPKR2gYVa8QlT9wAAAAB3QlEKAFCld9/9znH7vnsvtzBJRROev1P+3ja1bFCspnlJ2r8r0epIAAAAAE4ARSkAQJU+fWuukg4bSVLrYD8NGd7f4kRlbDabRo+80HF/ygufW5gGAAAAwMmgKAUAqFJpcYk+/vwXx/0nJtWOKXy33TdUzYO8JUnx+4v17affWpwIAAAAwImiKAUAOKYpT0xXVqFNktSjc7BO697e4kTSxCdHOG6/9+HPFiYBAAAAcLIoSgEAjin7YKa+jkmQJNls0nNT7rU0z/W3DlC70ABJUkq29MYz71uaBwAAAMDJoSgFADiu58a/o6JS6UiJTUGR7eXp7W1Zlmeev9Vxe/qsFSouLLQsCwAAAICTR1EKAHBcO/7Yrne+3aMP45poY34LnTv0SktyXHVdP3VqHShJOpAnTR7/liU5AAAAAJw6ilIAgGp59fE3VVBS9rHRf/Qt8vTycnmGKa/f5bj94dxfVZiX5/IMAAAAAGoGRSkAQLUkbdmmLctXSZKahoep59BBLj1+eKcOWpMXrsQcb+3PlZ4f+7pLjw8AAACgZlGUAgBU24/vfiRJ8vUs1Suv3K52HVq67NgDH7pX6YU+WrC7kW5/ZJ7ysnJcdmwAAAAANY+iFACg2vb+uUVH4n7X7R0ydX6Eh96f84RLjtuhd091iT5PknRwX4oWfzTXJccFAAAA4DwUpQAAJ+SjZ99S6dHb/Xu1Ub/Lejr1eM2CG+n6Jx9x3P/h7Q9UUlTk1GMCAAAAcD6KUgCAExK/cas+/S7Ocf+9jx6Wh4fzPk6+XPQfjb0kUG0aHNHujZv0+7c/OO1YAAAAAFzHrYpSL774ovbv36+MjAy9/PLLstlsVfa97LLLFBsbq7y8PMXGxmrAgAEuTAoAddujdzyvA3llv4PbtwzUM1Puccpxrr3pEvU7t42a+JZqYOtsff/qNBljnHIsAAAAAK7lNkWpsWPH6sYbb9TVV1+tYcOG6aabbtLYsWMr7RsVFaWFCxfq448/1mmnnaZZs2bpq6++UkREhItTA0DdlJV+UP+e8r3s9aFHHxygzl1r9nds8xaN9cH7Yxz3ZyzcrB0bNtfoMQAAAABYy7hDS0xMNCNHjnTcv+mmm8yuXbsq7du3b1/z+uuvl9uWkZFhrrvuumofLzAw0BhjTGBgoOXnTqPRaLWx2Ww2E7Nlrik135hS8435c+ds4+npUWP7X7XhPce+tx/42vg1bGD5OdNoNBqNRqPRaLTjt+rWVNxipFRYWJjatGmj5cuXO7atXLlSbdu2VWhoaIX+y5Yt08MPPyxJ8vLy0u233y5fX1/9+uuvLssMAHWdMUa3Xfu0DhWWTePrGtlE786aUCP7fmby3Tqve7gkKa/YpptveFkFObk1sm8AAAAAtYPbFKUkKTk52bEtLS1NktSqVasqnxcVFaX8/Hx9+OGHevbZZ5WYmOjcoABQzyRuidfEl39QiSm7f/uN5+nsPmee0j6vvaG/Jo670nF/8odrte7/Vp/SPgEAAADUPl5WB7Dz8/NTy5YtK32sYcOGkqTCwkLHNvttX1/fKvd54MAB9ezZU+edd55ee+017dixQwsWLKjB1ACAdye9pT59z9T1F4Tph6RAXTZhorbferdyMw+d8L76XdpDc2Y/JI+j17H4YeNhPT/mPzUbGAAAAECtUGuKUr169dLSpUsrfezRRx+VVFaA+mcxKi8vr8p9ZmVlKTY2VrGxserataseeOABilIA4ASjr3xYKV99KI+QYDVv20Z3Tn9dH9zz8AkVpoKaNdb8r56Ur1dZRerP1BJd32+MSotLnJQaAAAAgJVqzfS9ZcuWyWazVdo+/fRTSSq3fpT9dkpKSoV9de3aVdHR0eW2bdmyRcHBwU48AwCovwpycvXCzQ/qcNoBSVLrrp31wKzpGnXvkGo9Pziite6dOV1LDzSRMdKOg9LAvmOVcxKjrQAAAAC4h1pTlDqWlJQUJSYmlis0RUdHKzExUampqRX6Dx48WB988EG5beecc462bt3q9KwAUF8dSk3T9Dsf0KG0/ZKky3s014fvjNZvf76v8/p0qfQ5zUMaK/rG6zT2y4/VIjJCu7J99OVmmy6NHqukuJ2ujA8AAADAxWwquwxfrTd+/Hg98MADuummmyRJn376qV599VW9/vrrkqTg4GDl5+crNzdXLVu21ObNmzV9+nTNmDFDl112mV577TWdd9552rBhQ7WOFxgYqKysLAUFBSk7O9tp5wUAdU2TsFA9PPN1Tbi0obz+9k8fe1KytWZdvPanZapFSBP1PreDAgIbas6uYJV9HEkp8Qn6cMw4ZSZX/AcHAAAAAO6hujUVtylKeXh4aMqUKRo1apSKi4v14Ycf6vHHH3c8vmvXLn388cd65plnJJWtUTV16lSdeeaZ2r17tyZMmKBvvvmm2sejKAUAJ8/H309Pv/dv3Tf8DDX2KT1m368TA7Uz21erPp+vRa+/o8JjrBUIAAAAoParc0UpV6MoBQCnLuqsMzRp6kPqd064wgOKZbP99VipkVLzvPTF/+3UG5PeVeLGP60LCgAAAKDGUJQ6RRSlAKDmBDUPVo/L+ur0s9qrWXAjHczI0u9rt2jj0l+Utf+A1fEAAAAA1CCKUqeIohQAAAAAAMCJq25NxS2uvgcAAAAAAIC6haIUAAAAAAAAXI6iFAAAAAAAAFyOohQAAAAAAABcjqIUAAAAAAAAXI6iFAAAAAAAAFyOohQAAAAAAABcjqIUAAAAAAAAXM7L6gC1XWBgoNURAAAAAAAA3EZ1aykUpapg/x+4b98+i5MAAAAAAAC4n8DAQGVnZ1f5uE2ScV0c9xIeHn7M/3kAAAAAAACoKDAwUMnJycfsQ1EKAAAAAAAALsdC5wAAAAAAAHA5ilIAAAAAAABwOYpSAAAAAAAAcDmKUgAAAAAAAHA5ilIAAAAAAABwOYpSAAAAAAAAcDmKUgAAAAAAAHA5ilJ1gK+vr2bMmKHMzEwlJydr7NixVfbt3r271qxZo9zcXP366686++yzXZgUNS08PFzz5s1TRkaGkpKS9Oqrr8rX17fSvl999ZWMMeXaoEGDXJwYNWXo0KEVXs958+ZV2vfiiy/Wpk2blJubqyVLligyMtLFaVETRo4cWeE1N8aopKSk0v6xsbEV+p522mkuTo1T5ePjo02bNqlv376ObW3bttVPP/2knJwcbd68WZdeeukx9zFixAjt2LFDubm5WrBggZo1a+bs2KgBlb32vXr10qpVq5Sdna1t27bpjjvuOOY+MjMzK/weaNCggbOj4xRV9tpPnTq1wmt5//33V7mPBx98UElJScrKytKMGTPk7+/viug4Rf987WfOnFnpZ/+SJUsqfX7jxo0r9D1w4IArTwEn4Fjf5erbZ72huXd74403TGxsrDnrrLPM0KFDzeHDh82wYcMq9AsICDDJyclmypQppnPnzmbq1KkmJSXFBAQEWH4OtJNrq1evNosWLTJdu3Y10dHRJi4uzkyePLnSvnFxcebGG280ISEhjubj42P5OdBOrj3xxBPm66+/Lvd6NmrUqEK/1q1bm+zsbDN27FjTtWtX8/nnn5uNGzdanp924s3Pz6/c692qVSsTFxdnXnvttQp9PTw8TF5enrngggvKPcfT09Py86BVv/n6+pr58+cbY4zp27evY3tsbKyZM2eO6dy5s5kwYYLJyckxrVu3rnQfPXv2NLm5ueaWW24xZ5xxhomJiTHffPON5edGO/HXPiQkxBw8eNC88MILpn379mb48OEmLy/PDBw4sNJ9hIeHG2OMiYyMLPd7wOpzo534ay/J/Pjjj2b8+PHlXkt/f/9K93HNNdeYzMxMM2jQINOjRw/z559/mjfffNPyc6Od+GsfFBRU7jXv1auXyc/PN0OGDKl0H+eff745cOBAuec0b97c8nOjVd6O9V2unn3WWx6AdgotICDA5OXllfvQmjhxoomJianQd9SoUSYhIaHctri4ODNy5EjLz4N24q1Tp07GGGNatGjh2DZixAiTlJRUoa+Pj48pKioyHTp0sDw3rWbanDlzzAsvvHDcfs8880y53wf+/v7m8OHD5X5n0NyzTZgwwcTHx1daXI6KijLFxcXG19fX8py0k2tdunQxGzZsMLGxseW+oPTr189kZ2eX+weln376yUyaNKnS/cyaNcvMnDnTcb9Vq1ampKTEtG3b1vJzpJ3Ya3/33XebLVu2lOs7ffp088knn1S6n4svvtjs27fP8vOhnfprL8ns3bvXXHrppdXaz7Jly8r9TujTp4/Jzc2tsohFs74d67X/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l6MoBQAAAAAAAJejKAUAAAAAAACXoygFAAAAAAAAl6MoBQAA4AQzZ86UMabKNnLkSBljFBER4fQsMTExMsYoPz//mP3effddRz5X5AIAAPWbTZKxOgQAAEBdExQUJH9/f0nS8OHDNW7cOPXs2dPxeGZmppo0aaIDBw6otLTUqVliYmL022+/acqUKdq/f3+V/QIDA9WpUyetW7dObdu2VWJiolNzAQCA+s3L6gAAAAB1UVZWlrKysiRJhw8fVklJidLS0sr1+ed9Z8rJyTlmQUqSsrOzdeDAARclAgAA9R3T9wAAACwQERFRbpqcMUbXXnuttmzZotzcXH322Wdq27atlixZotzcXC1fvlzh4eGO5w8dOlSbN29Wbm6u1q5dqwsvvLDax+7Xr582bNig/Px8JSQk6K677qrx8wMAADgeilIAAAC1xLPPPqvbbrtNgwYN0rBhw7R69Wq9++67Ov/88xUWFqbHHntMknTmmWdq1qxZev7553XmmWfqk08+0ffff6+oqKjjHsPDw0Pz5s3TvHnz1LlzZz311FN655131KVLF2efHgAAQDlM3wMAAKglXn/9df3666+SpA0bNmj79u3673//K0maP3++unfvLkkaN26cPvjgA82dO1eS9Oabb6pv37669957NW7cuGMeo1GjRmrWrJnS0tKUmJioxMREJScnKyUlxXknBgAAUAlGSgEAANQSO3fudNzOz8/X7t27y9339fWVJHXp0kVjxoxRdna2ow0ePFgdO3Y87jEyMzP1zjvvaMaMGdq9e7fefPNNHT58WIcOHarp0wEAADgmilIAAAC1RHFxcbn7VV2Vz8vLSy+//LK6d+/uaF27dtU999xTrePcf//9Ou200/T++++rV69eWrt2rQYMGHDK+QEAAE4ERSkAAAA3s337dkVGRiohIcHR7rrrLl1xxRXHfW5ISIjeeust7dixQ//5z3907rnnasmSJbrqqqtckBwAAOAvrCkFAADgZl5//XWtWLFC69at06JFizR48GCNHTtW/fv3P+5zDx48qGuuuUY2m02vvvqqWrZsqe7du2vBggUuSA4AAPAXRkoBAAC4mbVr1+qWW27Rfffdpy1btuiuu+7SDTfcoBUrVhz3uUVFRbrqqqvUvXt3/fHHH/ryyy/14YcfasaMGS5IDgAA8BebJGN1CAAAADhPTEyMli5dqmeeeea4fSMiIrR79261bdtWiYmJLkgHAADqK0ZKAQAA1AMNGzZUixYtjtknMDBQzZs3d1EiAABQ31GUAgAAqAfGjRt33JFPkydP1rp161yUCAAA1HdM3wMAAAAAAIDLMVIKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAOCoTz75RMYYjR071mnHGDlypIwxioiIkCTNnDlTu3btOuX9RkREyBijkSNHVtnH09NTM2fOVFZWlg4fPqyLLrrolI8LAABwsihKAQAASAoKCtLVV1+tP/74Q3fddZfLjvvcc8/p6quvdsmxBgwYoNtuu02vv/66rrzySq1bt84lxwUAAKgMRSkAAABJN9xwgyTpwQcfVKdOndS/f3+XHHfnzp2KjY11ybGaNWsmqWx01ooVK5Sbm+uS4wIAAFSGohQAAICk22+/XUuWLNHSpUsVHx+vu+++u9zjMTExmjlzph5//HGlpqbq0KFDWrhwodq0aePoM2nSJO3atUuDBg3S1q1blZubq19++UV9+/at8riVTd+744479Oeff6qgoECJiYmaNGmSPDzK/9l29dVXKzY2Vnl5eVq/fr26d+9+zPObOXOmZs2aJUnatWuXYmJiJEm+vr568skntXXrVuXn5ysuLk6PPfaYbDZbuXOfM2eO5s2bp5ycHP3444+SpMDAQL3xxhtKSkpSTk6Ofv31Vw0cOPCEzwUAANRfhkaj0Wg0Gq0+t65duxpjjBk2bJiRZCZOnGgKCwtNixYtHH1iYmLMwYMHTVxcnLnuuuvMiBEjzO7du83OnTuNv7+/kWQmTZpkcnJyTEZGhnnggQfMwIEDzZIlS0xBQYHp1q2bkWRGjhxpjDEmIiLCSDIzZ840u3btchxnwoQJpqSkxEydOtVceuml5tFHHzV5eXlmxowZjj5XXnmlKSkpMbNnzzaXXXaZGT9+vMnOzjbGGDNy5MhKz7Fdu3bm2WefNcYYM3ToUNOlSxcjyfz4448mKyvLPPLII+aSSy4xL7zwgikqKjLvvfdeuXM/cuSI+eijj0z//v3NJZdcYjw8PMwvv/xiMjIyzP33328uvvhiM3v2bHPkyBETHR1d7XOh0Wg0Go1Wr5vlAWg0Go1Go9Esba+88oo5cOCA8fb2NpJMq1atTHFxsXn88ccdfWJiYkxhYaGJjIx0bOvevbsxxpi7777bSGVFKWOMufnmmx19/Pz8THJyspk7d66Rjl2UCgoKMrm5ueadd94pl+/22283xhjTtWtXI8n8+uuv5pdffinX57HHHjtmUaqyYw8YMMAYY8zw4cPL9Zs4cWK548XExJicnBzj4+Pj6DNo0CBjjDFXXXWVY5vNZjOrVq0yTz/9dLXPhUaj0Wg0Wv1tjJ0GAAD1mpeXl26++WZ99dVXCggIUKNGjZSdna2VK1fqzjvvLDeNbeXKleWm2sXGxiohIaHc9LyioiLNnTvXcb+goEDffffdMafw2Z133nkKCAjQ//73P3l6ejraN998I0m69NJL5efnp3POOcexze7LL7884XO/6KKLVFRUpHnz5pXb/sknn0hSucxbt27VkSNHHPejo6N15MiRcjmMMerTp4+effbZap0LAACo37ysDgAAAGClK6+8UiEhIRo9erRGjx5d4fHLL79cP/zwgyRp3759FR7fv3+/mjZt6rifmpqqkpKSY/apin0h8u+//77Sx8PDw9WkSRN5eHgoPT293GMpKSnH3f8/NW3aVOnp6SotLS23PTU1VZLUuHFjx7acnJwKWTMyMmSMqXTf1TkXAABQv1GUAgAA9dqoUaOUkJCgO+64o9x2m82mhQsX6p577nEUpYKDgys8PyQkRDt27HDctxdj/tln//79x81y6NAhSdKNN96ouLi4Co+npaXp4MGDKikpUUhISLnHKjvu8Rw8eFDBwcHy8PAoV5gKCwuTpAqFr39mreyY3bt3l81mq9a5AACA+o3pewAAoN4KCQnRFVdcoc8//1zLli0r15YuXap58+Zp0KBBjlE90dHR5UY8nX322WrXrp2WLFni2BYQEKDLLrvMcd/Pz08DBw4s16cqa9asUWFhoVq2bKn169c7WnFxsV588UVFRkaqsLBQq1ev1rBhw8o9d/DgwSd8/suWLZO3t7euu+66cttvvvlmSWXTFauyYsUK+fj4aMCAAeW2z5w5U0888US1zgUAANRvjJQCAAD11q233ipvb+9ya0D93ezZs3XnnXfqzjvvlCQ1aNBA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07hx4/Txxx/r+eef1yWXXKJmzZrp3nvvVV5enrZv366LLrpILVq0qHR/6enpKikp0eHDh5Wenq7BgwerZcuW6tWrlw4dOiRJuv/++/XNN984CmlTp07Vzp07JZUtkt68eXONGjVKOTk52rJlix544AGVlJQcN+s/nX322fL09FRGRoZj28aNGxUdHV2t1+NkUZQC4BThHaMkSclxZSOlJCmcK/ABAAAAtZsxVieolhEjRqigoECLFy+WVLZI98GDBzVy5MhyRam/j1L67bff1KJFC8eUumM9VpkuXbqoWbNmysrKcmzz8PBQQECAmjZtqq5duyo+Pl55eXmOx9etW6dBgwZV65y6dOmiuLg4R0FKklavXi1vb295eZWVb3bv3u14rFOnToqLi1NOTo5j28cffyyprJh1rKwHDx4sd+yzzz5bc+fO1aRJkxzbnLmWlB1FKQBO4Zi+t32H9u/cLUlqFNJcDRo3Uu6hwxYmAwAAAFCVt0be4xbT92644QYFBASUK7p4eXnpuuuuK7euUlFRkeO2fc0o+6Llx3qsMl5eXtq2bZuGDBlS4bHDh8u+49hstnLbjxw5Uu1zqmyhc3su+3//3ufv+U8m69+dffbZevzxx5WQkFDtvDWBhc4B1LiGTZsoqHlw2ZUqEnaqMC9P6XuTJP1VrAIAAABQOx3JL3BZOxkdOnTQ2WefrQceeEDdu3d3tOHDh6tRo0a6+uqrHX27d+/uuN2jRw/t27fPMUWtssf+OYLo77Zv3642bdrowIEDSkhIUEJCgiIjI/XMM8/IGKM///xTHTt2VFBQkOM5Z511VrXPa/v27erYsaOaNGni2HbeeeepqKhIJSUlFfrHx8erffv28vf3d2ybMmWKpk2bdtysfxcZGakmTZpow4YN1c5aUyhKAahx9sJTxp4kxwdN8vYd5R4DAAAAgJNxww03KCMjQ++//742b97saF9++aU2b96skSNHOvpOmzZN55xzji6++GI9++yzevvtt6v1WGV+/PFHJSYm6pNPPtHpp5+u6Ohovf/++8rLy1Npaan+7//+T3v27NGHH36ozp07a+TIkRo+fHi1z+unn37Szp07NWfOHJ1++um66KKL9Oabb+qzzz4rN6XPbvHixUpNTdV7772nTp06afDgwbrnnnu0ePHi42b9u3POOUfFxcUuX+RcoigFwAnC7VP3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tm3k5eOj4iNHLE4EAAAAAKgupxelnnrqKUnSe++9V+ExY4y8vKoXoVu3bvL29tbq1asd21auXKmJEyfKZrPJGFOuf9OmTTV58mRddtll2rx58ymcAVB3efv5KrhNK0lScly8xWlOXNb+A8rNPKQGTRorJKqt9m2NszoSAAAAAKCanL6mlKenZ5WtugUpSQoLC1N6erqKiooc29LS0uTv769mzZpV6P/aa69p1qxZ2rJlS42cB1AXhUa1k4enp7IzDionI9PqOCfFPsIrnCl8AAAAAOBWnD5SSpL8/Px08803q0uXLvL09NS2bdv0xRdfKDOz+l+CAwICVFhYWG6b/b6vr2+57RdffLGio6N1+umnn3p4oA4LOzp1L8UNp+7ZJcftUIdePRznAgAAAABwD04fKXXaaacpPj5eTz75pCIiIhQREaEnnnhC27ZtU5cuXaq9n4KCggrFJ/v9vLw8xzY/Pz+99957uu+++yqsXwWgvPBOR9eTcsNFzu3sBbVwilIAAAAA4FacXpSaNm2afvrpJ0VFRenaa6/V1VdfrcjISH333XeaOnVqtfezb98+BQcHy9PT07EtNDRUeXl5OnTokGPbueeeq6ioKM2fP1/Z2dnKzs6WJH3//fd69913a+q0gDohzI0XObejKAUAAAAA7snp0/fOO+883XfffSopKXFsKykp0UsvvaTffvut2vuJjY1VUVGRevfurVWrVkmSoqOjtW7dunKLnP/6669q3778l9MdO3Zo9OjR+umnn07xbIC65a+RUu63yLld6o5dKi0pUYMmjRXUormy9h+wOhIAAAAAoBqcXpRKSUlR+/btFRdX/qpY7du3V1ZWVrX3k5+fr1mzZmn69OkaNWqUWrZsqXHjxmnUqFGSpJCQEB0+fFgFBQVKSEio8Px9+/bpwAG+rAJ2jUNaKCAoSCVFxdq/K9HqOCet+MgR7d+9R6FRkQrvGEVRCgAAAADchNOn702fPl0zZszQ7bffrtNPP12nn366Ro8erQ8++EAzZsw4oX2NHTtW69evV0xMjN5++21NmjRJCxculCSlpqZq+PDhzjgFoE6yT91L27VbJX+7qqU7Sjk60ovFzgEAAADAfTh9pNQrr7yiBg0a6OWXX1bTpk0lSWlpaXrttdf06quvntC+8vPzddttt+m2226r8JjNZqvyecd6DKivwjt1kOTe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", "text/plain": [ "
" - ], - "image/png": "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iy/Wpk2blJubqyVLligyMtLFaVETRo4cWeE1N8aopKSk0v6xsbEV+p522mkuTo1T5ePjo02bNqlv376ObW3bttVPP/2knJwcbd68WZdeeukx9zFixAjt2LFDubm5WrBggZo1a+bs2KgBlb32vXr10qpVq5Sdna1t27bpjjvuOOY+MjMzK/weaNCggbOj4xRV9tpPnTq1wmt5//33V7mPBx98UElJScrKytKMGTPk7+/viug4Rf987WfOnFnpZ/+SJUsqfX7jxo0r9D1w4IArTwEn4Fjf5erbZ72huXd74403TGxsrDnrrLPM0KFDzeHDh82wYcMq9AsICDDJyclmypQppnPnzmbq1KkmJSXFBAQEWH4OtJNrq1evNosWLTJdu3Y10dHRJi4uzkyePLnSvnFxcebGG280ISEhjubj42P5OdBOrj3xxBPm66+/Lvd6NmrUqEK/1q1bm+zsbDN27FjTtWtX8/nnn5uNGzdanp924s3Pz6/c692qVSsTFxdnXnvttQp9PTw8TF5enrngggvKPcfT09Py86BVv/n6+pr58+cbY4zp27evY3tsbKyZM2eO6dy5s5kwYYLJyckxrVu3rnQfPXv2NLm5ueaWW24xZ5xxhomJiTHffPON5edGO/HXPiQkxBw8eNC88MILpn379mb48OEmLy/PDBw4sNJ9hIeHG2OMiYyMLPd7wOpzo534ay/J/Pjjj2b8+PHlXkt/f/9K93HNNdeYzMxMM2jQINOjRw/z559/mjfffNPyc6Od+GsfFBRU7jXv1auXyc/PN0OGDKl0H+eff745cOBAuec0b97c8nOjVd6O9V2unn3WWx6AdgotICDA5OXllfvQmjhxoomJianQd9SoUSYhIaHctri4ODNy5EjLz4N24q1Tp07GGGNatGjh2DZixAiTlJRUoa+Pj48pKioyHTp0sDw3rWbanDlzzAsvvHDcfs8880y53wf+/v7m8OHD5X5n0NyzTZgwwcTHx1daXI6KijLFxcXG19fX8py0k2tdunQxGzZsMLGxseW+oPTr189kZ2eX+weln376yUyaNKnS/cyaNcvMnDnTcb9Vq1ampKTEtG3b1vJzpJ3Ya3/33XebLVu2lOs7ffp088knn1S6n4svvtjs27fP8vOhnfprL8ns3bvXXHrppdXaz7Jly8r9TujTp4/Jzc2tsohFs74d67X/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l6MoBQAAAAAAAJejKAUAAAAAAACXoygFAAAAAAAAl6MoBQAA4AQzZ86UMabKNnLkSBljFBER4fQsMTExMsYoPz//mP3effddRz5X5AIAAPWbTZKxOgQAAEBdExQUJH9/f0nS8OHDNW7cOPXs2dPxeGZmppo0aaIDBw6otLTUqVliYmL022+/acqUKdq/f3+V/QIDA9WpUyetW7dObdu2VWJiolNzAQCA+s3L6gAAAAB1UVZWlrKysiRJhw8fVklJidLS0sr1+ed9Z8rJyTlmQUqSsrOzdeDAARclAgAA9R3T9wAAACwQERFRbpqcMUbXXnuttmzZotzcXH322Wdq27atlixZotzcXC1fvlzh4eGO5w8dOlSbN29Wbm6u1q5dqwsvvLDax+7Xr582bNig/Px8JSQk6K677qrx8wMAADgeilIAAAC1xLPPPqvbbrtNgwYN0rBhw7R69Wq9++67Ov/88xUWFqbHHntMknTmmWdq1qxZev7553XmmWfqk08+0ffff6+oqKjjHsPDw0Pz5s3TvHnz1LlzZz311FN655131KVLF2efHgAAQDlM3wMAAKglXn/9df3666+SpA0bNmj79u3673//K0maP3++unfvLkkaN26cPvjgA82dO1eS9Oabb6pv37669957NW7cuGMeo1GjRmrWrJnS0tKUmJioxMREJScnKyUlxXknBgAAUAlGSgEAANQSO3fudNzOz8/X7t27y9339fWVJHXp0kVjxoxRdna2ow0ePFgdO3Y87jEyMzP1zjvvaMaMGdq9e7fefPNNHT58WIcOHarp0wEAADgmilIAAAC1RHFxcbn7VV2Vz8vLSy+//LK6d+/uaF27dtU999xTrePcf//9Ou200/T++++rV69eWrt2rQYMGHDK+QEAAE4ERSkAAAA3s337dkVGRiohIcHR7rrrLl1xxRXHfW5ISIjeeust7dixQ//5z3907rnnasmSJbrqqqtckBwAAOAvrCkFAADgZl5//XWtWLFC69at06JFizR48GCNHTtW/fv3P+5zDx48qGuuuUY2m02vvvqqWrZsqe7du2vBggUuSA4AAPAXRkoBAAC4mbVr1+qWW27Rfffdpy1btuiuu+7SDTfcoBUrVhz3uUVFRbrqqqvUvXt3/fHHH/ryyy/14YcfasaMGS5IDgAA8BebJGN1CAAAADhPTEyMli5dqmeeeea4fSMiIrR79261bdtWiYmJLkgHAADqK0ZKAQAA1AMNGzZUixYtjtknMDBQzZs3d1EiAABQ31GUAgAAqAfGjRt33JFPkydP1rp161yUCAAA1HdM3wMAAAAAAIDLMVIKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAAAAAAAuR1EKAAAAAAAALkdRCgAAAAAAAC5HUQoAAOCoTz75RMYYjR071mnHGDlypIwxioiIkCTNnDlTu3btOuX9RkREyBijkSNHVtnH09NTM2fOVFZWlg4fPqyLLrrolI8LAABwsihKAQAASAoKCtLVV1+tP/74Q3fddZfLjvvcc8/p6quvdsmxBgwYoNtuu02vv/66rrzySq1bt84lxwUAAKgMRSkAAABJN9xwgyTpwQcfVKdOndS/f3+XHHfnzp2KjY11ybGaNWsmqWx01ooVK5Sbm+uS4wIAAFSGohQAAICk22+/XUuWLNHSpUsVHx+vu+++u9zjMTExmjlzph5//HGlpqbq0KFDWrhwodq0aePoM2nSJO3atUuDBg3S1q1blZubq19++UV9+/at8riVTd+744479Oeff6qgoECJiYmaNGmSPDzK/9l29dVXKzY2Vnl5eVq/fr26d+9+zPObOXOmZs2aJUnatWuXYmJiJEm+vr568skntXXrVuXn5ysuLk6PPfaYbDZbuXOfM2eO5s2bp5ycHP3444+SpMDAQL3xxhtKSkpSTk6Ofv31Vw0cOPCEzwUAANRfhkaj0Wg0Gq0+t65duxpjjBk2bJiRZCZOnGgKCwtNixYtHH1iYmLMwYMHTVxcnLnuuuvMiBEjzO7du83OnTuNv7+/kWQmTZpkcnJyTEZGhnnggQfMwIEDzZIlS0xBQYHp1q2bkWRGjhxpjDEmIiLCSDIzZ840u3btchxnwoQJpqSkxEydOtVceuml5tFHHzV5eXlmxowZjj5XXnmlKSkpMbNnzzaXXXaZGT9+vMnOzjbGGDNy5MhKz7Fdu3bm2WefNcYYM3ToUNOlSxcjyfz4448mKyvLPPLII+aSSy4xL7zwgikqKjLvvfdeuXM/cuSI+eijj0z//v3NJZdcYjw8PMwvv/xiMjIyzP33328uvvhiM3v2bHPkyBETHR1d7XOh0Wg0Go1Wr5vlAWg0Go1Go9Esba+88oo5cOCA8fb2NpJMq1atTHFxsXn88ccdfWJiYkxhYaGJjIx0bOvevbsxxpi7777bSGVFKWOMufnmmx19/Pz8THJyspk7d66Rjl2UCgoKMrm5ueadd94pl+/22283xhjTtWtXI8n8+uuv5pdffinX57HHHjtmUaqyYw8YMMAYY8zw4cPL9Zs4cWK548XExJicnBzj4+Pj6DNo0CBjjDFXXXWVY5vNZjOrVq0yTz/9dLXPhUaj0Wg0Wv1tjJ0GAAD1mpeXl26++WZ99dVXCggIUKNGjZSdna2VK1fqzjvvLDeNbeXKleWm2sXGxiohIaHc9LyioiLNnTvXcb+goEDffffdMafw2Z133nkKCAjQ//73P3l6ejraN998I0m69NJL5efnp3POOcexze7LL7884XO/6KKLVFRUpHnz5pXb/sknn0hSucxbt27VkSNHHPejo6N15MiRcjmMMerTp4+effbZap0LAACo37ysDgAAAGClK6+8UiEhIRo9erRGjx5d4fHLL79cP/zwgyRp3759FR7fv3+/mjZt6rifmpqqkpKSY/apin0h8u+//77Sx8PDw9WkSRN5eHgoPT293GMpKSnH3f8/NW3aVOnp6SotLS23PTU1VZLUuHFjx7acnJwKWTMyMmSMqXTf1TkXAABQv1GUAgAA9dqoUaOUkJCgO+64o9x2m82mhQsX6p577nEUpYKDgys8PyQkRDt27HDctxdj/tln//79x81y6NAhSdKNN96ouLi4Co+npaXp4MGDKikpUUhISLnHKjvu8Rw8eFDBwcHy8PAoV5gKCwuTpAqFr39mreyY3bt3l81mq9a5AACA+o3pewAAoN4KCQnRFVdcoc8//1zLli0r15YuXap58+Zp0KBBjlE90dHR5UY8nX322WrXrp2WLFni2BYQEKDLLrvMcd/Pz08DBw4s16cqa9asUWFhoVq2bKn169c7WnFxsV588UVFRkaqsLBQq1ev1rBhw8o9d/DgwSd8/suWLZO3t7euu+66cttvvvlmSWXTFauyYsUK+fj4aMCAAeW2z5w5U0888US1zgUAANRvjJQCAAD11q233ipvb+9ya0D93ezZs3XnnXfqzjvvlCQ1aNBA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07hx4/Txxx/r+eef1yWXXKJmzZrp3nvvVV5enrZv366LLrpILVq0qHR/6enpKikp0eHDh5Wenq7BgwerZcuW6tWrlw4dOiRJuv/++/XNN984CmlTp07Vzp07JZUtkt68eXONGjVKOTk52rJlix544AGVlJQcN+s/nX322fL09FRGRoZj28aNGxUdHV2t1+NkUZQC4BThHaMkSclxZSOlJCmcK/ABAAAAtZsxVieolhEjRqigoECLFy+WVLZI98GDBzVy5MhyRam/j1L67bff1KJFC8eUumM9VpkuXbqoWbNmysrKcmzz8PBQQECAmjZtqq5duyo+Pl55eXmOx9etW6dBgwZV65y6dOmiuLg4R0FKklavXi1vb295eZWVb3bv3u14rFOnToqLi1NOTo5j28cffyyprJh1rKwHDx4sd+yzzz5bc+fO1aRJkxzbnLmWlB1FKQBO4Zi+t32H9u/cLUlqFNJcDRo3Uu6hwxYmAwAAAFCVt0be4xbT92644QYFBASUK7p4eXnpuuuuK7euUlFRkeO2fc0o+6Llx3qsMl5eXtq2bZuGDBlS4bHDh8u+49hstnLbjxw5Uu1zqmyhc3su+3//3ufv+U8m69+dffbZevzxx5WQkFDtvDWBhc4B1LiGTZsoqHlw2ZUqEnaqMC9P6XuTJP1VrAIAAABQOx3JL3BZOxkdOnTQ2WefrQceeEDdu3d3tOHDh6tRo0a6+uqrHX27d+/uuN2jRw/t27fPMUWtssf+OYLo77Zv3642bdrowIEDSkhIUEJCgiIjI/XMM8/IGKM///xTHTt2VFBQkOM5Z511VrXPa/v27erYsaOaNGni2HbeeeepqKhIJSUlFfrHx8erffv28vf3d2ybMmWKpk2bdtysfxcZGakmTZpow4YN1c5aUyhKAahx9sJTxp4kxwdN8vYd5R4DAAAAgJNxww03KCMjQ++//742b97saF9++aU2b96skSNHOvpOmzZN55xzji6++GI9++yzevvtt6v1WGV+/PFHJSYm6pNPPtHpp5+u6Ohovf/++8rLy1Npaan+7//+T3v27NGHH36ozp07a+TIkRo+fHi1z+unn37Szp07NWfOHJ1++um66KKL9Oabb+qzzz4rN6XPbvHixUpNTdV7772nTp06afDgwbrnnnu0ePHi42b9u3POOUfFxcUuX+RcoigFwAnC7VP3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tm3k5eOj4iNHLE4EAAAAAKgupxelnnrqKUnSe++9V+ExY4y8vKoXoVu3bvL29tbq1asd21auXKmJEyfKZrPJGFOuf9OmTTV58mRddtll2rx58ymcAVB3efv5KrhNK0lScly8xWlOXNb+A8rNPKQGTRorJKqt9m2NszoSAAAAAKCanL6mlKenZ5WtugUpSQoLC1N6erqKiooc29LS0uTv769mzZpV6P/aa69p1qxZ2rJlS42cB1AXhUa1k4enp7IzDionI9PqOCfFPsIrnCl8AAAAAOBWnD5SSpL8/Px08803q0uXLvL09NS2bdv0xRdfKDOz+l+CAwICVFhYWG6b/b6vr2+57RdffLGio6N1+umnn3p4oA4LOzp1L8UNp+7ZJcftUIdePRznAgAAAABwD04fKXXaaacpPj5eTz75pCIiIhQREaEnnnhC27ZtU5cuXaq9n4KCggrFJ/v9vLw8xzY/Pz+99957uu+++yqsXwWgvPBOR9eTcsNFzu3sBbVwilIAAAAA4FacXpSaNm2afvrpJ0VFRenaa6/V1VdfrcjISH333XeaOnVqtfezb98+BQcHy9PT07EtNDRUeXl5OnTokGPbueeeq6ioKM2fP1/Z2dnKzs6WJH3//fd69913a+q0gDohzI0XObejKAUAAAAA7snp0/fOO+883XfffSopKXFsKykp0UsvvaTffvut2vuJjY1VUVGRevfurVWrVkmSoqOjtW7dunKLnP/6669q3778l9MdO3Zo9OjR+umnn07xbIC65a+RUu63yLld6o5dKi0pUYMmjRXUormy9h+wOhIAAAAAoBqcXpRKSUlR+/btFRdX/qpY7du3V1ZWVrX3k5+fr1mzZmn69OkaNWqUWrZsqXHjxmnUqFGSpJCQEB0+fFgFBQVKSEio8Px9+/bpwAG+rAJ2jUNaKCAoSCVFxdq/K9HqOCet+MgR7d+9R6FRkQrvGEVRCgAAAADchNOn702fPl0zZszQ7bffrtNPP12nn366Ro8erQ8++EAzZsw4oX2NHTtW69evV0xMjN5++21NmjRJCxculCSlpqZq+PDhzjgFoE6yT91L27VbJX+7qqU7Sjk60ovFzgEAAADAfTh9pNQrr7yiBg0a6OWXX1bTpk0lSWlpaXrttdf06quvntC+8vPzddttt+m2226r8JjNZqvyecd6DKivwjt1kOTe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" + ] }, - "metadata": {}, - "output_type": "display_data", "jetTransient": { "display_id": null - } + }, + "metadata": {}, + "output_type": "display_data" } ], - "execution_count": 7 + "source": [ + "# Recursive plot using getSamples\n", + "msd.importPythonModel(name=model_name)\n", + "msd.neuralizeModel(sample_time=0.01)\n", + "samples = msd.getSamples(dataset=\"simulations\", window=2000 - 10, index=0)\n", + "result = msd(samples, sampled=True, prediction_samples=2000 - 10)\n", + "\n", + "# region Plotting\n", + "t = np.arange(len(result[\"x_n\"])) * 0.01\n", + "plt.figure(figsize=(12, 7))\n", + "plt.subplot(2, 1, 1)\n", + "plt.plot(t, result[\"x_n\"], label=r\"Estimated $x$\", linewidth=2)\n", + "plt.plot(\n", + " t,\n", + " np.array(samples[\"x_t\"]).squeeze(-1).squeeze(-1),\n", + " label=r\"True $x$\",\n", + " linestyle=\"--\",\n", + " linewidth=2,\n", + ")\n", + "plt.title(\"Position estimation\")\n", + "plt.xlabel(\"Time [s]\")\n", + "plt.ylabel(r\"$x$ [m]\")\n", + "plt.grid()\n", + "plt.legend()\n", + "plt.subplot(2, 1, 2)\n", + "plt.plot(t, np.array(samples[\"F\"])[:, 0, 0], label=r\"Applied force $F$\")\n", + "plt.title(\"Applied force\")\n", + "plt.xlabel(\"Time [s]\")\n", + "plt.ylabel(\"Force [N]\")\n", + "plt.grid()\n", + "plt.legend()\n", + "plt.tight_layout()\n", + "# endregion\n", + "\n", + "plt.show()" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "", - "id": "439c02826528ca8" + "id": "439c02826528ca8", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/case-studies/neuralODE/saved/neuralODE_msd.py b/case-studies/neuralODE/saved/neuralODE_msd.py index d87c5470..8517e971 100644 --- a/case-studies/neuralODE/saved/neuralODE_msd.py +++ b/case-studies/neuralODE/saved/neuralODE_msd.py @@ -1,16 +1,20 @@ import torch + def nnodely_basic_model_update_state(data_in, rel): data_out = data_in.clone() max_dim = min(rel.size(1), data_in.size(1)) data_out[:, -max_dim:, :] = rel[:, -max_dim:, :] return data_out + def nnodely_basic_model_timeshift(data_in): return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1) + def nnodely_layers_neuralODE_FNeuralODE5(state, *weights): from nnodely.support.odeint.adjoint import odeint_adjoint as odeint + def ode_func_torch(t, state, weight_fir): import torch import torch.nn.functional as F @@ -20,80 +24,190 @@ def ode_func_torch(t, state, weight_fir): # 2 channels = [position_window, force_window] # # weight_fir: FIR weights applied independently to each channel - + # Apply two independent FIR filters (grouped convolution): # this produces one-step predictions for each channel # (B, W, 2) -> (B, 2, W) -> conv -> (B, 2, 1) out = F.conv1d(state.transpose(1, 2), weight_fir, groups=2) - + # Restore time dimension ordering # (B, 2, 1) -> (B, 1, 2) out = out.transpose(1, 2) - + # Combine the two FIR contributions: # next_velocity = free_response + forced_response out[:, :, 0] = out[:, :, 0] + out[:, :, -1] - + # Set the force prediction to zero, for the intermediate time steps done in the integration process (if a variable step integrator, such as Dopri5, is used) out[:, :, -1] = 0.0 - + # Shift the sliding window forward by one step # drop the oldest sample and append the new prediction # resulting shape: (B, W, 2) out = torch.cat((state[:, 1:, :], out), dim=1) - + return out - - ans = odeint(lambda t, y: ode_func_torch(t, y, *weights), state, t=torch.tensor([0.0, 0.01]), rtol=1e-07, atol=1e-09, method='dopri5', adjoint_params=list(weights)) + + ans = odeint( + lambda t, y: ode_func_torch(t, y, *weights), + state, + t=torch.tensor([0.0, 0.01]), + rtol=1e-07, + atol=1e-09, + method="dopri5", + adjoint_params=list(weights), + ) return ans[-1] + class TracerModel(torch.nn.Module): def __init__(self): super().__init__() self.all_parameters = {} self.all_constants = {} - self.all_parameters["weight_fir"] = torch.nn.Parameter(torch.tensor([[[-5.0214056968688965, -4.025257587432861, -3.0458261966705322, -1.9209206104278564, -0.7650308012962341, 0.5171442031860352, 1.7535150051116943, 3.087076187133789, 4.083190441131592, 5.219380855560303]], [[0.00026014805189333856, 0.0007117472123354673, 0.0013745456235483289, 0.002709923079237342, 0.003882204182446003, 0.005919742863625288, 0.0070823198184370995, 0.008177743293344975, 0.009529106318950653, 0.008549299091100693]]]), requires_grad=True) + self.all_parameters["weight_fir"] = torch.nn.Parameter( + torch.tensor( + [ + [ + [ + -5.0214056968688965, + -4.025257587432861, + -3.0458261966705322, + -1.9209206104278564, + -0.7650308012962341, + 0.5171442031860352, + 1.7535150051116943, + 3.087076187133789, + 4.083190441131592, + 5.219380855560303, + ] + ], + [ + [ + 0.00026014805189333856, + 0.0007117472123354673, + 0.0013745456235483289, + 0.002709923079237342, + 0.003882204182446003, + 0.005919742863625288, + 0.0070823198184370995, + 0.008177743293344975, + 0.009529106318950653, + 0.008549299091100693, + ] + ], + ] + ), + requires_grad=True, + ) self.all_constants["SamplePart10"] = torch.tensor([[1.0]], requires_grad=True) self.all_constants["Select7"] = torch.tensor([1.0, 0.0], requires_grad=True) - self.all_constants["TimePart1"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True) - self.all_constants["TimePart3"] = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True) - self.all_constants["TimePart8"] = torch.tensor([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True) + self.all_constants["TimePart1"] = torch.tensor( + [ + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + ], + requires_grad=True, + ) + self.all_constants["TimePart3"] = torch.tensor( + [ + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + ], + requires_grad=True, + ) + self.all_constants["TimePart8"] = torch.tensor( + [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]], requires_grad=True + ) self.all_parameters = torch.nn.ParameterDict(self.all_parameters) self.all_constants = torch.nn.ParameterDict(self.all_constants) def update(self, closed_loop={}, connect={}, disconnect=False): pass + def forward(self, kwargs): - getitem = kwargs['x_t'] + getitem = kwargs["x_t"] relation_forward_sample_part10_w = self.all_constants.SamplePart10 - einsum = torch.functional.einsum('bij,ki->bkj', getitem, relation_forward_sample_part10_w); getitem = relation_forward_sample_part10_w = None - getitem_1 = kwargs['x'] + einsum = torch.functional.einsum( + "bij,ki->bkj", getitem, relation_forward_sample_part10_w + ) + getitem = relation_forward_sample_part10_w = None + getitem_1 = kwargs["x"] relation_forward_time_part1_w = self.all_constants.TimePart1 - einsum_1 = torch.functional.einsum('bij,ki->bkj', getitem_1, relation_forward_time_part1_w); getitem_1 = relation_forward_time_part1_w = None - getitem_2 = kwargs['F']; kwargs = None + einsum_1 = torch.functional.einsum( + "bij,ki->bkj", getitem_1, relation_forward_time_part1_w + ) + getitem_1 = relation_forward_time_part1_w = None + getitem_2 = kwargs["F"] + kwargs = None relation_forward_time_part3_w = self.all_constants.TimePart3 - einsum_2 = torch.functional.einsum('bij,ki->bkj', getitem_2, relation_forward_time_part3_w); getitem_2 = relation_forward_time_part3_w = None - cat = torch.cat((einsum_1, einsum_2), dim = 2); einsum_1 = einsum_2 = None + einsum_2 = torch.functional.einsum( + "bij,ki->bkj", getitem_2, relation_forward_time_part3_w + ) + getitem_2 = relation_forward_time_part3_w = None + cat = torch.cat((einsum_1, einsum_2), dim=2) + einsum_1 = einsum_2 = None all_parameters_weight_fir = self.all_parameters.weight_fir - fneural_ode5 = nnodely_layers_neuralODE_FNeuralODE5(cat, all_parameters_weight_fir); cat = all_parameters_weight_fir = None + fneural_ode5 = nnodely_layers_neuralODE_FNeuralODE5( + cat, all_parameters_weight_fir + ) + cat = all_parameters_weight_fir = None relation_forward_select7_w = self.all_constants.Select7 - einsum_3 = torch.functional.einsum('ijk,k->ij', fneural_ode5, relation_forward_select7_w); fneural_ode5 = relation_forward_select7_w = None - unsqueeze = einsum_3.unsqueeze(2); einsum_3 = None + einsum_3 = torch.functional.einsum( + "ijk,k->ij", fneural_ode5, relation_forward_select7_w + ) + fneural_ode5 = relation_forward_select7_w = None + unsqueeze = einsum_3.unsqueeze(2) + einsum_3 = None relation_forward_time_part8_w = self.all_constants.TimePart8 - einsum_4 = torch.functional.einsum('bij,ki->bkj', unsqueeze, relation_forward_time_part8_w); unsqueeze = relation_forward_time_part8_w = None - return ({'x_n': einsum_4}, {'SamplePart10': einsum, 'TimePart8': einsum_4}, {'x': einsum_4}, {}) - + einsum_4 = torch.functional.einsum( + "bij,ki->bkj", unsqueeze, relation_forward_time_part8_w + ) + unsqueeze = relation_forward_time_part8_w = None + return ( + {"x_n": einsum_4}, + {"SamplePart10": einsum, "TimePart8": einsum_4}, + {"x": einsum_4}, + {}, + ) + + class RecurrentModel(torch.nn.Module): def __init__(self): super().__init__() self.Cell = TracerModel() - self.inputs = ['F', 'x_t', ] + self.inputs = [ + "F", + "x_t", + ] self.states = dict() - def forward(self, kwargs, n_samples = None): - n_samples = n_samples if n_samples else min([kwargs[key].size(0) for key in self.inputs]) - self.states['x'] = kwargs['x'] - results = {'x_n':[], } + def forward(self, kwargs, n_samples=None): + n_samples = ( + n_samples + if n_samples + else min([kwargs[key].size(0) for key in self.inputs]) + ) + self.states["x"] = kwargs["x"] + results = { + "x_n": [], + } X = dict() for idx in range(n_samples): for key in self.inputs: @@ -105,7 +219,9 @@ def forward(self, kwargs, n_samples = None): results[key].append(out[key]) for key, val in closed_loop.items(): self.states[key] = nnodely_basic_model_timeshift(self.states[key]) - self.states[key] = nnodely_basic_model_update_state(self.states[key], val) + self.states[key] = nnodely_basic_model_update_state( + self.states[key], val + ) for key, val in connect.items(): self.states[key] = nnodely_basic_model_timeshift(val) return results diff --git a/case-studies/pinn/pinn_Burgers_equation.ipynb b/case-studies/pinn/pinn_Burgers_equation.ipynb index 9a320310..a350df58 100644 --- a/case-studies/pinn/pinn_Burgers_equation.ipynb +++ b/case-studies/pinn/pinn_Burgers_equation.ipynb @@ -18,6 +18,7 @@ }, { "cell_type": "code", + "execution_count": 1, "id": "19b3470f12bc42b4", "metadata": { "ExecuteTime": { @@ -25,10 +26,6 @@ "start_time": "2026-02-25T17:39:06.884462Z" } }, - "source": [ - "from nnodely import *\n", - "import numpy as np" - ], "outputs": [ { "name": "stdout", @@ -38,7 +35,10 @@ ] } ], - "execution_count": 1 + "source": [ + "from nnodely import *\n", + "import numpy as np" + ] }, { "cell_type": "markdown", @@ -50,6 +50,7 @@ }, { "cell_type": "code", + "execution_count": 2, "id": "f6f28e7b27c45c9e", "metadata": { "ExecuteTime": { @@ -57,24 +58,29 @@ "start_time": "2026-02-25T17:39:07.631468Z" } }, + "outputs": [], "source": [ - "t = Input('t')\n", - "x = Input('x')\n", + "t = Input(\"t\")\n", + "x = Input(\"x\")\n", "x_last = x.last()\n", "t_last = t.last()\n", "\n", - "xt = Concatenate(x_last,t_last)\n", + "xt = Concatenate(x_last, t_last)\n", "for hidden in range(4):\n", - " xt = Tanh(Linear(20, b = True, b_init = 'init_constant', b_init_params = {'value':0})(xt))\n", - "u = Linear(1, b = True, b_init = 'init_constant', b_init_params = {'value':0})(xt)\n", + " xt = Tanh(\n", + " Linear(20, b=True, b_init=\"init_constant\", b_init_params={\"value\": 0})(xt)\n", + " )\n", + "u = Linear(1, b=True, b_init=\"init_constant\", b_init_params={\"value\": 0})(xt)\n", "\n", - "f = Differentiate(u,t_last) + Differentiate(u,x_last) * u - (0.01 / np.pi) * Differentiate(Differentiate(u,x_last),x_last)\n", + "f = (\n", + " Differentiate(u, t_last)\n", + " + Differentiate(u, x_last) * u\n", + " - (0.01 / np.pi) * Differentiate(Differentiate(u, x_last), x_last)\n", + ")\n", "\n", - "U = Output('U',u)\n", - "F = Output('F',f)" - ], - "outputs": [], - "execution_count": 2 + "U = Output(\"U\", u)\n", + "F = Output(\"F\", f)" + ] }, { "cell_type": "markdown", @@ -86,6 +92,7 @@ }, { "cell_type": "code", + "execution_count": 3, "id": "3997a16202edbedd", "metadata": { "ExecuteTime": { @@ -93,12 +100,11 @@ "start_time": "2026-02-25T17:39:07.654224Z" } }, - "source": [ - "u_target = Input('u_target').last()\n", - "bound_cond = Input('b').last()" - ], "outputs": [], - "execution_count": 3 + "source": [ + "u_target = Input(\"u_target\").last()\n", + "bound_cond = Input(\"b\").last()" + ] }, { "cell_type": "markdown", @@ -110,6 +116,7 @@ }, { "cell_type": "code", + "execution_count": 4, "id": "bca4c69105a806fb", "metadata": { "ExecuteTime": { @@ -117,20 +124,13 @@ "start_time": "2026-02-25T17:39:07.658898Z" } }, - "source": [ - "pinn = Modely(visualizer=TextVisualizer(), seed=42)\n", - "pinn.addModel('pinn',[U,F])\n", - "pinn.addMinimize('errorU',u * bound_cond, u_target * bound_cond)\n", - "pinn.addMinimize('errorF',f, bound_cond * 0)\n", - "pinn.neuralizeModel()" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001B[1;32m================================ nnodely Model =================================\u001B[0m\n", - "\u001B[32m{'Constants': {'Constant18': {'dim': 1, 'values': [0.0031830989755690098]},\n", + "\u001b[1;32m================================ nnodely Model =================================\u001b[0m\n", + "\u001b[32m{'Constants': {'Constant18': {'dim': 1, 'values': [0.0031830989755690098]},\n", " 'Constant23': {'dim': 1, 'values': [0.0]}},\n", " 'Functions': {},\n", " 'Info': {'SampleTime': 1,\n", @@ -1555,12 +1555,18 @@ " 'Tanh10': ['Tanh', ['Linear9']],\n", " 'Tanh12': ['Tanh', ['Linear11']],\n", " 'Tanh6': ['Tanh', ['Linear5']],\n", - " 'Tanh8': ['Tanh', ['Linear7']]}}\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n" + " 'Tanh8': ['Tanh', ['Linear7']]}}\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n" ] } ], - "execution_count": 4 + "source": [ + "pinn = Modely(visualizer=TextVisualizer(), seed=42)\n", + "pinn.addModel(\"pinn\", [U, F])\n", + "pinn.addMinimize(\"errorU\", u * bound_cond, u_target * bound_cond)\n", + "pinn.addMinimize(\"errorF\", f, bound_cond * 0)\n", + "pinn.neuralizeModel()" + ] }, { "cell_type": "markdown", @@ -1572,6 +1578,7 @@ }, { "cell_type": "code", + "execution_count": 5, "id": "b5e35e8f7b0e19a2", "metadata": { "ExecuteTime": { @@ -1579,24 +1586,26 @@ "start_time": "2026-02-25T17:39:07.704197Z" } }, + "outputs": [], "source": [ "import torch\n", + "\n", "# Create boundary conditions\n", "Nu = 100\n", - "tt_0 = torch.zeros(Nu//2, dtype=torch.float32)\n", - "xx_0 = 2 * torch.rand(Nu//2, dtype=torch.float32) - 1\n", + "tt_0 = torch.zeros(Nu // 2, dtype=torch.float32)\n", + "xx_0 = 2 * torch.rand(Nu // 2, dtype=torch.float32) - 1\n", "uu_0 = -torch.sin(torch.pi * xx_0)\n", - "b_0 = torch.ones(Nu//2, dtype=torch.float32)\n", + "b_0 = torch.ones(Nu // 2, dtype=torch.float32)\n", "#\n", - "tt_1 = torch.rand(Nu//4, dtype=torch.float32)\n", - "xx_1 = torch.ones(Nu//4, dtype=torch.float32)\n", - "uu_1 = torch.zeros(Nu//4, dtype=torch.float32)\n", - "b_1 = torch.ones(Nu//4, dtype=torch.float32)\n", + "tt_1 = torch.rand(Nu // 4, dtype=torch.float32)\n", + "xx_1 = torch.ones(Nu // 4, dtype=torch.float32)\n", + "uu_1 = torch.zeros(Nu // 4, dtype=torch.float32)\n", + "b_1 = torch.ones(Nu // 4, dtype=torch.float32)\n", "#\n", - "tt_2 = torch.rand(Nu//4, dtype=torch.float32)\n", - "xx_2 = -torch.ones(Nu//4, dtype=torch.float32)\n", - "uu_2 = torch.zeros(Nu//4, dtype=torch.float32)\n", - "b_2 = torch.ones(Nu//4, dtype=torch.float32)\n", + "tt_2 = torch.rand(Nu // 4, dtype=torch.float32)\n", + "xx_2 = -torch.ones(Nu // 4, dtype=torch.float32)\n", + "uu_2 = torch.zeros(Nu // 4, dtype=torch.float32)\n", + "b_2 = torch.ones(Nu // 4, dtype=torch.float32)\n", "# Internal points\n", "Nf = 10000\n", "tt_3 = torch.rand(Nf, dtype=torch.float32)\n", @@ -1604,13 +1613,13 @@ "uu_3 = torch.zeros(Nf, dtype=torch.float32)\n", "b_3 = torch.zeros(Nf, dtype=torch.float32)\n", "\n", - "data = {'x':torch.cat((xx_0, xx_1, xx_2, xx_3)),\n", - " 't':torch.cat((tt_0, tt_1, tt_2, tt_3)),\n", - " 'u_target':torch.cat((uu_0, uu_1, uu_2, uu_3)),\n", - " 'b':torch.cat((b_0, b_1, b_2, b_3))}" - ], - "outputs": [], - "execution_count": 5 + "data = {\n", + " \"x\": torch.cat((xx_0, xx_1, xx_2, xx_3)),\n", + " \"t\": torch.cat((tt_0, tt_1, tt_2, tt_3)),\n", + " \"u_target\": torch.cat((uu_0, uu_1, uu_2, uu_3)),\n", + " \"b\": torch.cat((b_0, b_1, b_2, b_3)),\n", + "}" + ] }, { "cell_type": "markdown", @@ -1622,6 +1631,7 @@ }, { "cell_type": "code", + "execution_count": 6, "id": "4a20e7b55167eb6c", "metadata": { "ExecuteTime": { @@ -1629,52 +1639,43 @@ "start_time": "2026-02-25T17:39:07.718011Z" } }, - "source": [ - "from nnodely.support import earlystopping\n", - "pinn.loadData('dataset2',data)\n", - "pinn.trainModel(train_dataset='dataset2', train_batch_size=128, num_of_epochs=5000, lr=0.0005, \n", - " minimize_gain={'errorU':1,'errorF':0.0005}, \n", - " early_stopping=earlystopping.early_stop_patience, \n", - " early_stopping_params={'patience':500, 'error':'errorU'}, \n", - " select_model=earlystopping.select_best_model)" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001B[1;32m============================ nnodely Model Dataset =============================\u001B[0m\n", - "\u001B[32mDataset Name: dataset2\u001B[0m\n", - "\u001B[32mNumber of files: 1\u001B[0m\n", - "\u001B[32mTotal number of samples: 10100\u001B[0m\n", - "\u001B[32mShape of b: (10100, 1, 1)\u001B[0m\n", - "\u001B[32mShape of x: (10100, 1, 1)\u001B[0m\n", - "\u001B[32mShape of t: (10100, 1, 1)\u001B[0m\n", - "\u001B[32mShape of u_target: (10100, 1, 1)\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[33m[_setup_recurrent_variables] The value of the prediction_samples=0 but the network has no recurrent variables.\u001B[0m\n", - "\u001B[1;32m======================== nnodely Model Train Parameters ========================\u001B[0m\n", - "\u001B[32mmodels: ['pinn']\u001B[0m\n", - "\u001B[32mnum of epochs: 5000\u001B[0m\n", - "\u001B[32mupdate per epochs: 78\u001B[0m\n", - "\u001B[34mâ””>(n_samples-batch_size)/batch_size+1\u001B[0m\n", - "\u001B[32mshuffle data: True\u001B[0m\n", - "\u001B[32mearly stopping: early_stop_patience\u001B[0m\n", - "\u001B[32mearly stopping params: {'error': 'errorU', 'patience': 500}\u001B[0m\n", - "\u001B[32mtrain dataset: dataset2\u001B[0m\n", - "\u001B[32m\t- batch size: 128\u001B[0m\n", - "\u001B[32m\t- num of samples: 10100\u001B[0m\n", - "\u001B[32mminimizers: {'errorF': {'A': 'Sub22',\n", + "\u001b[1;32m============================ nnodely Model Dataset =============================\u001b[0m\n", + "\u001b[32mDataset Name: dataset2\u001b[0m\n", + "\u001b[32mNumber of files: 1\u001b[0m\n", + "\u001b[32mTotal number of samples: 10100\u001b[0m\n", + "\u001b[32mShape of b: (10100, 1, 1)\u001b[0m\n", + "\u001b[32mShape of x: (10100, 1, 1)\u001b[0m\n", + "\u001b[32mShape of t: (10100, 1, 1)\u001b[0m\n", + "\u001b[32mShape of u_target: (10100, 1, 1)\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[33m[_setup_recurrent_variables] The value of the prediction_samples=0 but the network has no recurrent variables.\u001b[0m\n", + "\u001b[1;32m======================== nnodely Model Train Parameters ========================\u001b[0m\n", + "\u001b[32mmodels: ['pinn']\u001b[0m\n", + "\u001b[32mnum of epochs: 5000\u001b[0m\n", + "\u001b[32mupdate per epochs: 78\u001b[0m\n", + "\u001b[34mâ””>(n_samples-batch_size)/batch_size+1\u001b[0m\n", + "\u001b[32mshuffle data: True\u001b[0m\n", + "\u001b[32mearly stopping: early_stop_patience\u001b[0m\n", + "\u001b[32mearly stopping params: {'error': 'errorU', 'patience': 500}\u001b[0m\n", + "\u001b[32mtrain dataset: dataset2\u001b[0m\n", + "\u001b[32m\t- batch size: 128\u001b[0m\n", + "\u001b[32m\t- num of samples: 10100\u001b[0m\n", + "\u001b[32mminimizers: {'errorF': {'A': 'Sub22',\n", " 'B': 'Mul30',\n", " 'gain': 0.0005,\n", " 'loss': 'mse'},\n", " 'errorU': {'A': 'Mul27',\n", " 'B': 'Mul28',\n", " 'gain': 1,\n", - " 'loss': 'mse'}}\u001B[0m\n", - "\u001B[32moptimizer: Adam\u001B[0m\n", - "\u001B[32moptimizer defaults: {'lr': 0.0005}\u001B[0m\n", - "\u001B[32moptimizer params: [{'params': 'PLinear12W'},\n", + " 'loss': 'mse'}}\u001b[0m\n", + "\u001b[32moptimizer: Adam\u001b[0m\n", + "\u001b[32moptimizer defaults: {'lr': 0.0005}\u001b[0m\n", + "\u001b[32moptimizer params: [{'params': 'PLinear12W'},\n", " {'params': 'PLinear12b'},\n", " {'params': 'PLinear15W'},\n", " {'params': 'PLinear15b'},\n", @@ -1683,92 +1684,106 @@ " {'params': 'PLinear6W'},\n", " {'params': 'PLinear6b'},\n", " {'params': 'PLinear9W'},\n", - " {'params': 'PLinear9b'}]\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[1;32m=========================== nnodely Training ===========================\u001B[0m\n", - "\u001B[32m| Epoch |\u001B[0m\u001B[32m errorU |\u001B[0m\u001B[32m errorF |\u001B[0m\u001B[32m Total |\u001B[0m\n", - "\u001B[32m| |\u001B[0m\u001B[32m Loss |\u001B[0m\u001B[32m Loss |\u001B[0m\u001B[32m Loss |\u001B[0m\n", - "\u001B[32m| |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m train |\u001B[0m\u001B[32m train |\u001B[0m\n", - "\u001B[32m|----------------------------------------------------------------------|\u001B[0m\n", - "\u001B[32m| 50/5000 |\u001B[0m\u001B[32m 9.018e-02 |\u001B[0m\u001B[32m 1.413e+00 |\u001B[0m\u001B[32m 7.515e-01 |\u001B[0m\n", - "\u001B[32m| 100/5000 |\u001B[0m\u001B[32m 1.886e-02 |\u001B[0m\u001B[32m 4.612e-02 |\u001B[0m\u001B[32m 3.249e-02 |\u001B[0m\n", - "\u001B[32m| 150/5000 |\u001B[0m\u001B[32m 3.863e-03 |\u001B[0m\u001B[32m 1.532e-03 |\u001B[0m\u001B[32m 2.698e-03 |\u001B[0m\n", - "\u001B[32m| 200/5000 |\u001B[0m\u001B[32m 1.946e-03 |\u001B[0m\u001B[32m 3.386e-04 |\u001B[0m\u001B[32m 1.142e-03 |\u001B[0m\n", - "\u001B[32m| 250/5000 |\u001B[0m\u001B[32m 1.838e-03 |\u001B[0m\u001B[32m 5.892e-04 |\u001B[0m\u001B[32m 1.214e-03 |\u001B[0m\n", - "\u001B[32m| 300/5000 |\u001B[0m\u001B[32m 1.732e-03 |\u001B[0m\u001B[32m 3.606e-04 |\u001B[0m\u001B[32m 1.046e-03 |\u001B[0m\n", - "\u001B[32m| 350/5000 |\u001B[0m\u001B[32m 1.636e-03 |\u001B[0m\u001B[32m 4.242e-04 |\u001B[0m\u001B[32m 1.030e-03 |\u001B[0m\n", - "\u001B[32m| 400/5000 |\u001B[0m\u001B[32m 1.564e-03 |\u001B[0m\u001B[32m 2.444e-04 |\u001B[0m\u001B[32m 9.043e-04 |\u001B[0m\n", - "\u001B[32m| 450/5000 |\u001B[0m\u001B[32m 1.543e-03 |\u001B[0m\u001B[32m 1.600e-04 |\u001B[0m\u001B[32m 8.514e-04 |\u001B[0m\n", - "\u001B[32m| 500/5000 |\u001B[0m\u001B[32m 1.53e-03 |\u001B[0m\u001B[32m 1.101e-04 |\u001B[0m\u001B[32m 8.200e-04 |\u001B[0m\n", - "\u001B[32m| 550/5000 |\u001B[0m\u001B[32m 1.505e-03 |\u001B[0m\u001B[32m 6.108e-05 |\u001B[0m\u001B[32m 7.830e-04 |\u001B[0m\n", - "\u001B[32m| 600/5000 |\u001B[0m\u001B[32m 1.504e-03 |\u001B[0m\u001B[32m 3.504e-05 |\u001B[0m\u001B[32m 7.694e-04 |\u001B[0m\n", - "\u001B[32m| 650/5000 |\u001B[0m\u001B[32m 3.171e-04 |\u001B[0m\u001B[32m 1.438e-04 |\u001B[0m\u001B[32m 2.304e-04 |\u001B[0m\n", - "\u001B[32m| 700/5000 |\u001B[0m\u001B[32m 8.892e-05 |\u001B[0m\u001B[32m 1.083e-04 |\u001B[0m\u001B[32m 9.863e-05 |\u001B[0m\n", - "\u001B[32m| 750/5000 |\u001B[0m\u001B[32m 4.373e-05 |\u001B[0m\u001B[32m 8.569e-05 |\u001B[0m\u001B[32m 6.471e-05 |\u001B[0m\n", - "\u001B[32m| 800/5000 |\u001B[0m\u001B[32m 2.790e-05 |\u001B[0m\u001B[32m 5.551e-05 |\u001B[0m\u001B[32m 4.170e-05 |\u001B[0m\n", - "\u001B[32m| 850/5000 |\u001B[0m\u001B[32m 3.441e-05 |\u001B[0m\u001B[32m 5.859e-05 |\u001B[0m\u001B[32m 4.650e-05 |\u001B[0m\n", - "\u001B[32m| 900/5000 |\u001B[0m\u001B[32m 6.985e-06 |\u001B[0m\u001B[32m 1.973e-05 |\u001B[0m\u001B[32m 1.336e-05 |\u001B[0m\n", - "\u001B[32m| 950/5000 |\u001B[0m\u001B[32m 6.454e-06 |\u001B[0m\u001B[32m 1.649e-05 |\u001B[0m\u001B[32m 1.147e-05 |\u001B[0m\n", - "\u001B[32m|1000/5000 |\u001B[0m\u001B[32m 7.318e-06 |\u001B[0m\u001B[32m 1.616e-05 |\u001B[0m\u001B[32m 1.174e-05 |\u001B[0m\n", - "\u001B[32m|1050/5000 |\u001B[0m\u001B[32m 7.773e-06 |\u001B[0m\u001B[32m 1.579e-05 |\u001B[0m\u001B[32m 1.178e-05 |\u001B[0m\n", - "\u001B[32m|1100/5000 |\u001B[0m\u001B[32m 5.03e-06 |\u001B[0m\u001B[32m 1.188e-05 |\u001B[0m\u001B[32m 8.453e-06 |\u001B[0m\n", - "\u001B[32m|1150/5000 |\u001B[0m\u001B[32m 3.668e-06 |\u001B[0m\u001B[32m 8.326e-06 |\u001B[0m\u001B[32m 5.997e-06 |\u001B[0m\n", - "\u001B[32m|1200/5000 |\u001B[0m\u001B[32m 2.6e-06 |\u001B[0m\u001B[32m 7.771e-06 |\u001B[0m\u001B[32m 5.185e-06 |\u001B[0m\n", - "\u001B[32m|1250/5000 |\u001B[0m\u001B[32m 8.389e-06 |\u001B[0m\u001B[32m 1.483e-05 |\u001B[0m\u001B[32m 1.161e-05 |\u001B[0m\n", - "\u001B[32m|1300/5000 |\u001B[0m\u001B[32m 2.944e-06 |\u001B[0m\u001B[32m 6.592e-06 |\u001B[0m\u001B[32m 4.768e-06 |\u001B[0m\n", - "\u001B[32m|1350/5000 |\u001B[0m\u001B[32m 4.007e-06 |\u001B[0m\u001B[32m 8.243e-06 |\u001B[0m\u001B[32m 6.125e-06 |\u001B[0m\n", - "\u001B[32m|1400/5000 |\u001B[0m\u001B[32m 4.226e-06 |\u001B[0m\u001B[32m 8.995e-06 |\u001B[0m\u001B[32m 6.610e-06 |\u001B[0m\n", - "\u001B[32m|1450/5000 |\u001B[0m\u001B[32m 2.225e-05 |\u001B[0m\u001B[32m 2.984e-05 |\u001B[0m\u001B[32m 2.604e-05 |\u001B[0m\n", - "\u001B[32m|1500/5000 |\u001B[0m\u001B[32m 1.576e-05 |\u001B[0m\u001B[32m 2.343e-05 |\u001B[0m\u001B[32m 1.959e-05 |\u001B[0m\n", - "\u001B[32m|1550/5000 |\u001B[0m\u001B[32m 5.138e-06 |\u001B[0m\u001B[32m 1.073e-05 |\u001B[0m\u001B[32m 7.933e-06 |\u001B[0m\n", - "\u001B[32m|1600/5000 |\u001B[0m\u001B[32m 2.006e-06 |\u001B[0m\u001B[32m 4.534e-06 |\u001B[0m\u001B[32m 3.270e-06 |\u001B[0m\n", - "\u001B[32m|1650/5000 |\u001B[0m\u001B[32m 3.014e-06 |\u001B[0m\u001B[32m 7.368e-06 |\u001B[0m\u001B[32m 5.191e-06 |\u001B[0m\n", - "\u001B[32m|1700/5000 |\u001B[0m\u001B[32m 1.711e-06 |\u001B[0m\u001B[32m 3.738e-06 |\u001B[0m\u001B[32m 2.724e-06 |\u001B[0m\n", - "\u001B[32m|1750/5000 |\u001B[0m\u001B[32m 1.402e-06 |\u001B[0m\u001B[32m 3.050e-06 |\u001B[0m\u001B[32m 2.226e-06 |\u001B[0m\n", - "\u001B[32m|1800/5000 |\u001B[0m\u001B[32m 5.405e-06 |\u001B[0m\u001B[32m 6.894e-06 |\u001B[0m\u001B[32m 6.15e-06 |\u001B[0m\n", - "\u001B[32m|1850/5000 |\u001B[0m\u001B[32m 4.134e-06 |\u001B[0m\u001B[32m 4.535e-06 |\u001B[0m\u001B[32m 4.334e-06 |\u001B[0m\n", - "\u001B[32m|1900/5000 |\u001B[0m\u001B[32m 1.579e-06 |\u001B[0m\u001B[32m 2.762e-06 |\u001B[0m\u001B[32m 2.171e-06 |\u001B[0m\n", - "\u001B[32m|1950/5000 |\u001B[0m\u001B[32m 2.646e-06 |\u001B[0m\u001B[32m 5.012e-06 |\u001B[0m\u001B[32m 3.829e-06 |\u001B[0m\n", - "\u001B[32m|2000/5000 |\u001B[0m\u001B[32m 1.381e-06 |\u001B[0m\u001B[32m 1.573e-06 |\u001B[0m\u001B[32m 1.477e-06 |\u001B[0m\n", - "\u001B[32m|2050/5000 |\u001B[0m\u001B[32m 1.613e-06 |\u001B[0m\u001B[32m 3.587e-06 |\u001B[0m\u001B[32m 2.6e-06 |\u001B[0m\n", - "\u001B[32m|2100/5000 |\u001B[0m\u001B[32m 2.693e-06 |\u001B[0m\u001B[32m 3.447e-06 |\u001B[0m\u001B[32m 3.07e-06 |\u001B[0m\n", - "\u001B[32m|2150/5000 |\u001B[0m\u001B[32m 1.465e-06 |\u001B[0m\u001B[32m 1.942e-06 |\u001B[0m\u001B[32m 1.704e-06 |\u001B[0m\n", - "\u001B[32m|2200/5000 |\u001B[0m\u001B[32m 1.141e-06 |\u001B[0m\u001B[32m 1.933e-06 |\u001B[0m\u001B[32m 1.537e-06 |\u001B[0m\n", - "\u001B[32m|2250/5000 |\u001B[0m\u001B[32m 4.073e-06 |\u001B[0m\u001B[32m 3.550e-06 |\u001B[0m\u001B[32m 3.812e-06 |\u001B[0m\n", - "\u001B[32m|2300/5000 |\u001B[0m\u001B[32m 2.918e-06 |\u001B[0m\u001B[32m 2.235e-06 |\u001B[0m\u001B[32m 2.576e-06 |\u001B[0m\n", - "\u001B[32m|2350/5000 |\u001B[0m\u001B[32m 2.835e-06 |\u001B[0m\u001B[32m 1.932e-06 |\u001B[0m\u001B[32m 2.384e-06 |\u001B[0m\n", - "\u001B[32m|2400/5000 |\u001B[0m\u001B[32m 7.148e-06 |\u001B[0m\u001B[32m 6.592e-06 |\u001B[0m\u001B[32m 6.870e-06 |\u001B[0m\n", - "\u001B[32m|2450/5000 |\u001B[0m\u001B[32m 8.410e-07 |\u001B[0m\u001B[32m 1.945e-06 |\u001B[0m\u001B[32m 1.393e-06 |\u001B[0m\n", - "\u001B[32m|2500/5000 |\u001B[0m\u001B[32m 6.379e-07 |\u001B[0m\u001B[32m 9.164e-07 |\u001B[0m\u001B[32m 7.772e-07 |\u001B[0m\n", - "\u001B[32m|2550/5000 |\u001B[0m\u001B[32m 1.717e-06 |\u001B[0m\u001B[32m 2.593e-06 |\u001B[0m\u001B[32m 2.155e-06 |\u001B[0m\n", - "\u001B[32m|2600/5000 |\u001B[0m\u001B[32m 2.722e-06 |\u001B[0m\u001B[32m 7.063e-06 |\u001B[0m\u001B[32m 4.892e-06 |\u001B[0m\n", - "\u001B[32m|2650/5000 |\u001B[0m\u001B[32m 5.706e-07 |\u001B[0m\u001B[32m 1.253e-06 |\u001B[0m\u001B[32m 9.120e-07 |\u001B[0m\n", - "\u001B[32m|2700/5000 |\u001B[0m\u001B[32m 3.990e-07 |\u001B[0m\u001B[32m 1.038e-06 |\u001B[0m\u001B[32m 7.186e-07 |\u001B[0m\n", - "\u001B[32m|2750/5000 |\u001B[0m\u001B[32m 9.519e-07 |\u001B[0m\u001B[32m 1.662e-06 |\u001B[0m\u001B[32m 1.307e-06 |\u001B[0m\n", - "\u001B[32m|2800/5000 |\u001B[0m\u001B[32m 4.111e-06 |\u001B[0m\u001B[32m 3.098e-06 |\u001B[0m\u001B[32m 3.605e-06 |\u001B[0m\n", - "\u001B[32m|2850/5000 |\u001B[0m\u001B[32m 1.347e-06 |\u001B[0m\u001B[32m 1.464e-06 |\u001B[0m\u001B[32m 1.405e-06 |\u001B[0m\n", - "\u001B[32m|2900/5000 |\u001B[0m\u001B[32m 5.103e-07 |\u001B[0m\u001B[32m 1.178e-06 |\u001B[0m\u001B[32m 8.44e-07 |\u001B[0m\n", - "\u001B[32m|2950/5000 |\u001B[0m\u001B[32m 5.521e-07 |\u001B[0m\u001B[32m 1.155e-06 |\u001B[0m\u001B[32m 8.536e-07 |\u001B[0m\n", - "\u001B[32m|3000/5000 |\u001B[0m\u001B[32m 5.444e-07 |\u001B[0m\u001B[32m 7.651e-07 |\u001B[0m\u001B[32m 6.548e-07 |\u001B[0m\n", - "\u001B[32m|3050/5000 |\u001B[0m\u001B[32m 4.684e-07 |\u001B[0m\u001B[32m 6.802e-07 |\u001B[0m\u001B[32m 5.743e-07 |\u001B[0m\n", - "\u001B[32m|3100/5000 |\u001B[0m\u001B[32m 7.432e-07 |\u001B[0m\u001B[32m 1.625e-06 |\u001B[0m\u001B[32m 1.184e-06 |\u001B[0m\n", - "\u001B[32m|3150/5000 |\u001B[0m\u001B[32m 1.323e-06 |\u001B[0m\u001B[32m 2.188e-06 |\u001B[0m\u001B[32m 1.755e-06 |\u001B[0m\n", - "\u001B[32m|3200/5000 |\u001B[0m\u001B[32m 1.093e-06 |\u001B[0m\u001B[32m 1.363e-06 |\u001B[0m\u001B[32m 1.228e-06 |\u001B[0m\n", - "\u001B[32m|3250/5000 |\u001B[0m\u001B[32m 1.243e-06 |\u001B[0m\u001B[32m 1.959e-06 |\u001B[0m\u001B[32m 1.601e-06 |\u001B[0m\n", - "\u001B[32m|3300/5000 |\u001B[0m\u001B[32m 1.701e-06 |\u001B[0m\u001B[32m 3.564e-06 |\u001B[0m\u001B[32m 2.632e-06 |\u001B[0m\n", - "\u001B[32m|3350/5000 |\u001B[0m\u001B[32m 5.400e-07 |\u001B[0m\u001B[32m 7.495e-07 |\u001B[0m\u001B[32m 6.448e-07 |\u001B[0m\n", - "\u001B[32m|3400/5000 |\u001B[0m\u001B[32m 6.398e-07 |\u001B[0m\u001B[32m 1.927e-06 |\u001B[0m\u001B[32m 1.283e-06 |\u001B[0m\n", - "\u001B[32m|3450/5000 |\u001B[0m\u001B[32m 7.234e-06 |\u001B[0m\u001B[32m 1.095e-05 |\u001B[0m\u001B[32m 9.094e-06 |\u001B[0m\n", - "|3485/5000 | 6.045e-07 | 9.439e-07 | 7.742e-07 |\u001B[34mStopping the training at epoch 3485 due to early stopping.\u001B[0m\n", - "\u001B[1;32m============================ nnodely Training Time =============================\u001B[0m\n", - "\u001B[32mTotal time of Training: 694.5884737968445\u001B[0m\n", - "\u001B[32m================================================================================\u001B[0m\n", - "\u001B[33m[trainModel] If not validation set is provided the selected model can differ from the optimal.\u001B[0m\n", - "\u001B[34mSelected the model at the epoch 3461.\u001B[0m\n" + " {'params': 'PLinear9b'}]\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[1;32m=========================== nnodely Training ===========================\u001b[0m\n", + "\u001b[32m| Epoch |\u001b[0m\u001b[32m errorU |\u001b[0m\u001b[32m errorF |\u001b[0m\u001b[32m Total |\u001b[0m\n", + "\u001b[32m| |\u001b[0m\u001b[32m Loss |\u001b[0m\u001b[32m Loss |\u001b[0m\u001b[32m Loss |\u001b[0m\n", + "\u001b[32m| |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m train |\u001b[0m\u001b[32m train |\u001b[0m\n", + "\u001b[32m|----------------------------------------------------------------------|\u001b[0m\n", + "\u001b[32m| 50/5000 |\u001b[0m\u001b[32m 9.018e-02 |\u001b[0m\u001b[32m 1.413e+00 |\u001b[0m\u001b[32m 7.515e-01 |\u001b[0m\n", + "\u001b[32m| 100/5000 |\u001b[0m\u001b[32m 1.886e-02 |\u001b[0m\u001b[32m 4.612e-02 |\u001b[0m\u001b[32m 3.249e-02 |\u001b[0m\n", + "\u001b[32m| 150/5000 |\u001b[0m\u001b[32m 3.863e-03 |\u001b[0m\u001b[32m 1.532e-03 |\u001b[0m\u001b[32m 2.698e-03 |\u001b[0m\n", + "\u001b[32m| 200/5000 |\u001b[0m\u001b[32m 1.946e-03 |\u001b[0m\u001b[32m 3.386e-04 |\u001b[0m\u001b[32m 1.142e-03 |\u001b[0m\n", + "\u001b[32m| 250/5000 |\u001b[0m\u001b[32m 1.838e-03 |\u001b[0m\u001b[32m 5.892e-04 |\u001b[0m\u001b[32m 1.214e-03 |\u001b[0m\n", + "\u001b[32m| 300/5000 |\u001b[0m\u001b[32m 1.732e-03 |\u001b[0m\u001b[32m 3.606e-04 |\u001b[0m\u001b[32m 1.046e-03 |\u001b[0m\n", + "\u001b[32m| 350/5000 |\u001b[0m\u001b[32m 1.636e-03 |\u001b[0m\u001b[32m 4.242e-04 |\u001b[0m\u001b[32m 1.030e-03 |\u001b[0m\n", + "\u001b[32m| 400/5000 |\u001b[0m\u001b[32m 1.564e-03 |\u001b[0m\u001b[32m 2.444e-04 |\u001b[0m\u001b[32m 9.043e-04 |\u001b[0m\n", + "\u001b[32m| 450/5000 |\u001b[0m\u001b[32m 1.543e-03 |\u001b[0m\u001b[32m 1.600e-04 |\u001b[0m\u001b[32m 8.514e-04 |\u001b[0m\n", + "\u001b[32m| 500/5000 |\u001b[0m\u001b[32m 1.53e-03 |\u001b[0m\u001b[32m 1.101e-04 |\u001b[0m\u001b[32m 8.200e-04 |\u001b[0m\n", + "\u001b[32m| 550/5000 |\u001b[0m\u001b[32m 1.505e-03 |\u001b[0m\u001b[32m 6.108e-05 |\u001b[0m\u001b[32m 7.830e-04 |\u001b[0m\n", + "\u001b[32m| 600/5000 |\u001b[0m\u001b[32m 1.504e-03 |\u001b[0m\u001b[32m 3.504e-05 |\u001b[0m\u001b[32m 7.694e-04 |\u001b[0m\n", + "\u001b[32m| 650/5000 |\u001b[0m\u001b[32m 3.171e-04 |\u001b[0m\u001b[32m 1.438e-04 |\u001b[0m\u001b[32m 2.304e-04 |\u001b[0m\n", + "\u001b[32m| 700/5000 |\u001b[0m\u001b[32m 8.892e-05 |\u001b[0m\u001b[32m 1.083e-04 |\u001b[0m\u001b[32m 9.863e-05 |\u001b[0m\n", + "\u001b[32m| 750/5000 |\u001b[0m\u001b[32m 4.373e-05 |\u001b[0m\u001b[32m 8.569e-05 |\u001b[0m\u001b[32m 6.471e-05 |\u001b[0m\n", + "\u001b[32m| 800/5000 |\u001b[0m\u001b[32m 2.790e-05 |\u001b[0m\u001b[32m 5.551e-05 |\u001b[0m\u001b[32m 4.170e-05 |\u001b[0m\n", + "\u001b[32m| 850/5000 |\u001b[0m\u001b[32m 3.441e-05 |\u001b[0m\u001b[32m 5.859e-05 |\u001b[0m\u001b[32m 4.650e-05 |\u001b[0m\n", + "\u001b[32m| 900/5000 |\u001b[0m\u001b[32m 6.985e-06 |\u001b[0m\u001b[32m 1.973e-05 |\u001b[0m\u001b[32m 1.336e-05 |\u001b[0m\n", + "\u001b[32m| 950/5000 |\u001b[0m\u001b[32m 6.454e-06 |\u001b[0m\u001b[32m 1.649e-05 |\u001b[0m\u001b[32m 1.147e-05 |\u001b[0m\n", + "\u001b[32m|1000/5000 |\u001b[0m\u001b[32m 7.318e-06 |\u001b[0m\u001b[32m 1.616e-05 |\u001b[0m\u001b[32m 1.174e-05 |\u001b[0m\n", + "\u001b[32m|1050/5000 |\u001b[0m\u001b[32m 7.773e-06 |\u001b[0m\u001b[32m 1.579e-05 |\u001b[0m\u001b[32m 1.178e-05 |\u001b[0m\n", + "\u001b[32m|1100/5000 |\u001b[0m\u001b[32m 5.03e-06 |\u001b[0m\u001b[32m 1.188e-05 |\u001b[0m\u001b[32m 8.453e-06 |\u001b[0m\n", + "\u001b[32m|1150/5000 |\u001b[0m\u001b[32m 3.668e-06 |\u001b[0m\u001b[32m 8.326e-06 |\u001b[0m\u001b[32m 5.997e-06 |\u001b[0m\n", + "\u001b[32m|1200/5000 |\u001b[0m\u001b[32m 2.6e-06 |\u001b[0m\u001b[32m 7.771e-06 |\u001b[0m\u001b[32m 5.185e-06 |\u001b[0m\n", + "\u001b[32m|1250/5000 |\u001b[0m\u001b[32m 8.389e-06 |\u001b[0m\u001b[32m 1.483e-05 |\u001b[0m\u001b[32m 1.161e-05 |\u001b[0m\n", + "\u001b[32m|1300/5000 |\u001b[0m\u001b[32m 2.944e-06 |\u001b[0m\u001b[32m 6.592e-06 |\u001b[0m\u001b[32m 4.768e-06 |\u001b[0m\n", + "\u001b[32m|1350/5000 |\u001b[0m\u001b[32m 4.007e-06 |\u001b[0m\u001b[32m 8.243e-06 |\u001b[0m\u001b[32m 6.125e-06 |\u001b[0m\n", + "\u001b[32m|1400/5000 |\u001b[0m\u001b[32m 4.226e-06 |\u001b[0m\u001b[32m 8.995e-06 |\u001b[0m\u001b[32m 6.610e-06 |\u001b[0m\n", + "\u001b[32m|1450/5000 |\u001b[0m\u001b[32m 2.225e-05 |\u001b[0m\u001b[32m 2.984e-05 |\u001b[0m\u001b[32m 2.604e-05 |\u001b[0m\n", + "\u001b[32m|1500/5000 |\u001b[0m\u001b[32m 1.576e-05 |\u001b[0m\u001b[32m 2.343e-05 |\u001b[0m\u001b[32m 1.959e-05 |\u001b[0m\n", + "\u001b[32m|1550/5000 |\u001b[0m\u001b[32m 5.138e-06 |\u001b[0m\u001b[32m 1.073e-05 |\u001b[0m\u001b[32m 7.933e-06 |\u001b[0m\n", + "\u001b[32m|1600/5000 |\u001b[0m\u001b[32m 2.006e-06 |\u001b[0m\u001b[32m 4.534e-06 |\u001b[0m\u001b[32m 3.270e-06 |\u001b[0m\n", + "\u001b[32m|1650/5000 |\u001b[0m\u001b[32m 3.014e-06 |\u001b[0m\u001b[32m 7.368e-06 |\u001b[0m\u001b[32m 5.191e-06 |\u001b[0m\n", + "\u001b[32m|1700/5000 |\u001b[0m\u001b[32m 1.711e-06 |\u001b[0m\u001b[32m 3.738e-06 |\u001b[0m\u001b[32m 2.724e-06 |\u001b[0m\n", + "\u001b[32m|1750/5000 |\u001b[0m\u001b[32m 1.402e-06 |\u001b[0m\u001b[32m 3.050e-06 |\u001b[0m\u001b[32m 2.226e-06 |\u001b[0m\n", + "\u001b[32m|1800/5000 |\u001b[0m\u001b[32m 5.405e-06 |\u001b[0m\u001b[32m 6.894e-06 |\u001b[0m\u001b[32m 6.15e-06 |\u001b[0m\n", + "\u001b[32m|1850/5000 |\u001b[0m\u001b[32m 4.134e-06 |\u001b[0m\u001b[32m 4.535e-06 |\u001b[0m\u001b[32m 4.334e-06 |\u001b[0m\n", + "\u001b[32m|1900/5000 |\u001b[0m\u001b[32m 1.579e-06 |\u001b[0m\u001b[32m 2.762e-06 |\u001b[0m\u001b[32m 2.171e-06 |\u001b[0m\n", + "\u001b[32m|1950/5000 |\u001b[0m\u001b[32m 2.646e-06 |\u001b[0m\u001b[32m 5.012e-06 |\u001b[0m\u001b[32m 3.829e-06 |\u001b[0m\n", + "\u001b[32m|2000/5000 |\u001b[0m\u001b[32m 1.381e-06 |\u001b[0m\u001b[32m 1.573e-06 |\u001b[0m\u001b[32m 1.477e-06 |\u001b[0m\n", + "\u001b[32m|2050/5000 |\u001b[0m\u001b[32m 1.613e-06 |\u001b[0m\u001b[32m 3.587e-06 |\u001b[0m\u001b[32m 2.6e-06 |\u001b[0m\n", + "\u001b[32m|2100/5000 |\u001b[0m\u001b[32m 2.693e-06 |\u001b[0m\u001b[32m 3.447e-06 |\u001b[0m\u001b[32m 3.07e-06 |\u001b[0m\n", + "\u001b[32m|2150/5000 |\u001b[0m\u001b[32m 1.465e-06 |\u001b[0m\u001b[32m 1.942e-06 |\u001b[0m\u001b[32m 1.704e-06 |\u001b[0m\n", + "\u001b[32m|2200/5000 |\u001b[0m\u001b[32m 1.141e-06 |\u001b[0m\u001b[32m 1.933e-06 |\u001b[0m\u001b[32m 1.537e-06 |\u001b[0m\n", + "\u001b[32m|2250/5000 |\u001b[0m\u001b[32m 4.073e-06 |\u001b[0m\u001b[32m 3.550e-06 |\u001b[0m\u001b[32m 3.812e-06 |\u001b[0m\n", + "\u001b[32m|2300/5000 |\u001b[0m\u001b[32m 2.918e-06 |\u001b[0m\u001b[32m 2.235e-06 |\u001b[0m\u001b[32m 2.576e-06 |\u001b[0m\n", + "\u001b[32m|2350/5000 |\u001b[0m\u001b[32m 2.835e-06 |\u001b[0m\u001b[32m 1.932e-06 |\u001b[0m\u001b[32m 2.384e-06 |\u001b[0m\n", + "\u001b[32m|2400/5000 |\u001b[0m\u001b[32m 7.148e-06 |\u001b[0m\u001b[32m 6.592e-06 |\u001b[0m\u001b[32m 6.870e-06 |\u001b[0m\n", + "\u001b[32m|2450/5000 |\u001b[0m\u001b[32m 8.410e-07 |\u001b[0m\u001b[32m 1.945e-06 |\u001b[0m\u001b[32m 1.393e-06 |\u001b[0m\n", + "\u001b[32m|2500/5000 |\u001b[0m\u001b[32m 6.379e-07 |\u001b[0m\u001b[32m 9.164e-07 |\u001b[0m\u001b[32m 7.772e-07 |\u001b[0m\n", + "\u001b[32m|2550/5000 |\u001b[0m\u001b[32m 1.717e-06 |\u001b[0m\u001b[32m 2.593e-06 |\u001b[0m\u001b[32m 2.155e-06 |\u001b[0m\n", + "\u001b[32m|2600/5000 |\u001b[0m\u001b[32m 2.722e-06 |\u001b[0m\u001b[32m 7.063e-06 |\u001b[0m\u001b[32m 4.892e-06 |\u001b[0m\n", + "\u001b[32m|2650/5000 |\u001b[0m\u001b[32m 5.706e-07 |\u001b[0m\u001b[32m 1.253e-06 |\u001b[0m\u001b[32m 9.120e-07 |\u001b[0m\n", + "\u001b[32m|2700/5000 |\u001b[0m\u001b[32m 3.990e-07 |\u001b[0m\u001b[32m 1.038e-06 |\u001b[0m\u001b[32m 7.186e-07 |\u001b[0m\n", + "\u001b[32m|2750/5000 |\u001b[0m\u001b[32m 9.519e-07 |\u001b[0m\u001b[32m 1.662e-06 |\u001b[0m\u001b[32m 1.307e-06 |\u001b[0m\n", + "\u001b[32m|2800/5000 |\u001b[0m\u001b[32m 4.111e-06 |\u001b[0m\u001b[32m 3.098e-06 |\u001b[0m\u001b[32m 3.605e-06 |\u001b[0m\n", + "\u001b[32m|2850/5000 |\u001b[0m\u001b[32m 1.347e-06 |\u001b[0m\u001b[32m 1.464e-06 |\u001b[0m\u001b[32m 1.405e-06 |\u001b[0m\n", + "\u001b[32m|2900/5000 |\u001b[0m\u001b[32m 5.103e-07 |\u001b[0m\u001b[32m 1.178e-06 |\u001b[0m\u001b[32m 8.44e-07 |\u001b[0m\n", + "\u001b[32m|2950/5000 |\u001b[0m\u001b[32m 5.521e-07 |\u001b[0m\u001b[32m 1.155e-06 |\u001b[0m\u001b[32m 8.536e-07 |\u001b[0m\n", + "\u001b[32m|3000/5000 |\u001b[0m\u001b[32m 5.444e-07 |\u001b[0m\u001b[32m 7.651e-07 |\u001b[0m\u001b[32m 6.548e-07 |\u001b[0m\n", + "\u001b[32m|3050/5000 |\u001b[0m\u001b[32m 4.684e-07 |\u001b[0m\u001b[32m 6.802e-07 |\u001b[0m\u001b[32m 5.743e-07 |\u001b[0m\n", + "\u001b[32m|3100/5000 |\u001b[0m\u001b[32m 7.432e-07 |\u001b[0m\u001b[32m 1.625e-06 |\u001b[0m\u001b[32m 1.184e-06 |\u001b[0m\n", + "\u001b[32m|3150/5000 |\u001b[0m\u001b[32m 1.323e-06 |\u001b[0m\u001b[32m 2.188e-06 |\u001b[0m\u001b[32m 1.755e-06 |\u001b[0m\n", + "\u001b[32m|3200/5000 |\u001b[0m\u001b[32m 1.093e-06 |\u001b[0m\u001b[32m 1.363e-06 |\u001b[0m\u001b[32m 1.228e-06 |\u001b[0m\n", + "\u001b[32m|3250/5000 |\u001b[0m\u001b[32m 1.243e-06 |\u001b[0m\u001b[32m 1.959e-06 |\u001b[0m\u001b[32m 1.601e-06 |\u001b[0m\n", + "\u001b[32m|3300/5000 |\u001b[0m\u001b[32m 1.701e-06 |\u001b[0m\u001b[32m 3.564e-06 |\u001b[0m\u001b[32m 2.632e-06 |\u001b[0m\n", + "\u001b[32m|3350/5000 |\u001b[0m\u001b[32m 5.400e-07 |\u001b[0m\u001b[32m 7.495e-07 |\u001b[0m\u001b[32m 6.448e-07 |\u001b[0m\n", + "\u001b[32m|3400/5000 |\u001b[0m\u001b[32m 6.398e-07 |\u001b[0m\u001b[32m 1.927e-06 |\u001b[0m\u001b[32m 1.283e-06 |\u001b[0m\n", + "\u001b[32m|3450/5000 |\u001b[0m\u001b[32m 7.234e-06 |\u001b[0m\u001b[32m 1.095e-05 |\u001b[0m\u001b[32m 9.094e-06 |\u001b[0m\n", + "|3485/5000 | 6.045e-07 | 9.439e-07 | 7.742e-07 |\u001b[34mStopping the training at epoch 3485 due to early stopping.\u001b[0m\n", + "\u001b[1;32m============================ nnodely Training Time =============================\u001b[0m\n", + "\u001b[32mTotal time of Training: 694.5884737968445\u001b[0m\n", + "\u001b[32m================================================================================\u001b[0m\n", + "\u001b[33m[trainModel] If not validation set is provided the selected model can differ from the optimal.\u001b[0m\n", + "\u001b[34mSelected the model at the epoch 3461.\u001b[0m\n" ] } ], - "execution_count": 6 + "source": [ + "from nnodely.support import earlystopping\n", + "\n", + "pinn.loadData(\"dataset2\", data)\n", + "pinn.trainModel(\n", + " train_dataset=\"dataset2\",\n", + " train_batch_size=128,\n", + " num_of_epochs=5000,\n", + " lr=0.0005,\n", + " minimize_gain={\"errorU\": 1, \"errorF\": 0.0005},\n", + " early_stopping=earlystopping.early_stop_patience,\n", + " early_stopping_params={\"patience\": 500, \"error\": \"errorU\"},\n", + " select_model=earlystopping.select_best_model,\n", + ")" + ] }, { "cell_type": "markdown", @@ -1780,6 +1795,7 @@ }, { "cell_type": "code", + "execution_count": 7, "id": "51fa79586fe9b9be", "metadata": { "ExecuteTime": { @@ -1787,153 +1803,155 @@ "start_time": "2026-02-25T17:50:42.919168Z" } }, - "source": [ - "# Custom visualizer for results\n", - "class FunctionVisualizer(TextVisualizer):\n", - " def showResults(self):\n", - " import matplotlib.pyplot as plt\n", - " plt.figure()\n", - " plt.title('Initial Condition')\n", - " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", - " t = torch.zeros(100, dtype=torch.float32)\n", - " u_target = -torch.sin(torch.pi * x)\n", - " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n", - " plt.plot(x.tolist(), u['U'], label=f'network')\n", - " plt.plot(x.tolist(), u_target.tolist(), label=f'target')\n", - " plt.grid(True)\n", - " plt.legend(loc='best')\n", - " plt.xlabel('x[t=0]')\n", - " plt.ylabel('u')\n", - "\n", - " plt.figure()\n", - " plt.title('U value at t=[0.25,0.5,0.75,1]')\n", - " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", - " t = torch.ones(100, dtype=torch.float32)*0.25\n", - " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n", - " plt.plot(x.tolist(), u['U'], label=f'network t=0.25')\n", - " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", - " t = torch.ones(100, dtype=torch.float32)*0.5\n", - " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n", - " plt.plot(x.tolist(), u['U'], label=f'network t=0.5')\n", - " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", - " t = torch.ones(100, dtype=torch.float32)*0.75\n", - " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n", - " plt.plot(x.tolist(), u['U'], label=f'network t=0.75')\n", - " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", - " t = torch.ones(100, dtype=torch.float32)\n", - " u = self.modely({'x':x.tolist(),'t':t.tolist()})\n", - " plt.plot(x.tolist(), u['U'], label=f'network t=1')\n", - " plt.grid(True)\n", - " plt.legend(loc='best')\n", - " plt.xlabel('x')\n", - " plt.ylabel('u')\n", - "\n", - " plt.figure()\n", - " plt.title('Boudary Condition')\n", - " t_1 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n", - " x_1 = torch.ones(100, dtype=torch.float32)\n", - " u_1_target = torch.zeros(100, dtype=torch.float32)\n", - " u_1 = self.modely({'x': x_1.tolist(), 't': t_1.tolist()})\n", - " plt.plot(t_1.tolist(), u_1['U'], label=f'network x=1')\n", - " plt.plot(t_1.tolist(), u_1_target.tolist(), label=f'target x=1')\n", - " t_2 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n", - " x_2 = -torch.ones(100, dtype=torch.float32)\n", - " u_2_target = torch.zeros(100, dtype=torch.float32)\n", - " u_2 = self.modely({'x': x_2.tolist(), 't': t_2.tolist()})\n", - " plt.plot(t_2.tolist(), u_2['U'], label=f'network x=-1')\n", - " plt.plot(t_2.tolist(), u_2_target.tolist(), label=f'target x=-1')\n", - " plt.grid(True)\n", - " plt.legend(loc='best')\n", - " plt.xlabel('x[t]')\n", - " plt.ylabel('u')\n", - "\n", - " plt.figure()\n", - " plt.title('Function Integration')\n", - " t_3 = torch.linspace(0, 1, steps=100, dtype=torch.float32).numpy()\n", - " x_3 = torch.linspace(-1, 1, steps=100, dtype=torch.float32).numpy()\n", - " T, X = np.meshgrid(t_3, x_3)\n", - " u_2 = self.modely({'x': X.flatten().tolist(), 't': T.flatten().tolist()})\n", - " plt.contourf(T, X, np.array(u_2['U']).reshape(100,100))\n", - " plt.xlabel('t')\n", - " plt.ylabel('x')\n", - " plt.show()\n", - "\n", - "res = FunctionVisualizer()\n", - "res.setModely(pinn)\n", - "res.showResults()" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n", - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n", - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n", - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n", - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n", - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n", - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n", - "\u001B[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001B[0m\n" + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n", + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n", + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n", + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n", + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n", + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n", + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n", + "\u001b[33m[__call__] Inputs not provided: ['u_target', 'b']. Autofilling with zeros..\u001b[0m\n" ] }, { "data": { + "image/png": "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S7fNChYsqMAyP3PZqa99+/YqMTFRvffee6p8+fLqyy+/VLGxsapMmTIKUJ06dVLR0dGqbdu2qm7duur06dNq9uzZmm+/7NQIKF9fXxUXF5fpOJs2bVLHjx9XdevWVbVr11b79u1TW7du1by+f0//Pe3eQj+D2r9ReWXKrCEKDg5Od42M8uXLqz179qj4+Hh1+vRp1aJFi3Trv/rqq+r8+fMqNjZW7dq1S9NroTyY9Hq9mj59urpz5466ceOGmjRp0iNrrF+/vlJKmS+c9u/JYDCoadOmqbCwMHX//n21efNm5enpaVX1vfHGG+rEiRMqLi5OnT171vwL7sHk5+enQkJCVExMjFq3bp0qWrSo1dS3fft2lZl/Xzvl888/V1euXFHx8fFqz549qmrVqprU5ejoqJYvX65iYmJUaGioGjBggHnZfy+SWq9ePXX06FEVFxengoKCVK1atSx6m2WnvnPnzmW6zR5c18USP3PZ3X49e/ZUf//9t4qPj1dHjhxRTZo0STfWsGHD1PXr11VUVJRavHix5tfJepIa3377bRUeHp7pOIULF1ZLlixRN27cUHfv3lUrV65UhQsX1ry+f0//bYgs8TOo+98/hBBCCCHyLDmGSAghhBB5njREQgghhMjzpCESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIWyWUgqlFNu3bwegZs2aNGzYMMvPL1q0KL/88gv37t3jn3/+oWvXrgC4urqax162bFmuZBdCPFvSEAkhbFqHDh145513ANiwYQMVK1bM8nOXL19OoUKFaNiwIePHj2fx4sXUq1ePGzdu4Obmxo8//phbsYUQz5i91gGEECI33blzx3z3cJ1Ol+XnlStXjjZt2lC2bFlCQkL466+/aNiwIf3796d79+5ERkYSHx+fS6mFEM+a7CESQli1Hj16kJCQQPny5QGoVKkS8fHxvPHGG+nWCwgIoGzZsixfvpxly5bh5+dn/trrv1OZMmVo0KABV69eJSQkxDxGYGBgtr5yE0JYD9lDJISwakuXLuW9995j5syZvPHGGyxatIj169ezefPmdOt16NCBkydPMm3aNJYvX05SUhI7duzIdMybN2/i7u5OeHh4uvmRkZF4enrmWi1CCO1IQySEsHoffPABJ0+eZPXq1VSqVIkOHTpkWCcqKorU1FTu3r3LvXv3AEhISHjomE5OTiQmJqabl5iYiMFgyNnwQgiLIF+ZCSGs3sWLF5k8eTJdu3ZlyJAh3L59+7HP6dKlCzExMZlOpUqVIiEhIUPzYzAYiIuLy60yhBAakj1EQgibULNmTVJSUmjevDmrV69+7PqbN2/m0KFDmS4LDw8nLCwMNze3dPPd3NyIiIjIkbxCCMsiDZEQwuq98cYbtGzZktatW7N582ZWrVpFQEBAhvWUUuZ/379/n/v37z90zKCgIMqWLYuHhwdhYWEA+Pj4EBQUlPMFCCE0J1+ZCSGsWoECBfjmm28YP348O3fuZO7cuSxcuDDTY31iY2OpXLkyRYoUeey4wcHB7Nixg1WrVlG9enV69OhBly5dmDdvXm6UIYSwAEommWSSyVqnOXPmqL///ls5ODgoQBUoUECFhYWpCRMmKKWU8vX1Na/br18/FRMTo9atW5elsYsXL642bdqk4uLi1OXLl1Xnzp3TLV+2bJlatmyZ5u+BTDLJ9PST7n//EEIIm6OUomnTpuzduzdXxn9w247u3bvnyvhCiGdHvjITQti0okWLUrhw4RwdU6fT4erqiqOjY46OK4TQjjREQgibtn79etasWZOjY5YoUYLr16/TqVOnHB1XCKEd+cpMCCGEEHme7CESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIYQQIs+ThkgIIYQQeZ40REIIIYTI86QhEkIIIUSe938qBo+/palfkAAAAABJRU5ErkJggg==", "text/plain": [ "
" - ], - "image/png": "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S7fNChYsqMAyP3PZqa99+/YqMTFRvffee6p8+fLqyy+/VLGxsapMmTIKUJ06dVLR0dGqbdu2qm7duur06dNq9uzZmm+/7NQIKF9fXxUXF5fpOJs2bVLHjx9XdevWVbVr11b79u1TW7du1by+f0//Pe3eQj+D2r9ReWXKrCEKDg5Od42M8uXLqz179qj4+Hh1+vRp1aJFi3Trv/rqq+r8+fMqNjZW7dq1S9NroTyY9Hq9mj59urpz5466ceOGmjRp0iNrrF+/vlJKmS+c9u/JYDCoadOmqbCwMHX//n21efNm5enpaVX1vfHGG+rEiRMqLi5OnT171vwL7sHk5+enQkJCVExMjFq3bp0qWrSo1dS3fft2lZl/Xzvl888/V1euXFHx8fFqz549qmrVqprU5ejoqJYvX65iYmJUaGioGjBggHnZfy+SWq9ePXX06FEVFxengoKCVK1atSx6m2WnvnPnzmW6zR5c18USP3PZ3X49e/ZUf//9t4qPj1dHjhxRTZo0STfWsGHD1PXr11VUVJRavHix5tfJepIa3377bRUeHp7pOIULF1ZLlixRN27cUHfv3lUrV65UhQsX1ry+f0//bYgs8TOo+98/hBBCCCHyLDmGSAghhBB5njREQgghhMjzpCESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIWyWUgqlFNu3bwegZs2aNGzYMMvPL1q0KL/88gv37t3jn3/+oWvXrgC4urqax162bFmuZBdCPFvSEAkhbFqHDh145513ANiwYQMVK1bM8nOXL19OoUKFaNiwIePHj2fx4sXUq1ePGzdu4Obmxo8//phbsYUQz5i91gGEECI33blzx3z3cJ1Ol+XnlStXjjZt2lC2bFlCQkL466+/aNiwIf3796d79+5ERkYSHx+fS6mFEM+a7CESQli1Hj16kJCQQPny5QGoVKkS8fHxvPHGG+nWCwgIoGzZsixfvpxly5bh5+dn/trrv1OZMmVo0KABV69eJSQkxDxGYGBgtr5yE0JYD9lDJISwakuXLuW9995j5syZvPHGGyxatIj169ezefPmdOt16NCBkydPMm3aNJYvX05SUhI7duzIdMybN2/i7u5OeHh4uvmRkZF4enrmWi1CCO1IQySEsHoffPABJ0+eZPXq1VSqVIkOHTpkWCcqKorU1FTu3r3LvXv3AEhISHjomE5OTiQmJqabl5iYiMFgyNnwQgiLIF+ZCSGs3sWLF5k8eTJdu3ZlyJAh3L59+7HP6dKlCzExMZlOpUqVIiEhIUPzYzAYiIuLy60yhBAakj1EQgibULNmTVJSUmjevDmrV69+7PqbN2/m0KFDmS4LDw8nLCwMNze3dPPd3NyIiIjIkbxCCMsiDZEQwuq98cYbtGzZktatW7N582ZWrVpFQEBAhvWUUuZ/379/n/v37z90zKCgIMqWLYuHhwdhYWEA+Pj4EBQUlPMFCCE0J1+ZCSGsWoECBfjmm28YP348O3fuZO7cuSxcuDDTY31iY2OpXLkyRYoUeey4wcHB7Nixg1WrVlG9enV69OhBly5dmDdvXm6UIYSwAEommWSSyVqnOXPmqL///ls5ODgoQBUoUECFhYWpCRMmKKWU8vX1Na/br18/FRMTo9atW5elsYsXL642bdqk4uLi1OXLl1Xnzp3TLV+2bJlatmyZ5u+BTDLJ9PST7n//EEIIm6OUomnTpuzduzdXxn9w247u3bvnyvhCiGdHvjITQti0okWLUrhw4RwdU6fT4erqiqOjY46OK4TQjjREQgibtn79etasWZOjY5YoUYLr16/TqVOnHB1XCKEd+cpMCCGEEHme7CESQgghRJ4nDZEQQggh8jxpiIQQQgiR50lDJIQQQog8TxoiIYQQQuR50hAJIYQQIs+ThkgIIYQQeZ40REIIIYTI86QhEkIIIUSe938qBo+/palfkAAAAABJRU5ErkJggg==" + ] }, - "metadata": {}, - "output_type": "display_data", "jetTransient": { "display_id": null - } + }, + "metadata": {}, + "output_type": "display_data" }, { "data": { + "image/png": "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", "text/plain": [ "
" - ], - "image/png": "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" + ] }, - "metadata": {}, - "output_type": "display_data", "jetTransient": { "display_id": null - } + }, + "metadata": {}, + "output_type": "display_data" }, { "data": { + "image/png": "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", "text/plain": [ "
" - ], - "image/png": "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" + ] }, - "metadata": {}, - "output_type": "display_data", "jetTransient": { "display_id": null - } + }, + "metadata": {}, + "output_type": "display_data" }, { "data": { + "image/png": "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dddd57LO4XC4fHzy5EnnWDzh6XFiY2MVHh7uEiSna9OmjS6++GJJqvX1yM/Pr7V/XccZNWqUZs6cqUsvvVSFhYXavn17rXGeS8uWLev9WoaHh6t58+bOdd76WgJ2RTgBcDLGuMycSFKzZs08Ps7Ro0fVsWPHWuv79OmjoqIi7dq166yv//HHHyWdusH8TPHx8Zb57bujR4+qtLRUAwcOrHP7nj17FBsbK0lq3bq19u/f79xWc9P52fTr10+LFi3SvHnz9Nxzzyk3N1eSNGvWLP385z93e5w//vhjvV9LSSosLHT+M4Cz438jADiVlJS4/NaadOqym6c++ugjderUyeXZQpGRkXrnnXd07733Sjp1qag+33zzjfLy8vQ///M/Lus7duyoPn36aNOmTR6PyRvOHPOGDRsUExOjsLAwbdu2zblcc801mjJlii644AJt3rxZJ06cqPUMrdtvv/2cn69v376KiIjQ1KlTndEUHh6uG2+8UZIUFhZW57jOtGHDBt18880uERweHq677rpLW7du1fHjx8998gAkEU4ATrN8+XL1799fEydO1IABAzRnzhwNGjTI4+OkpaXp+++/17vvvqtRo0bppptu0jvvvKPGjRvrb3/7m6RTszXXXnutbrjhhlr36xhj9Oc//1lDhw7VG2+8oaFDh2r06NH68MMP9eOPP2rOnDleOV9PHT16VK1bt9bQoUMVFxenlStXasOGDVq2bJkeeOAB9e/fX48++qhSU1NVXV2twsJC7d27V//4xz80c+ZMPfLII7rxxhv18ssva8SIEZJOXQqrz9atWyVJf/3rXzVgwADdfvvt+vDDD9WtWzdJP91Tdua4zjRt2jRFRUVp3bp1GjlypEaMGKHVq1erc+fOeuKJJ7z9ZQJsjXAC4PT0009rwYIFevTRR/Xuu+8qPj7eOUPkiWPHjumGG25QZmam/vrXv+rtt99WRESEBgwYoH379kmSnnvuOcXFxen9999Xjx49ah0jIyNDI0eO1BVXXKGlS5dqzpw52rJli3r16lXn/UH+kJaWpn379mnZsmXO504NHz5cb731lp544gm9//77euCBBzRnzhzdddddztc99NBDmj9/vh555BEtW7ZM7dq104wZMyTVfV9TjQ0bNujBBx9U3759tWrVKs2ZM0c//PCDc7aq5nLdmeM6086dO5WcnKzDhw8r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", "text/plain": [ "
" - ], - "image/png": "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dddd57LO4XC4fHzy5EnnWDzh6XFiY2MVHh7uEiSna9OmjS6++GJJqvX1yM/Pr7V/XccZNWqUZs6cqUsvvVSFhYXavn17rXGeS8uWLev9WoaHh6t58+bOdd76WgJ2RTgBcDLGuMycSFKzZs08Ps7Ro0fVsWPHWuv79OmjoqIi7dq166yv//HHHyWdusH8TPHx8Zb57bujR4+qtLRUAwcOrHP7nj17FBsbK0lq3bq19u/f79xWc9P52fTr10+LFi3SvHnz9Nxzzyk3N1eSNGvWLP385z93e5w//vhjvV9LSSosLHT+M4Cz438jADiVlJS4/NaadOqym6c++ugjderUyeXZQpGRkXrnnXd07733Sjp1qag+33zzjfLy8vQ///M/Lus7duyoPn36aNOmTR6PyRvOHPOGDRsUExOjsLAwbdu2zblcc801mjJlii644AJt3rxZJ06cqPUMrdtvv/2cn69v376KiIjQ1KlTndEUHh6uG2+8UZIUFhZW57jOtGHDBt18880uERweHq677rpLW7du1fHjx8998gAkEU4ATrN8+XL1799fEydO1IABAzRnzhwNGjTI4+OkpaXp+++/17vvvqtRo0bppptu0jvvvKPGjRvrb3/7m6RTszXXXnutbrjhhlr36xhj9Oc//1lDhw7VG2+8oaFDh2r06NH68MMP9eOPP2rOnDleOV9PHT16VK1bt9bQoUMVFxenlStXasOGDVq2bJkeeOAB9e/fX48++qhSU1NVXV2twsJC7d27V//4xz80c+ZMPfLII7rxxhv18ssva8SIEZJOXQqrz9atWyVJf/3rXzVgwADdfvvt+vDDD9WtWzdJP91Tdua4zjRt2jRFRUVp3bp1GjlypEaMGKHVq1erc+fOeuKJJ7z9ZQJsjXAC4PT0009rwYIFevTRR/Xuu+8qPj7eOUPkiWPHjumGG25QZmam/vrXv+rtt99WRESEBgwYoH379kmSnnvuOcXFxen9999Xjx49ah0jIyNDI0eO1BVXXKGlS5dqzpw52rJli3r16lXn/UH+kJaWpn379mnZsmXO504NHz5cb731lp544gm9//77euCBBzRnzhzdddddztc99NBDmj9/vh555BEtW7ZM7dq104wZMyTVfV9TjQ0bNujBBx9U3759tWrVKs2ZM0c//PCDc7aq5nLdmeM6086dO5WcnKzDhw8r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" + ] }, - "metadata": {}, - "output_type": "display_data", "jetTransient": { "display_id": null - } + }, + "metadata": {}, + "output_type": "display_data" } ], - "execution_count": 7 + "source": [ + "# Custom visualizer for results\n", + "class FunctionVisualizer(TextVisualizer):\n", + " def showResults(self):\n", + " import matplotlib.pyplot as plt\n", + "\n", + " plt.figure()\n", + " plt.title(\"Initial Condition\")\n", + " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", + " t = torch.zeros(100, dtype=torch.float32)\n", + " u_target = -torch.sin(torch.pi * x)\n", + " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", + " plt.plot(x.tolist(), u[\"U\"], label=f\"network\")\n", + " plt.plot(x.tolist(), u_target.tolist(), label=f\"target\")\n", + " plt.grid(True)\n", + " plt.legend(loc=\"best\")\n", + " plt.xlabel(\"x[t=0]\")\n", + " plt.ylabel(\"u\")\n", + "\n", + " plt.figure()\n", + " plt.title(\"U value at t=[0.25,0.5,0.75,1]\")\n", + " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", + " t = torch.ones(100, dtype=torch.float32) * 0.25\n", + " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", + " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=0.25\")\n", + " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", + " t = torch.ones(100, dtype=torch.float32) * 0.5\n", + " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", + " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=0.5\")\n", + " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", + " t = torch.ones(100, dtype=torch.float32) * 0.75\n", + " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", + " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=0.75\")\n", + " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", + " t = torch.ones(100, dtype=torch.float32)\n", + " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", + " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=1\")\n", + " plt.grid(True)\n", + " plt.legend(loc=\"best\")\n", + " plt.xlabel(\"x\")\n", + " plt.ylabel(\"u\")\n", + "\n", + " plt.figure()\n", + " plt.title(\"Boudary Condition\")\n", + " t_1 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n", + " x_1 = torch.ones(100, dtype=torch.float32)\n", + " u_1_target = torch.zeros(100, dtype=torch.float32)\n", + " u_1 = self.modely({\"x\": x_1.tolist(), \"t\": t_1.tolist()})\n", + " plt.plot(t_1.tolist(), u_1[\"U\"], label=f\"network x=1\")\n", + " plt.plot(t_1.tolist(), u_1_target.tolist(), label=f\"target x=1\")\n", + " t_2 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n", + " x_2 = -torch.ones(100, dtype=torch.float32)\n", + " u_2_target = torch.zeros(100, dtype=torch.float32)\n", + " u_2 = self.modely({\"x\": x_2.tolist(), \"t\": t_2.tolist()})\n", + " plt.plot(t_2.tolist(), u_2[\"U\"], label=f\"network x=-1\")\n", + " plt.plot(t_2.tolist(), u_2_target.tolist(), label=f\"target x=-1\")\n", + " plt.grid(True)\n", + " plt.legend(loc=\"best\")\n", + " plt.xlabel(\"x[t]\")\n", + " plt.ylabel(\"u\")\n", + "\n", + " plt.figure()\n", + " plt.title(\"Function Integration\")\n", + " t_3 = torch.linspace(0, 1, steps=100, dtype=torch.float32).numpy()\n", + " x_3 = torch.linspace(-1, 1, steps=100, dtype=torch.float32).numpy()\n", + " T, X = np.meshgrid(t_3, x_3)\n", + " u_2 = self.modely({\"x\": X.flatten().tolist(), \"t\": T.flatten().tolist()})\n", + " plt.contourf(T, X, np.array(u_2[\"U\"]).reshape(100, 100))\n", + " plt.xlabel(\"t\")\n", + " plt.ylabel(\"x\")\n", + " plt.show()\n", + "\n", + "\n", + "res = FunctionVisualizer()\n", + "res.setModely(pinn)\n", + "res.showResults()" + ] }, { "cell_type": "code", + "execution_count": 7, "id": "938ccccb00979139", "metadata": { "ExecuteTime": { @@ -1941,9 +1959,8 @@ "start_time": "2026-02-25T17:50:52.695142Z" } }, - "source": [], "outputs": [], - "execution_count": 7 + "source": [] } ], "metadata": { diff --git a/docs/_autodoc/tutorials/examples/dataset.ipynb b/docs/_autodoc/tutorials/examples/dataset.ipynb index 12dbb739..675003d7 100644 --- a/docs/_autodoc/tutorials/examples/dataset.ipynb +++ b/docs/_autodoc/tutorials/examples/dataset.ipynb @@ -99,13 +99,13 @@ } ], "source": [ - "in1 = Input('in1')\n", - "target = Input('target')\n", + "in1 = Input(\"in1\")\n", + "target = Input(\"target\")\n", "relation = Fir(in1.tw(0.05))\n", - "output = Output('out', relation)\n", + "output = Output(\"out\", relation)\n", "\n", "model = Modely(visualizer=TextVisualizer())\n", - "model.addMinimize('out', output, target.last())\n", + "model.addMinimize(\"out\", output, target.last())\n", "model.neuralizeModel(0.01)" ] }, @@ -145,9 +145,9 @@ } ], "source": [ - "train_folder = 'data'\n", - "data_struct = ['in1', '', 'target']\n", - "model.loadData(name='dataset', source=train_folder, format=data_struct)" + "train_folder = \"data\"\n", + "data_struct = [\"in1\", \"\", \"target\"]\n", + "model.loadData(name=\"dataset\", source=train_folder, format=data_struct)" ] }, { @@ -182,7 +182,14 @@ } ], "source": [ - "model.loadData(name='dataset_2', source=train_folder, format=data_struct, skiplines=4, delimiter='\\t', header=None)" + "model.loadData(\n", + " name=\"dataset_2\",\n", + " source=train_folder,\n", + " format=data_struct,\n", + " skiplines=4,\n", + " delimiter=\"\\t\",\n", + " header=None,\n", + ")" ] }, { @@ -220,12 +227,13 @@ ], "source": [ "import numpy as np\n", + "\n", "data_x = np.array(range(10))\n", "data_a = 2\n", "data_b = -3\n", - "dataset = {'in1': data_x, 'target': (data_a*data_x) + data_b}\n", + "dataset = {\"in1\": data_x, \"target\": (data_a * data_x) + data_b}\n", "\n", - "model.loadData(name='dataset_3', source=dataset)" + "model.loadData(name=\"dataset_3\", source=dataset)" ] }, { @@ -263,12 +271,16 @@ ], "source": [ "import pandas as pd\n", + "\n", "# Create a DataFrame with random values for each input\n", - "df = pd.DataFrame({\n", - " 'in1': np.linspace(1,100,100, dtype=np.float32),\n", - " 'target': np.linspace(1,100,100, dtype=np.float32)})\n", + "df = pd.DataFrame(\n", + " {\n", + " \"in1\": np.linspace(1, 100, 100, dtype=np.float32),\n", + " \"target\": np.linspace(1, 100, 100, dtype=np.float32),\n", + " }\n", + ")\n", "\n", - "model.loadData(name='dataset_4', source=df)" + "model.loadData(name=\"dataset_4\", source=df)" ] }, { @@ -305,12 +317,17 @@ } ], "source": [ - "df = pd.DataFrame({\n", - " 'time': np.array([1.0,1.5,2.0,4.0,4.5,5.0,7.0,7.5,8.0,8.5], dtype=np.float32),\n", - " 'in1': np.linspace(1,10,10, dtype=np.float32),\n", - " 'target': np.linspace(1,10,10, dtype=np.float32)})\n", + "df = pd.DataFrame(\n", + " {\n", + " \"time\": np.array(\n", + " [1.0, 1.5, 2.0, 4.0, 4.5, 5.0, 7.0, 7.5, 8.0, 8.5], dtype=np.float32\n", + " ),\n", + " \"in1\": np.linspace(1, 10, 10, dtype=np.float32),\n", + " \"target\": np.linspace(1, 10, 10, dtype=np.float32),\n", + " }\n", + ")\n", "\n", - "model.loadData(name='dataset_resampled', source=df, resampling=True)" + "model.loadData(name=\"dataset_resampled\", source=df, resampling=True)" ] }, { @@ -348,7 +365,7 @@ } ], "source": [ - "sample = model.getSamples(dataset='dataset_4', window=5)\n", + "sample = model.getSamples(dataset=\"dataset_4\", window=5)\n", "model(sample, sampled=True)" ] } diff --git a/docs/_autodoc/tutorials/examples/equation_learner.ipynb b/docs/_autodoc/tutorials/examples/equation_learner.ipynb index f2e52ea4..2ce52bfa 100644 --- a/docs/_autodoc/tutorials/examples/equation_learner.ipynb +++ b/docs/_autodoc/tutorials/examples/equation_learner.ipynb @@ -109,9 +109,9 @@ } ], "source": [ - "x = Input('x')\n", + "x = Input(\"x\")\n", "equation_learner = EquationLearner(functions=[Tan, Sin, Cos])\n", - "Output('out',equation_learner(x.last()))" + "Output(\"out\", equation_learner(x.last()))" ] }, { @@ -171,10 +171,10 @@ } ], "source": [ - "x = Input('x')\n", + "x = Input(\"x\")\n", "input_layer = Linear(output_dimension=3)\n", "equation_learner = EquationLearner(functions=[Tan, Sin, Cos], linear_in=input_layer)\n", - "Output('out', equation_learner(x.last()))" + "Output(\"out\", equation_learner(x.last()))" ] }, { @@ -235,11 +235,13 @@ } ], "source": [ - "x = Input('x')\n", + "x = Input(\"x\")\n", "input_layer = Linear(output_dimension=3)\n", "output_layer = Linear(output_dimension=1)\n", - "equation_learner = EquationLearner(functions=[Tan, Sin, Cos], linear_in=input_layer, linear_out=output_layer)\n", - "Output('out', equation_learner(x.last()))" + "equation_learner = EquationLearner(\n", + " functions=[Tan, Sin, Cos], linear_in=input_layer, linear_out=output_layer\n", + ")\n", + "Output(\"out\", equation_learner(x.last()))" ] }, { @@ -307,10 +309,10 @@ } ], "source": [ - "x = Input('x')\n", - "F = Input('F')\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", "equation_learner = EquationLearner(functions=[Tan, Sin, Cos])\n", - "Output('out',equation_learner(inputs=(x.last(),F.last())))" + "Output(\"out\", equation_learner(inputs=(x.last(), F.last())))" ] }, { @@ -382,12 +384,14 @@ } ], "source": [ - "x = Input('x')\n", - "F = Input('F')\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", "\n", "linear_layer_in_1 = Linear(output_dimension=7)\n", - "equation_learner_1 = EquationLearner(functions=[Tan, Add, Sin, Mul, Identity], linear_in=linear_layer_in_1)\n", - "Output('out',equation_learner_1(x.last()))" + "equation_learner_1 = EquationLearner(\n", + " functions=[Tan, Add, Sin, Mul, Identity], linear_in=linear_layer_in_1\n", + ")\n", + "Output(\"out\", equation_learner_1(x.last()))" ] }, { @@ -461,17 +465,20 @@ "source": [ "import torch\n", "\n", + "\n", "def func1(K1):\n", " return torch.sin(K1)\n", "\n", + "\n", "def func2(K2):\n", " return torch.cos(K2)\n", "\n", - "x = Input('x')\n", + "\n", + "x = Input(\"x\")\n", "parfun1 = ParamFun(func1)\n", "parfun2 = ParamFun(func2)\n", "equation_learner = EquationLearner([parfun1, parfun2])\n", - "Output('out',equation_learner(x.last()))" + "Output(\"out\", equation_learner(x.last()))" ] }, { @@ -557,18 +564,19 @@ } ], "source": [ - "def myFun(K1,K2,p1,p2):\n", - " return K1*p1+K2*p2\n", + "def myFun(K1, K2, p1, p2):\n", + " return K1 * p1 + K2 * p2\n", + "\n", "\n", - "x = Input('x')\n", - "F = Input('F')\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", "\n", - "K1 = Parameter('k1', dimensions = 1, sw = 1, values=[[2.0]])\n", - "K2 = Parameter('k2', dimensions = 1, sw = 1, values=[[3.0]])\n", - "parfun = ParamFun(myFun, parameters_and_constants=[K1,K2])\n", + "K1 = Parameter(\"k1\", dimensions=1, sw=1, values=[[2.0]])\n", + "K2 = Parameter(\"k2\", dimensions=1, sw=1, values=[[3.0]])\n", + "parfun = ParamFun(myFun, parameters_and_constants=[K1, K2])\n", "\n", "equation_learner = EquationLearner([parfun, Sin, Add])\n", - "Output('out',equation_learner((x.last(),F.last())))" + "Output(\"out\", equation_learner((x.last(), F.last())))" ] }, { @@ -645,18 +653,19 @@ } ], "source": [ - "def myFun(K1,p1):\n", - " return K1*p1\n", + "def myFun(K1, p1):\n", + " return K1 * p1\n", "\n", - "x = Input('x')\n", - "F = Input('F')\n", "\n", - "K = Parameter('k', dimensions = 1, sw = 1,values=[[2.0]])\n", - "parfun = ParamFun(myFun, parameters_and_constants = [K])\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", "\n", - "fuzzi = Fuzzify(centers=[0,1,2,3])\n", + "K = Parameter(\"k\", dimensions=1, sw=1, values=[[2.0]])\n", + "parfun = ParamFun(myFun, parameters_and_constants=[K])\n", + "\n", + "fuzzi = Fuzzify(centers=[0, 1, 2, 3])\n", "equation_learner = EquationLearner([parfun, fuzzi])\n", - "Output('out',equation_learner((x.last(),F.last())))" + "Output(\"out\", equation_learner((x.last(), F.last())))" ] }, { @@ -779,23 +788,43 @@ } ], "source": [ - "x = Input('x')\n", - "F = Input('F')\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", + "\n", + "\n", + "def myFun(K1, K2, p1, p2):\n", + " return K1 * p1 + K2 * p2\n", "\n", - "def myFun(K1,K2,p1,p2):\n", - " return K1*p1+K2*p2\n", "\n", - "K1 = Parameter('k1', dimensions = 1, sw = 1, values=[[2.0]])\n", - "K2 = Parameter('k2', dimensions = 1, sw = 1, values=[[3.0]])\n", - "parfun = ParamFun(myFun, parameters_and_constants = [K1,K2])\n", + "K1 = Parameter(\"k1\", dimensions=1, sw=1, values=[[2.0]])\n", + "K2 = Parameter(\"k2\", dimensions=1, sw=1, values=[[3.0]])\n", + "parfun = ParamFun(myFun, parameters_and_constants=[K1, K2])\n", "\n", - "input_layer_1 = Linear(output_dimension=5, W_init='init_constant', W_init_params={'value':1}, b_init='init_constant', b_init_params={'value':0})\n", - "input_layer_2 = Linear(output_dimension=7, W_init='init_constant', W_init_params={'value':1}, b_init='init_constant', b_init_params={'value':0})\n", - "output_layer = Linear(output_dimension=1, W_init='init_constant', W_init_params={'value':1}, b=True)\n", + "input_layer_1 = Linear(\n", + " output_dimension=5,\n", + " W_init=\"init_constant\",\n", + " W_init_params={\"value\": 1},\n", + " b_init=\"init_constant\",\n", + " b_init_params={\"value\": 0},\n", + ")\n", + "input_layer_2 = Linear(\n", + " output_dimension=7,\n", + " W_init=\"init_constant\",\n", + " W_init_params={\"value\": 1},\n", + " b_init=\"init_constant\",\n", + " b_init_params={\"value\": 0},\n", + ")\n", + "output_layer = Linear(\n", + " output_dimension=1, W_init=\"init_constant\", W_init_params={\"value\": 1}, b=True\n", + ")\n", "equation_learner = EquationLearner([parfun, Sin, Add], linear_in=input_layer_1)\n", - "equation_learner_2 = EquationLearner(functions=[Tan, Add, Sin, Mul, Identity], linear_in=input_layer_2, linear_out=output_layer)\n", + "equation_learner_2 = EquationLearner(\n", + " functions=[Tan, Add, Sin, Mul, Identity],\n", + " linear_in=input_layer_2,\n", + " linear_out=output_layer,\n", + ")\n", "\n", - "Output('out',equation_learner_2(equation_learner((x.sw(1),F.sw(1)))))" + "Output(\"out\", equation_learner_2(equation_learner((x.sw(1), F.sw(1)))))" ] } ], diff --git a/docs/_autodoc/tutorials/examples/export.ipynb b/docs/_autodoc/tutorials/examples/export.ipynb index 6cb1b98e..d2ee24ba 100644 --- a/docs/_autodoc/tutorials/examples/export.ipynb +++ b/docs/_autodoc/tutorials/examples/export.ipynb @@ -86,22 +86,24 @@ } ], "source": [ - "x = Input('x')\n", - "y = Input('y')\n", - "z = Input('z')\n", + "x = Input(\"x\")\n", + "y = Input(\"y\")\n", + "z = Input(\"z\")\n", "\n", - "def myFun(K1,p1,p2):\n", - " return K1*p1*p2\n", "\n", - "K_x = Parameter('k_x', dimensions=1, tw=1)\n", - "c_v = Constant('c_v', tw=1, values=[[1],[2]])\n", - "parfun = ParamFun(myFun, parameters_and_constants = [K_x,c_v])\n", + "def myFun(K1, p1, p2):\n", + " return K1 * p1 * p2\n", "\n", - "out = Output('out', Fir(parfun(x.tw(1)))+Fir(parfun(y.tw(1)))+Fir(parfun(z.tw(1))))\n", "\n", - "result_path = './results'\n", + "K_x = Parameter(\"k_x\", dimensions=1, tw=1)\n", + "c_v = Constant(\"c_v\", tw=1, values=[[1], [2]])\n", + "parfun = ParamFun(myFun, parameters_and_constants=[K_x, c_v])\n", + "\n", + "out = Output(\"out\", Fir(parfun(x.tw(1))) + Fir(parfun(y.tw(1))) + Fir(parfun(z.tw(1))))\n", + "\n", + "result_path = \"./results\"\n", "model = Modely(workspace=result_path)\n", - "model.addModel('model', out)" + "model.addModel(\"model\", out)" ] }, { @@ -163,7 +165,7 @@ } ], "source": [ - "model.saveModel(name='model_definition', model_folder=result_path)" + "model.saveModel(name=\"model_definition\", model_folder=result_path)" ] }, { @@ -244,7 +246,7 @@ ], "source": [ "model.neuralizeModel(0.5)\n", - "model.saveTorchModel(name='model_parameters', model_folder=result_path)" + "model.saveTorchModel(name=\"model_parameters\", model_folder=result_path)" ] }, { @@ -275,7 +277,7 @@ } ], "source": [ - "model.loadTorchModel(name='model_parameters', model_folder=result_path)" + "model.loadTorchModel(name=\"model_parameters\", model_folder=result_path)" ] }, { @@ -311,7 +313,7 @@ } ], "source": [ - "model.exportPythonModel(name='pytorch_model', model_folder=result_path)" + "model.exportPythonModel(name=\"pytorch_model\", model_folder=result_path)" ] }, { @@ -394,7 +396,7 @@ } ], "source": [ - "model.importPythonModel(name='pytorch_model', model_folder=result_path)" + "model.importPythonModel(name=\"pytorch_model\", model_folder=result_path)" ] }, { @@ -485,7 +487,7 @@ ], "source": [ "model.neuralizeModel(0.5)\n", - "model.exportONNX(name='model_onnx', model_folder=result_path)" + "model.exportONNX(name=\"model_onnx\", model_folder=result_path)" ] }, { @@ -522,7 +524,7 @@ } ], "source": [ - "model.exportONNX(inputs_order=['x','y','z'], outputs_order=['out'])" + "model.exportONNX(inputs_order=[\"x\", \"y\", \"z\"], outputs_order=[\"out\"])" ] }, { @@ -558,12 +560,20 @@ "import numpy as np\n", "import os\n", "\n", - "val = np.random.rand(1,2,1).astype(np.float32)\n", + "val = np.random.rand(1, 2, 1).astype(np.float32)\n", "\n", - "data = {'x':val, 'y':val, 'z':val}\n", - "output_onnx = Modely().onnxInference(data, name = 'net', model_folder = os.path.join(result_path, 'onnx'))\n", - "output = model({'x':val.squeeze(-1).tolist()[0], 'y':val.squeeze(-1).tolist()[0], 'z':val.squeeze(-1).tolist()[0]})\n", - "print(f'model out : {output} | onnx out : {output_onnx}')" + "data = {\"x\": val, \"y\": val, \"z\": val}\n", + "output_onnx = Modely().onnxInference(\n", + " data, name=\"net\", model_folder=os.path.join(result_path, \"onnx\")\n", + ")\n", + "output = model(\n", + " {\n", + " \"x\": val.squeeze(-1).tolist()[0],\n", + " \"y\": val.squeeze(-1).tolist()[0],\n", + " \"z\": val.squeeze(-1).tolist()[0],\n", + " }\n", + ")\n", + "print(f\"model out : {output} | onnx out : {output_onnx}\")" ] }, { @@ -596,7 +606,7 @@ } ], "source": [ - "model.exportReport(name='model_report', model_folder=result_path)" + "model.exportReport(name=\"model_report\", model_folder=result_path)" ] }, { @@ -939,38 +949,50 @@ ], "source": [ "clearNames()\n", - "vehicle = nnodely(visualizer=TextVisualizer(), seed=2, workspace='results')\n", + "vehicle = nnodely(visualizer=TextVisualizer(), seed=2, workspace=\"results\")\n", "\n", "# Dimensions of the layers\n", - "n = 25\n", + "n = 25\n", "na = 21\n", "\n", - "#Create neural model inputs\n", - "velocity = Input('vel')\n", - "brake = Input('brk')\n", - "gear = Input('gear')\n", - "torque = Input('trq')\n", - "altitude = Input('alt',dimensions=na)\n", - "acc = Input('acc')\n", + "# Create neural model inputs\n", + "velocity = Input(\"vel\")\n", + "brake = Input(\"brk\")\n", + "gear = Input(\"gear\")\n", + "torque = Input(\"trq\")\n", + "altitude = Input(\"alt\", dimensions=na)\n", + "acc = Input(\"acc\")\n", "\n", "# Create neural network relations\n", - "air_drag_force = Linear(b=True)(velocity.last()**2)\n", - "breaking_force = -Relu(Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n)))\n", - "gravity_force = Linear(W_init = 'init_constant', W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last())\n", - "fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last())\n", - "local_model = LocalModel(input_function=lambda: Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3}))\n", + "air_drag_force = Linear(b=True)(velocity.last() ** 2)\n", + "breaking_force = -Relu(\n", + " Fir(\n", + " W_init=\"init_negexp\",\n", + " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n", + " )(brake.sw(n))\n", + ")\n", + "gravity_force = Linear(\n", + " W_init=\"init_constant\", W_init_params={\"value\": 0}, dropout=0.1, W=\"gravity\"\n", + ")(altitude.last())\n", + "fuzzi_gear = Fuzzify(6, range=[2, 7], functions=\"Rectangular\")(gear.last())\n", + "local_model = LocalModel(\n", + " input_function=lambda: Fir(\n", + " W_init=\"init_negexp\",\n", + " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n", + " )\n", + ")\n", "engine_force = local_model(torque.sw(n), fuzzi_gear)\n", "\n", "# Create neural network output\n", - "out = Output('acc_hat', air_drag_force+breaking_force+gravity_force+engine_force)\n", + "out = Output(\"acc_hat\", air_drag_force + breaking_force + gravity_force + engine_force)\n", "\n", "# Add the neural model to the nnodely structure and neuralization of the model\n", - "vehicle.addModel('vehicle',[out])\n", - "vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse')\n", + "vehicle.addModel(\"vehicle\", [out])\n", + "vehicle.addMinimize(\"acc_error\", acc.last(), out, loss_function=\"rmse\")\n", "vehicle.neuralizeModel(0.05)\n", "\n", "## Export the Onnx Model\n", - "vehicle.exportONNX(['vel','brk','gear','trq','alt'],['acc_hat'],models='vehicle')" + "vehicle.exportONNX([\"vel\", \"brk\", \"gear\", \"trq\", \"alt\"], [\"acc_hat\"], models=\"vehicle\")" ] }, { @@ -1328,44 +1350,62 @@ ], "source": [ "clearNames()\n", - "vehicle = nnodely(visualizer=MPLVisualizer(),seed=2, workspace=os.path.join(os.getcwd(), 'results'))\n", + "vehicle = nnodely(\n", + " visualizer=MPLVisualizer(), seed=2, workspace=os.path.join(os.getcwd(), \"results\")\n", + ")\n", "# Dimensions of the layers\n", - "n = 25\n", + "n = 25\n", "na = 21\n", "\n", - "#Create neural model inputs\n", - "velocity = Input('vel')\n", - "brake = Input('brk')\n", - "gear = Input('gear')\n", - "torque = Input('trq')\n", - "altitude = Input('alt',dimensions=na)\n", - "acc = Input('acc')\n", + "# Create neural model inputs\n", + "velocity = Input(\"vel\")\n", + "brake = Input(\"brk\")\n", + "gear = Input(\"gear\")\n", + "torque = Input(\"trq\")\n", + "altitude = Input(\"alt\", dimensions=na)\n", + "acc = Input(\"acc\")\n", "\n", "# Create neural network relations\n", - "air_drag_force = Linear(b=True)(velocity.last()**2)\n", - "breaking_force = -Relu(Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n)))\n", - "gravity_force = Linear(W_init = 'init_constant', W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last())\n", - "fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last())\n", - "local_model = LocalModel(input_function=lambda: Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3}))\n", + "air_drag_force = Linear(b=True)(velocity.last() ** 2)\n", + "breaking_force = -Relu(\n", + " Fir(\n", + " W_init=\"init_negexp\",\n", + " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n", + " )(brake.sw(n))\n", + ")\n", + "gravity_force = Linear(\n", + " W_init=\"init_constant\", W_init_params={\"value\": 0}, dropout=0.1, W=\"gravity\"\n", + ")(altitude.last())\n", + "fuzzi_gear = Fuzzify(6, range=[2, 7], functions=\"Rectangular\")(gear.last())\n", + "local_model = LocalModel(\n", + " input_function=lambda: Fir(\n", + " W_init=\"init_negexp\",\n", + " W_init_params={\"size_index\": 0, \"first_value\": 0.002, \"lambda\": 3},\n", + " )\n", + ")\n", "engine_force = local_model(torque.sw(n), fuzzi_gear)\n", "\n", - "acc_hat = air_drag_force+breaking_force+gravity_force+engine_force\n", - "vel_hat = acc_hat.s(-1,int_name='vel_init')\n", + "acc_hat = air_drag_force + breaking_force + gravity_force + engine_force\n", + "vel_hat = acc_hat.s(-1, int_name=\"vel_init\")\n", "\n", "# Closing the loop\n", "vel_hat.closedLoop(velocity)\n", "\n", "# Create neural network output\n", - "out1 = Output('acc_hat', acc_hat)\n", - "out2 = Output('vel_hat', vel_hat)\n", + "out1 = Output(\"acc_hat\", acc_hat)\n", + "out2 = Output(\"vel_hat\", vel_hat)\n", "\n", "# Add the neural model to the nnodely structure and neuralization of the model\n", - "vehicle.addModel('vehicle',[out1,out2])\n", - "vehicle.addMinimize('acc_error', acc.last(), out1, loss_function='rmse')\n", + "vehicle.addModel(\"vehicle\", [out1, out2])\n", + "vehicle.addMinimize(\"acc_error\", acc.last(), out1, loss_function=\"rmse\")\n", "vehicle.neuralizeModel(0.05)\n", "\n", "## Export the Onnx Model\n", - "vehicle.exportONNX(['brk','gear','trq','alt','vel','vel_init'],['acc_hat','vel_hat'],models='vehicle')" + "vehicle.exportONNX(\n", + " [\"brk\", \"gear\", \"trq\", \"alt\", \"vel\", \"vel_init\"],\n", + " [\"acc_hat\", \"vel_hat\"],\n", + " models=\"vehicle\",\n", + ")" ] }, { @@ -1388,11 +1428,27 @@ ], "source": [ "## Make inference using the onnx model\n", - "data = {'vel_init':np.random.rand(1,1,1).astype(np.float32),'vel':np.random.rand(1,1,1).astype(np.float32), 'brk':np.random.rand(1,1,25,1).astype(np.float32), 'gear':np.random.rand(1,1,1,1).astype(np.float32), 'trq':np.random.rand(1,1,25,1).astype(np.float32), 'alt':np.random.rand(1,1,1,21).astype(np.float32)}\n", - "output_onnx = Modely().onnxInference(data,'net_vehicle','results/onnx')\n", + "data = {\n", + " \"vel_init\": np.random.rand(1, 1, 1).astype(np.float32),\n", + " \"vel\": np.random.rand(1, 1, 1).astype(np.float32),\n", + " \"brk\": np.random.rand(1, 1, 25, 1).astype(np.float32),\n", + " \"gear\": np.random.rand(1, 1, 1, 1).astype(np.float32),\n", + " \"trq\": np.random.rand(1, 1, 25, 1).astype(np.float32),\n", + " \"alt\": np.random.rand(1, 1, 1, 21).astype(np.float32),\n", + "}\n", + "output_onnx = Modely().onnxInference(data, \"net_vehicle\", \"results/onnx\")\n", "\n", - "output = vehicle({'vel_init':data['vel_init'].squeeze(-1).tolist()[0], 'vel':data['vel'].squeeze(-1).tolist()[0], 'brk':data['brk'].squeeze(-1).tolist()[0][0], 'gear':data['gear'].squeeze(-1).tolist()[0], 'trq':data['trq'].squeeze(-1).tolist()[0][0], 'alt':data['alt'].squeeze(1).tolist()[0]})\n", - "print(f'model out : {output} | onnx out : {output_onnx}')" + "output = vehicle(\n", + " {\n", + " \"vel_init\": data[\"vel_init\"].squeeze(-1).tolist()[0],\n", + " \"vel\": data[\"vel\"].squeeze(-1).tolist()[0],\n", + " \"brk\": data[\"brk\"].squeeze(-1).tolist()[0][0],\n", + " \"gear\": data[\"gear\"].squeeze(-1).tolist()[0],\n", + " \"trq\": data[\"trq\"].squeeze(-1).tolist()[0][0],\n", + " \"alt\": data[\"alt\"].squeeze(1).tolist()[0],\n", + " }\n", + ")\n", + "print(f\"model out : {output} | onnx out : {output_onnx}\")" ] } ], diff --git a/docs/_autodoc/tutorials/examples/fir.ipynb b/docs/_autodoc/tutorials/examples/fir.ipynb index b832c001..0b2397e6 100644 --- a/docs/_autodoc/tutorials/examples/fir.ipynb +++ b/docs/_autodoc/tutorials/examples/fir.ipynb @@ -74,11 +74,11 @@ "metadata": {}, "outputs": [], "source": [ - "input = Input('x')\n", + "input = Input(\"x\")\n", "fir_layer = Fir(b=True)\n", - "output = Output('out', fir_layer(input.tw(0.05)))\n", + "output = Output(\"out\", fir_layer(input.tw(0.05)))\n", "# Inplace creation with default initialization\n", - "output = Output('out2', Fir(input.tw(0.05)))" + "output = Output(\"out2\", Fir(input.tw(0.05)))" ] }, { @@ -105,9 +105,9 @@ } ], "source": [ - "input = Input('x')\n", - "fir_layer = Fir(b='b', W='w')\n", - "output = Output('out', fir_layer(input.tw(0.05)))" + "input = Input(\"x\")\n", + "fir_layer = Fir(b=\"b\", W=\"w\")\n", + "output = Output(\"out\", fir_layer(input.tw(0.05)))" ] }, { @@ -136,13 +136,13 @@ } ], "source": [ - "input = Input('x')\n", + "input = Input(\"x\")\n", "\n", - "weight = Parameter('w', dimensions=3, sw=2, init='init_constant')\n", - "bias = Parameter('b', dimensions=3, init='init_constant')\n", + "weight = Parameter(\"w\", dimensions=3, sw=2, init=\"init_constant\")\n", + "bias = Parameter(\"b\", dimensions=3, init=\"init_constant\")\n", "\n", "fir_layer = Fir(W=weight, b=bias)(input.sw(2))\n", - "output = Output('out', fir_layer)" + "output = Output(\"out\", fir_layer)" ] }, { @@ -173,13 +173,18 @@ } ], "source": [ - "x = Input('x')\n", - "F = Input('F')\n", - "\n", - "fir_x = Fir(W_init=init_negexp, b_init=init_exp)(x.tw(0.2)) \n", - "fir_F = Fir(W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0})(F.last())\n", - "\n", - "output = Output('out', fir_x + fir_F)" + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", + "\n", + "fir_x = Fir(W_init=init_negexp, b_init=init_exp)(x.tw(0.2))\n", + "fir_F = Fir(\n", + " W_init=init_constant,\n", + " W_init_params={\"value\": 1},\n", + " b_init=init_constant,\n", + " b_init_params={\"value\": 0},\n", + ")(F.last())\n", + "\n", + "output = Output(\"out\", fir_x + fir_F)" ] }, { @@ -207,12 +212,12 @@ } ], "source": [ - "input = Input('x')\n", + "input = Input(\"x\")\n", "\n", - "weight = Parameter('w', dimensions=3, sw=2, init=init_constant)\n", + "weight = Parameter(\"w\", dimensions=3, sw=2, init=init_constant)\n", "\n", "fir_layer = Fir(W=weight, b=False, dropout=0.2)(input.sw(2))\n", - "output = Output('out', fir_layer)" + "output = Output(\"out\", fir_layer)" ] } ], diff --git a/docs/_autodoc/tutorials/examples/fuzzify.ipynb b/docs/_autodoc/tutorials/examples/fuzzify.ipynb index 0b9fa8e8..70702921 100644 --- a/docs/_autodoc/tutorials/examples/fuzzify.ipynb +++ b/docs/_autodoc/tutorials/examples/fuzzify.ipynb @@ -95,14 +95,14 @@ } ], "source": [ - "x = Input('x')\n", - "fuz = Fuzzify(5,[1,5])\n", - "out = Output('out',fuz(x.last()))\n", + "x = Input(\"x\")\n", + "fuz = Fuzzify(5, [1, 5])\n", + "out = Output(\"out\", fuz(x.last()))\n", "\n", "example = Modely(visualizer=MPLNotebookVisualizer())\n", - "example.addModel('model',out)\n", + "example.addModel(\"model\", out)\n", "example.neuralizeModel()\n", - "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))" + "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))" ] }, { @@ -159,14 +159,14 @@ } ], "source": [ - "x = Input('x')\n", - "fuz = Fuzzify(6,[1,6], functions = 'Rectangular')\n", - "out = Output('out',fuz(x.last()))\n", + "x = Input(\"x\")\n", + "fuz = Fuzzify(6, [1, 6], functions=\"Rectangular\")\n", + "out = Output(\"out\", fuz(x.last()))\n", "\n", "example = Modely(visualizer=MPLNotebookVisualizer())\n", - "example.addModel('model',out)\n", + "example.addModel(\"model\", out)\n", "example.neuralizeModel()\n", - "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))" + "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))" ] }, { @@ -235,16 +235,18 @@ "source": [ "def fun(x):\n", " import torch\n", + "\n", " return torch.tanh(x)\n", "\n", - "x = Input('x')\n", - "fuz = Fuzzify(output_dimension=11, range=[-5,5], functions=fun)\n", - "out = Output('out',fuz(x.last()))\n", + "\n", + "x = Input(\"x\")\n", + "fuz = Fuzzify(output_dimension=11, range=[-5, 5], functions=fun)\n", + "out = Output(\"out\", fuz(x.last()))\n", "\n", "example = Modely(visualizer=MPLNotebookVisualizer())\n", - "example.addModel('model',out)\n", + "example.addModel(\"model\", out)\n", "example.neuralizeModel()\n", - "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))" + "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))" ] }, { @@ -306,20 +308,24 @@ "source": [ "def fun1(x):\n", " import torch\n", + "\n", " return torch.sin(x)\n", "\n", + "\n", "def fun2(x):\n", " import torch\n", + "\n", " return torch.cos(x)\n", "\n", - "x = Input('x')\n", - "fuz = Fuzzify(2,range=[-1,5],functions=[fun1,fun2])\n", - "out = Output('out',fuz(x.last()))\n", + "\n", + "x = Input(\"x\")\n", + "fuz = Fuzzify(2, range=[-1, 5], functions=[fun1, fun2])\n", + "out = Output(\"out\", fuz(x.last()))\n", "\n", "example = Modely(visualizer=MPLNotebookVisualizer())\n", - "example.addModel('model',out)\n", + "example.addModel(\"model\", out)\n", "example.neuralizeModel()\n", - "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))" + "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))" ] }, { @@ -386,21 +392,25 @@ "source": [ "import torch\n", "\n", + "\n", "def fun1(x):\n", " return torch.sin(x)\n", + "\n", + "\n", "def fun2(x):\n", " return torch.cos(x)\n", "\n", - "x = Input('x')\n", - "F = Input('F')\n", "\n", - "fuz = Fuzzify(centers=[-1,0,3,5],functions=[fun1,fun2,fun1,fun2])\n", - "out = Output('out',fuz(x.last())+fuz(F.last()))\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", + "\n", + "fuz = Fuzzify(centers=[-1, 0, 3, 5], functions=[fun1, fun2, fun1, fun2])\n", + "out = Output(\"out\", fuz(x.last()) + fuz(F.last()))\n", "\n", "example = Modely(visualizer=MPLNotebookVisualizer())\n", - "example.addModel('model',out)\n", + "example.addModel(\"model\", out)\n", "example.neuralizeModel()\n", - "example.visualizer.showFunctions(list(example._model_def['Functions'].keys()))" + "example.visualizer.showFunctions(list(example._model_def[\"Functions\"].keys()))" ] } ], diff --git a/docs/_autodoc/tutorials/examples/inference.ipynb b/docs/_autodoc/tutorials/examples/inference.ipynb index 0c7395e5..4f21d720 100644 --- a/docs/_autodoc/tutorials/examples/inference.ipynb +++ b/docs/_autodoc/tutorials/examples/inference.ipynb @@ -98,15 +98,15 @@ ], "source": [ "## Model definition\n", - "x = Input('x')\n", - "F = Input('F')\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", "\n", - "next_x = Fir()(x.tw(0.5))+Fir()(F.last())\n", + "next_x = Fir()(x.tw(0.5)) + Fir()(F.last())\n", "\n", - "out = Output('next_x', next_x)\n", + "out = Output(\"next_x\", next_x)\n", "\n", "model = Modely()\n", - "model.addModel('model',[out])\n", + "model.addModel(\"model\", [out])\n", "model.neuralizeModel(0.05)" ] }, @@ -125,7 +125,9 @@ ], "source": [ "## Inference\n", - "results = model(inputs={'F':[[9]],'x':[[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11]]})\n", + "results = model(\n", + " inputs={\"F\": [[9]], \"x\": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]]}\n", + ")\n", "print(results)" ] }, @@ -143,7 +145,12 @@ } ], "source": [ - "results = model(inputs={'F':[[5],[4],[9]],'x':[[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13]]})\n", + "results = model(\n", + " inputs={\n", + " \"F\": [[5], [4], [9]],\n", + " \"x\": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13]],\n", + " }\n", + ")\n", "print(results)" ] }, @@ -170,7 +177,16 @@ } ], "source": [ - "results = model(inputs={'F':[[5],[2]],'x':[[1,2,3,4,5,6,7,8,9,10],[12,13,14,15,16,17,18,19,20,21]]}, sampled=True)\n", + "results = model(\n", + " inputs={\n", + " \"F\": [[5], [2]],\n", + " \"x\": [\n", + " [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n", + " [12, 13, 14, 15, 16, 17, 18, 19, 20, 21],\n", + " ],\n", + " },\n", + " sampled=True,\n", + ")\n", "print(results)" ] }, @@ -215,15 +231,15 @@ } ], "source": [ - "input1 = Input('in1')\n", - "W = Parameter('W', sw=3, values=[[1], [2], [3]])\n", - "out = Output('out',Fir(W=W)(input1.sw(3)))\n", + "input1 = Input(\"in1\")\n", + "W = Parameter(\"W\", sw=3, values=[[1], [2], [3]])\n", + "out = Output(\"out\", Fir(W=W)(input1.sw(3)))\n", "\n", "model = Modely(visualizer=TextVisualizer(), seed=42)\n", - "model.addModel('model', [out])\n", + "model.addModel(\"model\", [out])\n", "model.neuralizeModel()\n", "\n", - "result = model({'in1': [1, 2, 3]}, closed_loop={'in1':'out'})\n", + "result = model({\"in1\": [1, 2, 3]}, closed_loop={\"in1\": \"out\"})\n", "print(result)" ] }, @@ -277,14 +293,14 @@ } ], "source": [ - "input2 = Input('in2')\n", - "K = Parameter('K', sw=3, values=[[1], [2], [3]])\n", - "out2 = Output('out2',Fir(W=K)(input2.sw(3)))\n", + "input2 = Input(\"in2\")\n", + "K = Parameter(\"K\", sw=3, values=[[1], [2], [3]])\n", + "out2 = Output(\"out2\", Fir(W=K)(input2.sw(3)))\n", "\n", - "model.addModel('model2', [out2])\n", + "model.addModel(\"model2\", [out2])\n", "model.neuralizeModel()\n", "\n", - "result = model(inputs={'in1': [1, 2, 3]}, connect={'in2':'out'})\n", + "result = model(inputs={\"in1\": [1, 2, 3]}, connect={\"in2\": \"out\"})\n", "print(result)" ] }, @@ -313,7 +329,9 @@ } ], "source": [ - "result = model({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3)\n", + "result = model(\n", + " {\"in1\": [1, 2, 3, 4, 5]}, closed_loop={\"in1\": \"out\"}, prediction_samples=3\n", + ")\n", "print(result)" ] }, @@ -342,7 +360,12 @@ } ], "source": [ - "result = model({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3, num_of_samples = 5)\n", + "result = model(\n", + " {\"in1\": [1, 2, 3, 4, 5]},\n", + " closed_loop={\"in1\": \"out\"},\n", + " prediction_samples=3,\n", + " num_of_samples=5,\n", + ")\n", "print(result)" ] } diff --git a/docs/_autodoc/tutorials/examples/interpolation.ipynb b/docs/_autodoc/tutorials/examples/interpolation.ipynb index 7a015245..b365bffc 100644 --- a/docs/_autodoc/tutorials/examples/interpolation.ipynb +++ b/docs/_autodoc/tutorials/examples/interpolation.ipynb @@ -46,9 +46,11 @@ "metadata": {}, "outputs": [], "source": [ - "x = Input('x')\n", - "interpolation = Interpolation(x_points=[1.0, 2.0, 3.0, 4.0],y_points=[1.0, 4.0, 9.0, 16.0])(x.last())\n", - "out = Output('out',interpolation)" + "x = Input(\"x\")\n", + "interpolation = Interpolation(\n", + " x_points=[1.0, 2.0, 3.0, 4.0], y_points=[1.0, 4.0, 9.0, 16.0]\n", + ")(x.last())\n", + "out = Output(\"out\", interpolation)" ] }, { @@ -76,9 +78,11 @@ } ], "source": [ - "y = Input('y')\n", - "interpolation = Interpolation(x_points=[1.0, 4.0, 3.0, 2.0],y_points=[1.0, 16.0, 9.0, 4.0], mode='linear')(y.last())\n", - "out = Output('out',interpolation)" + "y = Input(\"y\")\n", + "interpolation = Interpolation(\n", + " x_points=[1.0, 4.0, 3.0, 2.0], y_points=[1.0, 16.0, 9.0, 4.0], mode=\"linear\"\n", + ")(y.last())\n", + "out = Output(\"out\", interpolation)" ] } ], diff --git a/docs/_autodoc/tutorials/examples/linear.ipynb b/docs/_autodoc/tutorials/examples/linear.ipynb index 065ba3e0..45f5c44d 100644 --- a/docs/_autodoc/tutorials/examples/linear.ipynb +++ b/docs/_autodoc/tutorials/examples/linear.ipynb @@ -69,7 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "x = Input('x').tw(0.05)\n", + "x = Input(\"x\").tw(0.05)\n", "linear = Linear(output_dimension=5)(x)" ] }, @@ -96,10 +96,10 @@ } ], "source": [ - "x = Input('x').last()\n", + "x = Input(\"x\").last()\n", "\n", - "weight = Parameter('W', values=[[1]])\n", - "bias = Parameter('b', values=[1])\n", + "weight = Parameter(\"W\", values=[[1]])\n", + "bias = Parameter(\"b\", values=[1])\n", "\n", "linear = Linear(W=weight, b=bias)(x)" ] @@ -131,8 +131,14 @@ } ], "source": [ - "x = Input('x').last()\n", - "linear = Linear(output_dimension=5, b=True, W_init=init_negexp, b_init=init_constant, b_init_params={'value':1})(x)" + "x = Input(\"x\").last()\n", + "linear = Linear(\n", + " output_dimension=5,\n", + " b=True,\n", + " W_init=init_negexp,\n", + " b_init=init_constant,\n", + " b_init_params={\"value\": 1},\n", + ")(x)" ] }, { @@ -158,9 +164,9 @@ } ], "source": [ - "input = Input('x')\n", + "input = Input(\"x\")\n", "linear = Linear(output_dimension=10, dropout=0.2)(input.sw(2))\n", - "output = Output('out', linear)" + "output = Output(\"out\", linear)" ] } ], diff --git a/docs/_autodoc/tutorials/examples/localmodel.ipynb b/docs/_autodoc/tutorials/examples/localmodel.ipynb index b62da3e0..9e6f94a5 100644 --- a/docs/_autodoc/tutorials/examples/localmodel.ipynb +++ b/docs/_autodoc/tutorials/examples/localmodel.ipynb @@ -59,11 +59,11 @@ "metadata": {}, "outputs": [], "source": [ - "c = Input('c')\n", - "x = Input('x')\n", - "activation = Fuzzify(2,[0,1],functions='Triangular')(c.last())\n", + "c = Input(\"c\")\n", + "x = Input(\"x\")\n", + "activation = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.last())\n", "loc = LocalModel(input_function=Fir())\n", - "out = Output('out', loc(x.tw(1), activation))" + "out = Output(\"out\", loc(x.tw(1), activation))" ] }, { @@ -93,11 +93,13 @@ } ], "source": [ - "c = Input('c')\n", - "x = Input('x')\n", - "activation = Fuzzify(2,[0,1],functions='Triangular')(c.last())\n", - "loc = LocalModel(input_function = lambda:Fir, output_function = lambda:Fir)(x.last(), activation)\n", - "out = Output('out', loc)" + "c = Input(\"c\")\n", + "x = Input(\"x\")\n", + "activation = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.last())\n", + "loc = LocalModel(input_function=lambda: Fir, output_function=lambda: Fir)(\n", + " x.last(), activation\n", + ")\n", + "out = Output(\"out\", loc)" ] }, { @@ -125,14 +127,17 @@ } ], "source": [ - "def myFun(in1,p1,p2):\n", - " return p1*in1+p2\n", + "def myFun(in1, p1, p2):\n", + " return p1 * in1 + p2\n", "\n", - "c = Input('c')\n", - "x = Input('x')\n", - "activation = Fuzzify(2,[0,1],functions='Triangular')(c.last())\n", - "loc = LocalModel(input_function = lambda:ParamFun(myFun), output_function = lambda:Fir)(x.last(), activation)\n", - "out = Output('out', loc)" + "\n", + "c = Input(\"c\")\n", + "x = Input(\"x\")\n", + "activation = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.last())\n", + "loc = LocalModel(input_function=lambda: ParamFun(myFun), output_function=lambda: Fir)(\n", + " x.last(), activation\n", + ")\n", + "out = Output(\"out\", loc)" ] }, { @@ -160,17 +165,21 @@ } ], "source": [ - "c = Input('c')\n", - "d = Input('d')\n", - "activationA = Fuzzify(2,[0,1],functions='Triangular')(c.tw(1))\n", - "activationB = Fuzzify(2,[0,1],functions='Triangular')(d.tw(1))\n", + "c = Input(\"c\")\n", + "d = Input(\"d\")\n", + "activationA = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.tw(1))\n", + "activationB = Fuzzify(2, [0, 1], functions=\"Triangular\")(d.tw(1))\n", + "\n", "\n", - "def myFun(in1,p1,p2):\n", - " return p1*in1+p2\n", + "def myFun(in1, p1, p2):\n", + " return p1 * in1 + p2\n", "\n", - "x = Input('x')\n", - "loc = LocalModel(input_function = lambda:ParamFun(myFun), output_function = Fir(3))(x.tw(1),(activationA,activationB))\n", - "out = Output('out', loc)" + "\n", + "x = Input(\"x\")\n", + "loc = LocalModel(input_function=lambda: ParamFun(myFun), output_function=Fir(3))(\n", + " x.tw(1), (activationA, activationB)\n", + ")\n", + "out = Output(\"out\", loc)" ] }, { @@ -203,32 +212,45 @@ } ], "source": [ - "c = Input('c')\n", - "d = Input('d')\n", - "activationA = Fuzzify(2,[0,1],functions='Triangular')(c.tw(1))\n", - "activationB = Fuzzify(2,[0,1],functions='Triangular')(d.tw(1))\n", + "c = Input(\"c\")\n", + "d = Input(\"d\")\n", + "activationA = Fuzzify(2, [0, 1], functions=\"Triangular\")(c.tw(1))\n", + "activationB = Fuzzify(2, [0, 1], functions=\"Triangular\")(d.tw(1))\n", + "\n", + "\n", + "def myFun(in1, p1, p2):\n", + " return p1 * in1 + p2\n", "\n", - "def myFun(in1,p1,p2):\n", - " return p1*in1+p2\n", "\n", "def input_function_gen(idx_list):\n", - " if idx_list == [0,0]:\n", - " p1, p2 = Parameter('p1_0',values=[[1]]), Parameter('p2_0',values=[[2]])\n", - " if idx_list == [0,1]:\n", - " p1, p2 = Parameter('p1_0',values=[[1]]), Parameter('p2_1',values=[[3]])\n", - " if idx_list == [1,0]:\n", - " p1, p2 = Parameter('p1_1',values=[[2]]), Parameter('p2_0',values=[[2]])\n", + " if idx_list == [0, 0]:\n", + " p1, p2 = Parameter(\"p1_0\", values=[[1]]), Parameter(\"p2_0\", values=[[2]])\n", + " if idx_list == [0, 1]:\n", + " p1, p2 = Parameter(\"p1_0\", values=[[1]]), Parameter(\"p2_1\", values=[[3]])\n", + " if idx_list == [1, 0]:\n", + " p1, p2 = Parameter(\"p1_1\", values=[[2]]), Parameter(\"p2_0\", values=[[2]])\n", " if idx_list == [1, 1]:\n", - " p1, p2 = Parameter('p1_1',values=[[2]]), Parameter('p2_1',values=[[3]])\n", - " return ParamFun(myFun,parameters_and_constants=[p1,p2])\n", + " p1, p2 = Parameter(\"p1_1\", values=[[2]]), Parameter(\"p2_1\", values=[[3]])\n", + " return ParamFun(myFun, parameters_and_constants=[p1, p2])\n", + "\n", "\n", "def output_function_gen(idx_list):\n", - " pfir = Parameter('pfir_'+str(idx_list),tw=1,dimensions=2,values=[[1+idx_list[0],2+idx_list[1]],[3+idx_list[0],4+idx_list[1]]])\n", - " return Fir(2,W=pfir)\n", + " pfir = Parameter(\n", + " \"pfir_\" + str(idx_list),\n", + " tw=1,\n", + " dimensions=2,\n", + " values=[[1 + idx_list[0], 2 + idx_list[1]], [3 + idx_list[0], 4 + idx_list[1]]],\n", + " )\n", + " return Fir(2, W=pfir)\n", + "\n", "\n", - "x = Input('x')\n", - "loc = LocalModel(input_function=input_function_gen, output_function= output_function_gen, pass_indexes = True)(x.tw(1),(activationA,activationB))\n", - "out = Output('out', loc)" + "x = Input(\"x\")\n", + "loc = LocalModel(\n", + " input_function=input_function_gen,\n", + " output_function=output_function_gen,\n", + " pass_indexes=True,\n", + ")(x.tw(1), (activationA, activationB))\n", + "out = Output(\"out\", loc)" ] } ], diff --git a/docs/_autodoc/tutorials/examples/parameter.ipynb b/docs/_autodoc/tutorials/examples/parameter.ipynb index 32f3ed14..ee3c001d 100644 --- a/docs/_autodoc/tutorials/examples/parameter.ipynb +++ b/docs/_autodoc/tutorials/examples/parameter.ipynb @@ -73,8 +73,8 @@ }, "outputs": [], "source": [ - "g = Constant('g', values=[9.81])\n", - "k = Parameter('k', tw=4)" + "g = Constant(\"g\", values=[9.81])\n", + "k = Parameter(\"k\", tw=4)" ] }, { @@ -107,14 +107,14 @@ } ], "source": [ - "x = Input('x')\n", + "x = Input(\"x\")\n", "\n", - "k = Parameter('k', dimensions=3, tw=4)\n", + "k = Parameter(\"k\", dimensions=3, tw=4)\n", "\n", "fir1 = Fir(W=k)\n", "fir2 = Fir(3, W=k)\n", "\n", - "out = Output('out', fir1(x.tw(4))+fir2(x.tw(4)))" + "out = Output(\"out\", fir1(x.tw(4)) + fir2(x.tw(4)))" ] }, { @@ -147,17 +147,19 @@ } ], "source": [ - "x= Input('x')\n", + "x = Input(\"x\")\n", + "\n", + "g = Parameter(\"g\", dimensions=3, values=[[4, 5, 6]])\n", + "t = Parameter(\"t\", dimensions=3, values=[[1, 2, 3]])\n", "\n", - "g = Parameter('g', dimensions=3, values=[[4,5,6]])\n", - "t = Parameter('t', dimensions=3, values=[[1,2,3]])\n", "\n", "def fun(x, k, t):\n", - " return x+(k+t)\n", + " return x + (k + t)\n", + "\n", "\n", - "p = ParamFun(fun, parameters_and_constants=[g,t])\n", + "p = ParamFun(fun, parameters_and_constants=[g, t])\n", "\n", - "out = Output('out', p(x.tw(1)))" + "out = Output(\"out\", p(x.tw(1)))" ] }, { @@ -190,10 +192,10 @@ } ], "source": [ - "g = Parameter('g', sw=1, values=[[1,2,3,4]])\n", - "o = Constant('o', sw=1, values=[[1,2,3,4]])\n", - "x = Input('x', dimensions=4)\n", - "out = Output('out', x.last()+g+o)" + "g = Parameter(\"g\", sw=1, values=[[1, 2, 3, 4]])\n", + "o = Constant(\"o\", sw=1, values=[[1, 2, 3, 4]])\n", + "x = Input(\"x\", dimensions=4)\n", + "out = Output(\"out\", x.last() + g + o)" ] }, { @@ -226,8 +228,8 @@ ], "source": [ "dt = SampleTime()\n", - "x = Input('x')\n", - "out = Output('out', x.last() + dt)" + "x = Input(\"x\")\n", + "out = Output(\"out\", x.last() + dt)" ] }, { @@ -250,7 +252,7 @@ }, "outputs": [], "source": [ - "p = Parameter('p', dimensions=4, init=init_constant, init_params={'value':4})" + "p = Parameter(\"p\", dimensions=4, init=init_constant, init_params={\"value\": 4})" ] } ], diff --git a/docs/_autodoc/tutorials/examples/parametric_functions.ipynb b/docs/_autodoc/tutorials/examples/parametric_functions.ipynb index 0ba8e250..aeeecda1 100644 --- a/docs/_autodoc/tutorials/examples/parametric_functions.ipynb +++ b/docs/_autodoc/tutorials/examples/parametric_functions.ipynb @@ -59,15 +59,17 @@ "metadata": {}, "outputs": [], "source": [ - "def myFun(K1,K2,p1,p2):\n", + "def myFun(K1, K2, p1, p2):\n", " import torch\n", - " return p1*K1+p2*torch.sin(K2)\n", "\n", - "x = Input('x')\n", - "F = Input('F')\n", + " return p1 * K1 + p2 * torch.sin(K2)\n", + "\n", + "\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", "\n", "parfun = ParamFun(myFun)\n", - "out = Output('out',parfun(x.last(),F.last()))" + "out = Output(\"out\", parfun(x.last(), F.last()))" ] }, { @@ -94,14 +96,16 @@ } ], "source": [ - "def myFun(K1,K2,p1):\n", + "def myFun(K1, K2, p1):\n", " import torch\n", - " return torch.stack([K1,2*K1,3*K1,4*K1],dim=2).squeeze(-1)*p1+K2\n", "\n", - "x=Input('x')\n", - "F=Input('F')\n", - "parfun = ParamFun(myFun, parameters_and_constants = {'p1':(1,4)})\n", - "out = Output('out',parfun(x.last(),F.last()))" + " return torch.stack([K1, 2 * K1, 3 * K1, 4 * K1], dim=2).squeeze(-1) * p1 + K2\n", + "\n", + "\n", + "x = Input(\"x\")\n", + "F = Input(\"F\")\n", + "parfun = ParamFun(myFun, parameters_and_constants={\"p1\": (1, 4)})\n", + "out = Output(\"out\", parfun(x.last(), F.last()))" ] }, { @@ -128,13 +132,14 @@ } ], "source": [ - "def myFun(K1,p1):\n", - " return K1*p1\n", + "def myFun(K1, p1):\n", + " return K1 * p1\n", "\n", - "x = Input('x')\n", - "K = Parameter('k', dimensions = 1, sw = 1,values=[[2.0]])\n", + "\n", + "x = Input(\"x\")\n", + "K = Parameter(\"k\", dimensions=1, sw=1, values=[[2.0]])\n", "parfun = ParamFun(myFun, parameters_and_constants=[K])\n", - "out = Output('out',parfun(x.sw(1)))" + "out = Output(\"out\", parfun(x.sw(1)))" ] }, { @@ -161,15 +166,16 @@ } ], "source": [ - "def myFun(K1,p1):\n", - " return K1*p1\n", + "def myFun(K1, p1):\n", + " return K1 * p1\n", + "\n", "\n", - "K = Parameter('k1', dimensions = 1, tw = 1, values=[[2.0],[3.0],[4.0],[5.0]])\n", - "R = Parameter('r1', dimensions = 1, tw = 1, values=[[5.0],[4.0],[3.0],[2.0]])\n", + "K = Parameter(\"k1\", dimensions=1, tw=1, values=[[2.0], [3.0], [4.0], [5.0]])\n", + "R = Parameter(\"r1\", dimensions=1, tw=1, values=[[5.0], [4.0], [3.0], [2.0]])\n", "\n", - "x = Input('x')\n", + "x = Input(\"x\")\n", "parfun = ParamFun(myFun)\n", - "out = Output('out',parfun(x.tw(1),K)+parfun(x.tw(1),R))" + "out = Output(\"out\", parfun(x.tw(1), K) + parfun(x.tw(1), R))" ] }, { @@ -196,13 +202,14 @@ } ], "source": [ - "def myFun(K1,p1):\n", - " return K1*p1\n", + "def myFun(K1, p1):\n", + " return K1 * p1\n", + "\n", "\n", "parfun = ParamFun(myFun)\n", - "x = Input('x')\n", - "c = Constant('c',values=[[5.0],[4.0],[3.0],[2.0]])\n", - "out = Output('out',parfun(x.sw(4),c))" + "x = Input(\"x\")\n", + "c = Constant(\"c\", values=[[5.0], [4.0], [3.0], [2.0]])\n", + "out = Output(\"out\", parfun(x.sw(4), c))" ] }, { @@ -232,13 +239,14 @@ ], "source": [ "def myFun(k, p1):\n", - " print(f'k:{k.shape}')\n", - " print(f'p1:{p1.shape}')\n", + " print(f\"k:{k.shape}\")\n", + " print(f\"p1:{p1.shape}\")\n", " return k * p1\n", "\n", - "x = Input('x')\n", + "\n", + "x = Input(\"x\")\n", "parfun = ParamFun(myFun, map_over_batch=True)\n", - "out = Output('out',parfun(x.sw(4)))" + "out = Output(\"out\", parfun(x.sw(4)))" ] } ], diff --git a/docs/_autodoc/tutorials/examples/partitioning.ipynb b/docs/_autodoc/tutorials/examples/partitioning.ipynb index e3a0708d..c916415b 100644 --- a/docs/_autodoc/tutorials/examples/partitioning.ipynb +++ b/docs/_autodoc/tutorials/examples/partitioning.ipynb @@ -80,13 +80,13 @@ } ], "source": [ - "x = Input('x', dimensions=10).last()\n", + "x = Input(\"x\", dimensions=10).last()\n", "sub = Part(x, 0, 2)\n", - "out = Output('out', sub)\n", + "out = Output(\"out\", sub)\n", "test = Modely(visualizer=None)\n", - "test.addModel('test', [out])\n", + "test.addModel(\"test\", [out])\n", "test.neuralizeModel()\n", - "test({'x': [[1,2,3,4,5,6,7,8,9,10]]})" + "test({\"x\": [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]})" ] }, { @@ -130,13 +130,13 @@ } ], "source": [ - "x = Input('x', dimensions=3).last()\n", + "x = Input(\"x\", dimensions=3).last()\n", "sub = Select(x, 1)\n", - "out = Output('out', sub)\n", + "out = Output(\"out\", sub)\n", "test = Modely(visualizer=None)\n", - "test.addModel('test', [out])\n", + "test.addModel(\"test\", [out])\n", "test.neuralizeModel()\n", - "test({'x': [[1,2,3]]})" + "test({\"x\": [[1, 2, 3]]})" ] }, { @@ -180,14 +180,14 @@ } ], "source": [ - "x = Input('x', dimensions=3).last()\n", - "y = Input('y', dimensions=5).last()\n", + "x = Input(\"x\", dimensions=3).last()\n", + "y = Input(\"y\", dimensions=5).last()\n", "cat = Concatenate(x, y)\n", - "out = Output('out', cat)\n", + "out = Output(\"out\", cat)\n", "test = Modely(visualizer=None)\n", - "test.addModel('test', [out])\n", + "test.addModel(\"test\", [out])\n", "test.neuralizeModel()\n", - "test({'x': [[1,2,3]],'y': [[4,5,6,7,8]]})" + "test({\"x\": [[1, 2, 3]], \"y\": [[4, 5, 6, 7, 8]]})" ] }, { @@ -231,16 +231,16 @@ } ], "source": [ - "x = Input('x')\n", + "x = Input(\"x\")\n", "x_sw10 = x.sw(10)\n", "relation = SamplePart(x_sw10, 0, 3)\n", - "out = Output('out', relation)\n", - "out2 = Output('out2', x.last())\n", + "out = Output(\"out\", relation)\n", + "out2 = Output(\"out2\", x.last())\n", "test = Modely(visualizer=None)\n", - "test.addModel('test', [out,out2])\n", + "test.addModel(\"test\", [out, out2])\n", "test.neuralizeModel()\n", "# Test 1 input in time\n", - "test({'x': [1,2,3,4,5,6,7,8,9,10]})" + "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})" ] }, { @@ -266,7 +266,7 @@ ], "source": [ "# Test 2 input in time\n", - "test({'x': [1,2,3,4,5,6,7,8,9,10,11]})" + "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})" ] }, { @@ -310,13 +310,13 @@ } ], "source": [ - "x = Input('x').sw(10)\n", + "x = Input(\"x\").sw(10)\n", "relation = SampleSelect(x, 5)\n", - "out = Output('out', relation)\n", + "out = Output(\"out\", relation)\n", "test = Modely(visualizer=None)\n", - "test.addModel('test', [out])\n", + "test.addModel(\"test\", [out])\n", "test.neuralizeModel()\n", - "test({'x': [1,2,3,4,5,6,7,8,9,10]})" + "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})" ] }, { @@ -349,9 +349,9 @@ ], "source": [ "test = Modely(visualizer=None)\n", - "test.addModel('test', [out, Output('out2', relation.sw(2))])\n", + "test.addModel(\"test\", [out, Output(\"out2\", relation.sw(2))])\n", "test.neuralizeModel()\n", - "test({'x': [1,2,3,4,5,6,7,8,9,10,11]})" + "test({\"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]})" ] }, { @@ -396,16 +396,16 @@ } ], "source": [ - "x = Input('x').sw(5)\n", - "y = Input('y').sw(3)\n", + "x = Input(\"x\").sw(5)\n", + "y = Input(\"y\").sw(3)\n", "cat = TimeConcatenate(x, y)\n", "cat2 = TimeConcatenate(Fir(x), y)\n", - "out = Output('out', cat)\n", - "out2 = Output('out2', cat2)\n", + "out = Output(\"out\", cat)\n", + "out2 = Output(\"out2\", cat2)\n", "test = Modely(visualizer=None)\n", - "test.addModel('test', [out,out2])\n", + "test.addModel(\"test\", [out, out2])\n", "test.neuralizeModel()\n", - "test({'x': [1,2,3,4,5],'y': [1,2,3]})" + "test({\"x\": [1, 2, 3, 4, 5], \"y\": [1, 2, 3]})" ] } ], diff --git a/docs/_autodoc/tutorials/examples/states.ipynb b/docs/_autodoc/tutorials/examples/states.ipynb index 2087b0ec..5b2f16b0 100644 --- a/docs/_autodoc/tutorials/examples/states.ipynb +++ b/docs/_autodoc/tutorials/examples/states.ipynb @@ -65,8 +65,8 @@ }, "outputs": [], "source": [ - "clearNames('x_state')\n", - "x_state = Input('x_state', dimensions=1)\n", + "clearNames(\"x_state\")\n", + "x_state = Input(\"x_state\", dimensions=1)\n", "x_out = Fir(x_state.tw(0.5))" ] }, @@ -90,9 +90,9 @@ }, "outputs": [], "source": [ - "clearNames('out')\n", + "clearNames(\"out\")\n", "x_out.closedLoop(x_state)\n", - "out = Output('out',x_out)" + "out = Output(\"out\", x_out)" ] }, { @@ -113,9 +113,9 @@ }, "outputs": [], "source": [ - "clearNames('out')\n", + "clearNames(\"out\")\n", "x_out = ClosedLoop(x_out, x_state)\n", - "out = Output('out',x_out)" + "out = Output(\"out\", x_out)" ] }, { @@ -143,7 +143,7 @@ "clearNames()\n", "x_out = Fir(x_state.tw(0.5))\n", "x_out.connect(x_state)\n", - "out = Output('out',x_out)" + "out = Output(\"out\", x_out)" ] }, { @@ -164,9 +164,9 @@ }, "outputs": [], "source": [ - "clearNames('out')\n", + "clearNames(\"out\")\n", "x_out = Connect(x_out, x_state)\n", - "out = Output('out',x_out)" + "out = Output(\"out\", x_out)" ] }, { @@ -366,52 +366,64 @@ } ], "source": [ - "clearNames(['a','b_t','c','d_t','b_in','shared','b','A','B','C','D','d'])\n", + "clearNames([\"a\", \"b_t\", \"c\", \"d_t\", \"b_in\", \"shared\", \"b\", \"A\", \"B\", \"C\", \"D\", \"d\"])\n", "import numpy as np\n", "\n", + "\n", "def linear_function(x, k1, k2):\n", - " return x*k1 + k2\n", + " return x * k1 + k2\n", + "\n", "\n", - "data_a = np.arange(1,101, dtype=np.float32)\n", + "data_a = np.arange(1, 101, dtype=np.float32)\n", "data_b_t = linear_function(data_a, 2, 3)\n", "\n", - "data_c = np.arange(1,101, dtype=np.float32)\n", - "data_b_in = np.arange(5,105, dtype=np.float32)\n", + "data_c = np.arange(1, 101, dtype=np.float32)\n", + "data_b_in = np.arange(5, 105, dtype=np.float32)\n", "data_d_t = linear_function(data_c, 5, 1)\n", "\n", - "dataset = {'a': data_a, 'b_t': data_b_t, 'c':data_c, 'b_in': data_b_in, 'd_t':data_d_t }\n", + "dataset = {\n", + " \"a\": data_a,\n", + " \"b_t\": data_b_t,\n", + " \"c\": data_c,\n", + " \"b_in\": data_b_in,\n", + " \"d_t\": data_d_t,\n", + "}\n", "## Model a\n", - "a = Input('a')\n", - "b_t = Input('b_t')\n", - "shared = Parameter('shared',dimensions=(1,1))\n", - "output_relation = Linear(W=shared)(a.last())+Linear(W='A')(Fir(W='B')(a.tw(0.5)))\n", - "b = Output('b',output_relation)\n", + "a = Input(\"a\")\n", + "b_t = Input(\"b_t\")\n", + "shared = Parameter(\"shared\", dimensions=(1, 1))\n", + "output_relation = Linear(W=shared)(a.last()) + Linear(W=\"A\")(Fir(W=\"B\")(a.tw(0.5)))\n", + "b = Output(\"b\", output_relation)\n", "\n", "model = Modely(seed=42)\n", - "model.addModel('b_model', b)\n", - "model.addMinimize('b_min', b, b_t.last())\n", + "model.addModel(\"b_model\", b)\n", + "model.addMinimize(\"b_min\", b, b_t.last())\n", "model.neuralizeModel(0.1)\n", "\n", "# Model d\n", - "c = Input('c')\n", - "d_t = Input('d_t')\n", - "b_in = Input('b_in')\n", + "c = Input(\"c\")\n", + "d_t = Input(\"d_t\")\n", + "b_in = Input(\"b_in\")\n", "output_relation.connect(b_in)\n", - "d = Output('d',Linear(W=shared)(c.last())+Fir(W='C')(c.tw(0.5))+Fir(W='D')(b_in.tw(0.3)))\n", + "d = Output(\n", + " \"d\", Linear(W=shared)(c.last()) + Fir(W=\"C\")(c.tw(0.5)) + Fir(W=\"D\")(b_in.tw(0.3))\n", + ")\n", "\n", - "model.addModel('d_model', [b,d])\n", - "model.addMinimize('d_min', d, d_t.last())\n", + "model.addModel(\"d_model\", [b, d])\n", + "model.addMinimize(\"d_min\", d, d_t.last())\n", "model.neuralizeModel(0.1)\n", - "model.loadData('dataset', dataset)\n", + "model.loadData(\"dataset\", dataset)\n", "\n", - "params = {'num_of_epochs': 1,\n", - " 'train_batch_size': 8,\n", - " 'val_batch_size': 8,\n", - " 'test_batch_size':1,\n", - " 'lr':0.1}\n", + "params = {\n", + " \"num_of_epochs\": 1,\n", + " \"train_batch_size\": 8,\n", + " \"val_batch_size\": 8,\n", + " \"test_batch_size\": 1,\n", + " \"lr\": 0.1,\n", + "}\n", "\n", "## training dei parametri di tutti i modelli\n", - "_ = model.trainModel(splits=[100,0,0], training_params=params, prediction_samples=4)" + "_ = model.trainModel(splits=[100, 0, 0], training_params=params, prediction_samples=4)" ] }, { @@ -551,29 +563,35 @@ ], "source": [ "import numpy as np\n", - "clearNames(['x','x_state','y_state','out'])\n", - "x = Input('x', dimensions=3)\n", - "x_state = Input('x_state', dimensions=3)\n", - "y_state = Input('y_state', dimensions=3)\n", + "\n", + "clearNames([\"x\", \"x_state\", \"y_state\", \"out\"])\n", + "x = Input(\"x\", dimensions=3)\n", + "x_state = Input(\"x_state\", dimensions=3)\n", + "y_state = Input(\"y_state\", dimensions=3)\n", "x_out = Linear(output_dimension=3)(x_state.tw(0.5))\n", "y_out = Linear(output_dimension=3)(y_state.tw(0.5))\n", "x_out.closedLoop(x_state)\n", "y_out.closedLoop(y_state)\n", - "out = Output('out',x_out+y_out)\n", + "out = Output(\"out\", x_out + y_out)\n", "\n", "test = Modely(seed=42)\n", - "test.addModel('model', out)\n", - "test.addMinimize('error', out, x.tw(0.5))\n", + "test.addModel(\"model\", out)\n", + "test.addMinimize(\"error\", out, x.tw(0.5))\n", "\n", "test.neuralizeModel(0.1)\n", "\n", - "dataset = {'x':np.array([np.random.uniform(1,4,300)]).reshape(100,3).tolist()}\n", - "test.loadData(name='dataset', source=dataset)\n", + "dataset = {\"x\": np.array([np.random.uniform(1, 4, 300)]).reshape(100, 3).tolist()}\n", + "test.loadData(name=\"dataset\", source=dataset)\n", "\n", "# Training non ricorrente\n", - "params = {'num_of_epochs': 10, 'train_batch_size': 1, 'val_batch_size':1, 'lr':0.01}\n", - "tp = test.trainModel(splits=[70,20,10], prediction_samples=5, shuffle_data=False, training_params=params)\n", - "print('finale state: ', test._states)" + "params = {\"num_of_epochs\": 10, \"train_batch_size\": 1, \"val_batch_size\": 1, \"lr\": 0.01}\n", + "tp = test.trainModel(\n", + " splits=[70, 20, 10],\n", + " prediction_samples=5,\n", + " shuffle_data=False,\n", + " training_params=params,\n", + ")\n", + "print(\"finale state: \", test._states)" ] }, { @@ -605,7 +623,7 @@ ], "source": [ "test.resetStates()\n", - "print('finale state: ', test.states)" + "print(\"finale state: \", test.states)" ] }, { @@ -728,29 +746,42 @@ ], "source": [ "import numpy as np\n", + "\n", "clearNames()\n", - "x = Input('x', dimensions=3)\n", - "x_s = Input('x_s', dimensions=3)\n", - "y_s = Input('y_s', dimensions=3)\n", + "x = Input(\"x\", dimensions=3)\n", + "x_s = Input(\"x_s\", dimensions=3)\n", + "y_s = Input(\"y_s\", dimensions=3)\n", "x_out = Linear(output_dimension=3)(x_s.tw(0.5))\n", "y_out = Linear(output_dimension=3)(y_s.tw(0.5))\n", - "out = Output('out',x_out+y_out)\n", - "out_x = Output('out_x',x_out)\n", - "out_y = Output('out_y',y_out)\n", + "out = Output(\"out\", x_out + y_out)\n", + "out_x = Output(\"out_x\", x_out)\n", + "out_y = Output(\"out_y\", y_out)\n", "\n", "test = Modely(seed=42)\n", - "test.addModel('model', [out,out_x,out_y])\n", - "test.addMinimize('error', out, x.tw(0.5))\n", + "test.addModel(\"model\", [out, out_x, out_y])\n", + "test.addMinimize(\"error\", out, x.tw(0.5))\n", "\n", "test.neuralizeModel(0.1)\n", "\n", - "dataset = {'x':np.array([np.random.uniform(1,4,300)]).reshape(100,3).tolist()}\n", - "test.loadData(name='dataset', source=dataset)\n", + "dataset = {\"x\": np.array([np.random.uniform(1, 4, 300)]).reshape(100, 3).tolist()}\n", + "test.loadData(name=\"dataset\", source=dataset)\n", "\n", "# Training non ricorrente\n", - "params = {'num_of_epochs': 10, 'train_batch_size': 4, 'val_batch_size':4, 'test_batch_size':1, 'lr':0.01}\n", - "test.trainModel(splits=[70,20,10], prediction_samples=3, shuffle_data=False, closed_loop={'x_s':'out_x','y_s':'out_y'}, training_params=params)\n", - "print('finale state: ', test.states)" + "params = {\n", + " \"num_of_epochs\": 10,\n", + " \"train_batch_size\": 4,\n", + " \"val_batch_size\": 4,\n", + " \"test_batch_size\": 1,\n", + " \"lr\": 0.01,\n", + "}\n", + "test.trainModel(\n", + " splits=[70, 20, 10],\n", + " prediction_samples=3,\n", + " shuffle_data=False,\n", + " closed_loop={\"x_s\": \"out_x\", \"y_s\": \"out_y\"},\n", + " training_params=params,\n", + ")\n", + "print(\"finale state: \", test.states)" ] } ], diff --git a/docs/_autodoc/tutorials/examples/training.ipynb b/docs/_autodoc/tutorials/examples/training.ipynb index 2a29fa7f..085eed9e 100644 --- a/docs/_autodoc/tutorials/examples/training.ipynb +++ b/docs/_autodoc/tutorials/examples/training.ipynb @@ -203,19 +203,19 @@ ], "source": [ "## Create a neural network model and Load a dataset\n", - "in1 = Input('in1')\n", - "target = Input('target')\n", + "in1 = Input(\"in1\")\n", + "target = Input(\"target\")\n", "relation = Fir(in1.tw(0.05))\n", - "output = Output('out', relation)\n", + "output = Output(\"out\", relation)\n", "\n", "model = Modely(visualizer=TextVisualizer())\n", - "model.addMinimize('error', output, target.last())\n", - "model.addModel('model', output)\n", + "model.addMinimize(\"error\", output, target.last())\n", + "model.addModel(\"model\", output)\n", "model.neuralizeModel(0.01)\n", "\n", - "train_folder = 'data'\n", - "data_struct = ['in1', '', 'target']\n", - "model.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=1)" + "train_folder = \"data\"\n", + "data_struct = [\"in1\", \"\", \"target\"]\n", + "model.loadData(name=\"dataset\", source=train_folder, format=data_struct, skiplines=1)" ] }, { @@ -384,7 +384,14 @@ } ], "source": [ - "training_parameters = model.trainModel(models='model', train_dataset='dataset', num_of_epochs=10, lr=0.01, shuffle_data=True, train_batch_size=4)" + "training_parameters = model.trainModel(\n", + " models=\"model\",\n", + " train_dataset=\"dataset\",\n", + " num_of_epochs=10,\n", + " lr=0.01,\n", + " shuffle_data=True,\n", + " train_batch_size=4,\n", + ")" ] }, { @@ -414,7 +421,14 @@ } ], "source": [ - "training_parameters = model.trainModel(models='model', dataset='dataset', splits=[70, 20, 10], num_of_epochs=10, shuffle_data=True, train_batch_size=4)" + "training_parameters = model.trainModel(\n", + " models=\"model\",\n", + " dataset=\"dataset\",\n", + " splits=[70, 20, 10],\n", + " num_of_epochs=10,\n", + " shuffle_data=True,\n", + " train_batch_size=4,\n", + ")" ] }, { @@ -479,19 +493,43 @@ } ], "source": [ - "target = Input('target')\n", - "x = Input('x')\n", + "target = Input(\"target\")\n", + "x = Input(\"x\")\n", "relation = Fir(x.last())\n", "relation.closedLoop(x)\n", - "output = Output('out', relation)\n", + "output = Output(\"out\", relation)\n", "\n", "test = Modely(visualizer=TextVisualizer(), seed=42)\n", - "test.addModel('model', output)\n", - "test.addMinimize('error', target.next(), relation)\n", + "test.addModel(\"model\", output)\n", + "test.addMinimize(\"error\", target.next(), relation)\n", "test.neuralizeModel(0.01)\n", "\n", - "dataset = {'x': [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], 'target': [21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40]}\n", - "test.loadData(name='dataset', source=dataset)" + "dataset = {\n", + " \"x\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],\n", + " \"target\": [\n", + " 21,\n", + " 22,\n", + " 23,\n", + " 24,\n", + " 25,\n", + " 26,\n", + " 27,\n", + " 28,\n", + " 29,\n", + " 30,\n", + " 31,\n", + " 32,\n", + " 33,\n", + " 34,\n", + " 35,\n", + " 36,\n", + " 37,\n", + " 38,\n", + " 39,\n", + " 40,\n", + " ],\n", + "}\n", + "test.loadData(name=\"dataset\", source=dataset)" ] }, { @@ -548,7 +586,15 @@ } ], "source": [ - "training_parameters = test.trainModel(train_dataset='dataset', lr=0.01, num_of_epochs=10, train_batch_size=4, prediction_samples=2, step=1, shuffle_data=True)" + "training_parameters = test.trainModel(\n", + " train_dataset=\"dataset\",\n", + " lr=0.01,\n", + " num_of_epochs=10,\n", + " train_batch_size=4,\n", + " prediction_samples=2,\n", + " step=1,\n", + " shuffle_data=True,\n", + ")" ] }, { @@ -615,7 +661,9 @@ } ], "source": [ - "training_parameters = test.trainModel(train_dataset='dataset', minimize_gain={'error':0})" + "training_parameters = test.trainModel(\n", + " train_dataset=\"dataset\", minimize_gain={\"error\": 0}\n", + ")" ] }, { @@ -683,12 +731,14 @@ } ], "source": [ - "optimizer_defaults = {\n", - " 'lr': 0.1,\n", - " 'betas': (0.5, 0.99)\n", - " }\n", + "optimizer_defaults = {\"lr\": 0.1, \"betas\": (0.5, 0.99)}\n", "\n", - "training_parameters = test.trainModel(train_dataset='dataset', optimizer='Adam', optimizer_defaults=optimizer_defaults, num_of_epochs=10)" + "training_parameters = test.trainModel(\n", + " train_dataset=\"dataset\",\n", + " optimizer=\"Adam\",\n", + " optimizer_defaults=optimizer_defaults,\n", + " num_of_epochs=10,\n", + ")" ] }, { @@ -759,7 +809,13 @@ ], "source": [ "from nnodely.support.earlystopping import early_stop_patience, select_best_model\n", - "training_parameters = test.trainModel(train_dataset='dataset', minimize_gain={'error':0}, early_stopping=early_stop_patience, select_model=select_best_model)" + "\n", + "training_parameters = test.trainModel(\n", + " train_dataset=\"dataset\",\n", + " minimize_gain={\"error\": 0},\n", + " early_stopping=early_stop_patience,\n", + " select_model=select_best_model,\n", + ")" ] }, { @@ -863,12 +919,22 @@ } ], "source": [ - "test_folder = 'data'\n", - "data_struct = ['in1', '', 'target']\n", - "model.loadData(name='dataset_test', source=test_folder, format=data_struct, skiplines=1)\n", - "model.loadData(name='dataset_val', source=test_folder, format=data_struct, skiplines=1)\n", + "test_folder = \"data\"\n", + "data_struct = [\"in1\", \"\", \"target\"]\n", + "model.loadData(name=\"dataset_test\", source=test_folder, format=data_struct, skiplines=1)\n", + "model.loadData(name=\"dataset_val\", source=test_folder, format=data_struct, skiplines=1)\n", "\n", - "training_parameters = model.trainAndAnalyze(models='model', train_dataset='dataset', validation_dataset='dataset_val', train_batch_size=4, test_dataset='dataset_test', test_batch_size=1, num_of_epochs=10, lr=0.01, shuffle_data=True)" + "training_parameters = model.trainAndAnalyze(\n", + " models=\"model\",\n", + " train_dataset=\"dataset\",\n", + " validation_dataset=\"dataset_val\",\n", + " train_batch_size=4,\n", + " test_dataset=\"dataset_test\",\n", + " test_batch_size=1,\n", + " num_of_epochs=10,\n", + " lr=0.01,\n", + " shuffle_data=True,\n", + ")" ] }, { @@ -924,6 +990,7 @@ ], "source": [ "import pprint\n", + "\n", "print(\"Training completed with parameters:\", pprint.pprint(training_parameters))" ] } diff --git a/docs/conf.py b/docs/conf.py index 1d6e9dda..28c8aec6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,29 +4,36 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- import os + + def read_version(): - version_file = os.path.join(os.path.dirname(__file__), '..', 'nnodely', '__init__.py') - with open(version_file, 'r') as f: + version_file = os.path.join( + os.path.dirname(__file__), "..", "nnodely", "__init__.py" + ) + with open(version_file, "r") as f: for line in f: - if line.startswith('__version__'): + if line.startswith("__version__"): delim = '"' if '"' in line else "'" return line.split(delim)[1] raise RuntimeError("Unable to find version string.") + def skip_nnodely(app, what, name, obj, skip, options): # Skip the alias nnodely if name == "nnodely": return True # esclude questo membro dalla documentazione return skip + def setup(app): app.connect("autodoc-skip-member", skip_nnodely) + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = __package__ -author = 'tonegas' +author = "tonegas" release = read_version() version = read_version() @@ -34,12 +41,12 @@ def setup(app): # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ -# 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx.ext.mathjax', - 'myst_parser', - 'nbsphinx', + # 'sphinx.ext.autodoc', + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", + "myst_parser", + "nbsphinx", ] templates_path = [] @@ -60,8 +67,8 @@ def setup(app): } -html_static_path = ['_static'] -html_logo = '_static/logo.png' +html_static_path = ["_static"] +html_logo = "_static/logo.png" # -- Options for EPUB output ------------------------------------------------- -epub_copyright = '2024, tonegas' # Add this line +epub_copyright = "2024, tonegas" # Add this line diff --git a/mplplots/__init__.py b/mplplots/__init__.py index eb1f48f8..9d18dea1 100644 --- a/mplplots/__init__.py +++ b/mplplots/__init__.py @@ -1 +1,7 @@ -from mplplots.plots import plot_training, plot_results, plot_fuzzy, plot_2d_function, plot_3d_function \ No newline at end of file +from mplplots.plots import ( + plot_training, + plot_results, + plot_fuzzy, + plot_2d_function, + plot_3d_function, +) diff --git a/mplplots/plots.py b/mplplots/plots.py index 9eea6db0..b5babfee 100644 --- a/mplplots/plots.py +++ b/mplplots/plots.py @@ -2,22 +2,30 @@ import matplotlib.colors as mcolors -def plot_training(ax, title, key, data_train, data_val = None, last = None): + +def plot_training(ax, title, key, data_train, data_val=None, last=None): # Plot data if last is not None: - ax.set_title(f'{title} - epochs last {last}') + ax.set_title(f"{title} - epochs last {last}") else: - ax.set_title(f'{title}') + ax.set_title(f"{title}") - ax.plot([i + 1 for i in range(len(data_train))], data_train, label=f'Train loss {key}') + ax.plot( + [i + 1 for i in range(len(data_train))], data_train, label=f"Train loss {key}" + ) if data_val: - ax.plot([i + 1 for i in range(len(data_val))], data_val, '-.', label=f'Validation loss {key}') + ax.plot( + [i + 1 for i in range(len(data_val))], + data_val, + "-.", + label=f"Validation loss {key}", + ) - ax.set_yscale('log') + ax.set_yscale("log") ax.grid(True) - ax.legend(loc='best') - ax.set_xlabel('Epochs') - ax.set_ylabel('Loss') + ax.legend(loc="best") + ax.set_xlabel("Epochs") + ax.set_ylabel("Loss") # Set plot limits data_train = np.nan_to_num(data_train, nan=np.nan, posinf=np.nan, neginf=np.nan) if data_val: @@ -32,13 +40,13 @@ def plot_training(ax, title, key, data_train, data_val = None, last = None): def plot_results(ax, name_data, key, A, B, data_idxs, sample_time): # Plot data - ax.set_title(f'{key} on the dataset {name_data}') + ax.set_title(f"{key} on the dataset {name_data}") A_t = np.transpose(np.array(A)) B_t = np.transpose(np.array(B)) idxs_t = np.transpose(np.array(data_idxs)) - color_A = 'tab:blue' + color_A = "tab:blue" rgb_A = mcolors.to_rgb(color_A) - color_B = 'tab:orange' + color_B = "tab:orange" rgb_B = mcolors.to_rgb(color_B) delta = 0.1 @@ -47,64 +55,136 @@ def plot_results(ax, name_data, key, A, B, data_idxs, sample_time): time_array = [] num_samples = A_t.shape[2] for o in range(A_t.shape[1]): - time_array.append(np.linspace(0, (num_samples - 1) * sample_time, num_samples) + sample_time * o) + time_array.append( + np.linspace(0, (num_samples - 1) * sample_time, num_samples) + + sample_time * o + ) time_array = np.array(time_array) for ind_dim in range(A_t.shape[0]): # Print the marker only if the output have a time window - ax.plot(time_array[0,:], A_t[ind_dim, 0, :], - color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A), - marker='s' if A_t.shape[1] > 1 else None, markersize=2, - label=f'A_{ind_dim}') - ax.plot(time_array[0,:], B_t[ind_dim, 0, :], '-.', - color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B), - marker='o' if A_t.shape[1] > 1 else None, markersize=2, - label=f'B_{ind_dim}') - if A_t.shape[1] > 1 : - correlation = np.empty((A_t.shape[0],A_t.shape[2])) + ax.plot( + time_array[0, :], + A_t[ind_dim, 0, :], + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A), + marker="s" if A_t.shape[1] > 1 else None, + markersize=2, + label=f"A_{ind_dim}", + ) + ax.plot( + time_array[0, :], + B_t[ind_dim, 0, :], + "-.", + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B), + marker="o" if A_t.shape[1] > 1 else None, + markersize=2, + label=f"B_{ind_dim}", + ) + if A_t.shape[1] > 1: + correlation = np.empty((A_t.shape[0], A_t.shape[2])) for ind_el in range(A_t.shape[2]): - ax.plot(time_array[:,ind_el], A_t[ind_dim,:,ind_el], color=tuple((x + delta*ind_dim)%1.0001 for x in rgb_A)) - ax.plot(time_array[:,ind_el], B_t[ind_dim,:,ind_el], '-.', color=tuple((x + delta*ind_dim)%1.0001 for x in rgb_B)) - correlation[ind_dim, ind_el] = np.corrcoef(A_t[ind_dim,:,ind_el], B_t[ind_dim,:,ind_el])[0, 1] - ax.text(0.05, 0.95 - 0.05 * ind_dim, - f'Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=1)[ind_dim]:.2f}', - transform=ax.transAxes, verticalalignment='top') + ax.plot( + time_array[:, ind_el], + A_t[ind_dim, :, ind_el], + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A), + ) + ax.plot( + time_array[:, ind_el], + B_t[ind_dim, :, ind_el], + "-.", + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B), + ) + correlation[ind_dim, ind_el] = np.corrcoef( + A_t[ind_dim, :, ind_el], B_t[ind_dim, :, ind_el] + )[0, 1] + ax.text( + 0.05, + 0.95 - 0.05 * ind_dim, + f"Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=1)[ind_dim]:.2f}", + transform=ax.transAxes, + verticalalignment="top", + ) else: correlation = np.empty((A_t.shape[0],)) - correlation[ind_dim] = np.corrcoef(A_t[ind_dim, 0], B_t[ind_dim, 0])[0, 1] - ax.text(0.05, 0.95-0.05*ind_dim, f'Correlation A_{ind_dim} - B_{ind_dim}: {correlation[ind_dim]:.2f}', transform=ax.transAxes, verticalalignment='top') + correlation[ind_dim] = np.corrcoef(A_t[ind_dim, 0], B_t[ind_dim, 0])[ + 0, 1 + ] + ax.text( + 0.05, + 0.95 - 0.05 * ind_dim, + f"Correlation A_{ind_dim} - B_{ind_dim}: {correlation[ind_dim]:.2f}", + transform=ax.transAxes, + verticalalignment="top", + ) else: correlation = np.empty(A_t.shape) for ind_dim in range(A_t.shape[0]): first = True - ax.scatter(idxs_t[:,0] * sample_time, A_t[ind_dim, 0, :, 0], marker='s', s=6, - color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A)) - ax.scatter(idxs_t[:,0] * sample_time, B_t[ind_dim, 0, :, 0], marker='o', s=6, - color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B)) + ax.scatter( + idxs_t[:, 0] * sample_time, + A_t[ind_dim, 0, :, 0], + marker="s", + s=6, + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A), + ) + ax.scatter( + idxs_t[:, 0] * sample_time, + B_t[ind_dim, 0, :, 0], + marker="o", + s=6, + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B), + ) for ind_el in range(A_t.shape[2]): time_array = idxs_t[ind_el] * sample_time # Print the marker only if the output have a time window - ax.plot(time_array, A_t[ind_dim, 0, ind_el], marker = 's' if A_t.shape[1] > 1 else None, markersize=2, + ax.plot( + time_array, + A_t[ind_dim, 0, ind_el], + marker="s" if A_t.shape[1] > 1 else None, + markersize=2, + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A), + label=f"A_{ind_dim}" if first else None, + ) + ax.plot( + time_array, + B_t[ind_dim, 0, ind_el], + "-.", + marker="o" if A_t.shape[1] > 1 else None, + markersize=2, + color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B), + label=f"B_{ind_dim}" if first else None, + ) + for ind_pred in range(A_t.shape[3]): + time_array = idxs_t[ind_el, ind_pred] * sample_time + np.linspace( + 0, (A_t.shape[1] - 1) * sample_time, A_t.shape[1] + ) + ax.plot( + time_array, + A_t[ind_dim, :, ind_el, ind_pred], color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A), - label=f'A_{ind_dim}' if first else None) - ax.plot(time_array, B_t[ind_dim, 0, ind_el], '-.', marker = 'o' if A_t.shape[1] > 1 else None, markersize=2, + ) + ax.plot( + time_array, + B_t[ind_dim, :, ind_el, ind_pred], + "-.", color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B), - label=f'B_{ind_dim}' if first else None) - for ind_pred in range(A_t.shape[3]): - time_array = idxs_t[ind_el,ind_pred] * sample_time + np.linspace(0, (A_t.shape[1] - 1) * sample_time, A_t.shape[1]) - ax.plot(time_array, A_t[ind_dim, :, ind_el,ind_pred], - color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_A)) - ax.plot(time_array, B_t[ind_dim, :, ind_el,ind_pred], '-.', - color=tuple((x + delta * ind_dim) % 1.0001 for x in rgb_B)) + ) first = False for ind_win in range(A_t.shape[1]): - correlation[ind_dim,ind_win,ind_el] = np.corrcoef(A_t[ind_dim,ind_win,ind_el], B_t[ind_dim,ind_win,ind_el])[0, 1] - ax.text(0.05, 0.95-0.05*ind_dim, f'Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=(1, 2, 3))[ind_dim]:.2f}', transform=ax.transAxes, verticalalignment='top') - + correlation[ind_dim, ind_win, ind_el] = np.corrcoef( + A_t[ind_dim, ind_win, ind_el], B_t[ind_dim, ind_win, ind_el] + )[0, 1] + ax.text( + 0.05, + 0.95 - 0.05 * ind_dim, + f"Correlation A_{ind_dim} - B_{ind_dim}: {np.mean(correlation, axis=(1, 2, 3))[ind_dim]:.2f}", + transform=ax.transAxes, + verticalalignment="top", + ) ax.grid(True) - ax.legend(loc='best') - ax.set_xlabel('Time [s]') - ax.set_ylabel(f'Value {key}') + ax.legend(loc="best") + ax.set_xlabel("Time [s]") + ax.set_ylabel(f"Value {key}") # min_val = min([min(A), min(B)]) # max_val = max([max(A), max(B)]) @@ -152,28 +232,38 @@ def plot_fuzzy(ax, name, x, y, chan_centers): tableau_colors = mcolors.TABLEAU_COLORS num_of_colors = len(list(tableau_colors.keys())) for ind in range(len(y)): - ax.axvline(x=chan_centers[ind], color=tableau_colors[list(tableau_colors.keys())[ind % num_of_colors]], - linestyle='--') - ax.plot(x, y[ind], label=f'Channel {int(ind) + 1}', linewidth=2) - ax.legend(loc='best') - ax.set_xlabel('Input') - ax.set_ylabel('Value') - ax.set_title(f'Function {name}') + ax.axvline( + x=chan_centers[ind], + color=tableau_colors[list(tableau_colors.keys())[ind % num_of_colors]], + linestyle="--", + ) + ax.plot(x, y[ind], label=f"Channel {int(ind) + 1}", linewidth=2) + ax.legend(loc="best") + ax.set_xlabel("Input") + ax.set_ylabel("Value") + ax.set_title(f"Function {name}") def plot_3d_function(plt, name, x0, x1, params, output, input_names): fig = plt.figure() # Clear the current plot plt.clf() - ax = fig.add_subplot(111, projection='3d') - ax.plot_surface(np.array(x0), np.array(x1), np.array(output), cmap='viridis') + ax = fig.add_subplot(111, projection="3d") + ax.plot_surface(np.array(x0), np.array(x1), np.array(output), cmap="viridis") ax.set_xlabel(input_names[0]) ax.set_ylabel(input_names[1]) - ax.set_zlabel(f'{name} output') + ax.set_zlabel(f"{name} output") for ind in range(len(input_names) - 2): - fig.text(0.01, 0.9 - 0.05 * ind, f"{input_names[ind + 2]} ={params[ind]}", fontsize=10, color='blue', - style='italic') - plt.title(f'Function {name}') + fig.text( + 0.01, + 0.9 - 0.05 * ind, + f"{input_names[ind + 2]} ={params[ind]}", + fontsize=10, + color="blue", + style="italic", + ) + plt.title(f"Function {name}") + def plot_2d_function(plt, name, x, params, output, input_names): fig = plt.figure() @@ -181,8 +271,14 @@ def plot_2d_function(plt, name, x, params, output, input_names): plt.clf() plt.plot(np.array(x), np.array(output), linewidth=2) plt.xlabel(input_names[0]) - plt.ylabel(f'{name} output') + plt.ylabel(f"{name} output") for ind in range(len(input_names) - 1): - fig.text(0.01, 0.9 - 0.05 * ind, f"{input_names[ind + 1]} ={params[ind]}", fontsize=10, color='blue', - style='italic') - plt.title(f'Function {name}') \ No newline at end of file + fig.text( + 0.01, + 0.9 - 0.05 * ind, + f"{input_names[ind + 1]} ={params[ind]}", + fontsize=10, + color="blue", + style="italic", + ) + plt.title(f"Function {name}") diff --git a/nnodely/__init__.py b/nnodely/__init__.py index 26201190..a08714a0 100644 --- a/nnodely/__init__.py +++ b/nnodely/__init__.py @@ -14,7 +14,15 @@ from nnodely.layers.trigonometric import Sin, Cos, Tan, Cosh, Tanh, Sech from nnodely.layers.parametricfunction import ParamFun from nnodely.layers.fuzzify import Fuzzify -from nnodely.layers.part import Part, Select, Concatenate, SamplePart, SampleSelect, TimePart, TimeConcatenate +from nnodely.layers.part import ( + Part, + Select, + Concatenate, + SamplePart, + SampleSelect, + TimePart, + TimeConcatenate, +) from nnodely.layers.localmodel import LocalModel from nnodely.layers.equationlearner import EquationLearner from nnodely.layers.timeoperation import Integrate, Differentiate @@ -37,43 +45,85 @@ major, minor = sys.version_info.major, sys.version_info.minor logger.LOG_LEVEL = logging.INFO -__version__ = '1.5.4' +__version__ = "1.5.4" if major < 3: - sys.exit("Sorry, Python 2 is not supported. You need Python >= 3.10 for "+__package__+".") + sys.exit( + "Sorry, Python 2 is not supported. You need Python >= 3.10 for " + + __package__ + + "." + ) elif minor < 9: - sys.exit("Sorry, You need Python >= 3.10 for "+__package__+".") + sys.exit("Sorry, You need Python >= 3.10 for " + __package__ + ".") else: - print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>' + - f' {__package__}_v{__version__} '.center(20, '-') + - '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<') + print( + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + + f" {__package__}_v{__version__} ".center(20, "-") + + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + ) __all__ = [ - 'nnodely', 'Modely', 'clearNames', - 'Input', 'Connect', 'ClosedLoop', - 'Parameter', 'Constant', 'SampleTime', - 'Output', - 'Relu', 'ELU', 'Softmax', 'Sigmoid', 'Identity', - 'Fir', - 'Linear', - 'NeuralODE', - 'Add', 'Sum', 'Sub', 'Mul', 'Div', 'Pow', 'Neg', 'Sign', - 'Sin', 'Cos', 'Tan', 'Cosh', 'Tanh', 'Sech', - 'ParamFun', - 'Fuzzify', - 'Part', 'Select', 'Concatenate', - 'SamplePart', 'SampleSelect', - 'TimePart', 'TimeConcatenate', - 'LocalModel', - 'EquationLearner', - 'Integrate', 'Differentiate', - 'Interpolation', - 'ForwardEuler', 'RK2', 'RK4', - 'TextVisualizer', 'MPLVisualizer', 'MPLNotebookVisualizer', - 'StandardExporter', - 'SGD', 'Adam', 'Optimizer', - 'init_negexp', 'init_lin', 'init_constant', 'init_exp', + "nnodely", + "Modely", + "clearNames", + "Input", + "Connect", + "ClosedLoop", + "Parameter", + "Constant", + "SampleTime", + "Output", + "Relu", + "ELU", + "Softmax", + "Sigmoid", + "Identity", + "Fir", + "Linear", + "NeuralODE", + "Add", + "Sum", + "Sub", + "Mul", + "Div", + "Pow", + "Neg", + "Sign", + "Sin", + "Cos", + "Tan", + "Cosh", + "Tanh", + "Sech", + "ParamFun", + "Fuzzify", + "Part", + "Select", + "Concatenate", + "SamplePart", + "SampleSelect", + "TimePart", + "TimeConcatenate", + "LocalModel", + "EquationLearner", + "Integrate", + "Differentiate", + "Interpolation", + "ForwardEuler", + "RK2", + "RK4", + "TextVisualizer", + "MPLVisualizer", + "MPLNotebookVisualizer", + "StandardExporter", + "SGD", + "Adam", + "Optimizer", + "init_negexp", + "init_lin", + "init_constant", + "init_exp", # Main nnodely classes - '__version__' + "__version__", ] diff --git a/nnodely/basic/loss.py b/nnodely/basic/loss.py index 11f46a35..2ec49f3b 100644 --- a/nnodely/basic/loss.py +++ b/nnodely/basic/loss.py @@ -2,26 +2,35 @@ import torch from nnodely.support.utils import check -available_losses = ['mse', 'rmse', 'mae', 'cross_entropy'] +available_losses = ["mse", "rmse", "mae", "cross_entropy"] + class CustomLoss(nn.Module): - def __init__(self, loss_type='mse', **kwargs): + def __init__(self, loss_type="mse", **kwargs): super(CustomLoss, self).__init__() - check(loss_type in available_losses, TypeError, f'The \"{loss_type}\" loss is not available. Possible losses are: {available_losses}.') + check( + loss_type in available_losses, + TypeError, + f'The "{loss_type}" loss is not available. Possible losses are: {available_losses}.', + ) self.loss_type = loss_type self.loss = nn.MSELoss(**kwargs) if callable(loss_type): self.loss = loss_type - elif self.loss_type == 'mae': + elif self.loss_type == "mae": self.loss = nn.L1Loss(**kwargs) - elif self.loss_type == 'cross_entropy': + elif self.loss_type == "cross_entropy": self.loss = nn.CrossEntropyLoss(**kwargs) - + def forward(self, inA, inB): - if self.loss_type == 'cross_entropy': - inB = inB.squeeze().float() if inA.shape == inB.shape else inB.squeeze().long() + if self.loss_type == "cross_entropy": + inB = ( + inB.squeeze().float() + if inA.shape == inB.shape + else inB.squeeze().long() + ) inA = inA.squeeze() - res = self.loss(inA,inB) - if self.loss_type == 'rmse': + res = self.loss(inA, inB) + if self.loss_type == "rmse": res = torch.sqrt(res) - return res \ No newline at end of file + return res diff --git a/nnodely/basic/model.py b/nnodely/basic/model.py index 81f66bd1..4937bd9e 100644 --- a/nnodely/basic/model.py +++ b/nnodely/basic/model.py @@ -9,34 +9,46 @@ from nnodely.support.utils import TORCH_DTYPE from nnodely.support import initializer + @torch.fx.wrap def update_state(data_in, rel): - #virtual = torch.roll(data_in, shifts=-1, dims=1) + # virtual = torch.roll(data_in, shifts=-1, dims=1) max_dim = min(rel.size(1), data_in.size(1)) data_out = data_in.clone() data_out[:, -max_dim:, :] = rel[:, -max_dim:, :] return data_out + class Model(nn.Module): def __init__(self, model_def): super(Model, self).__init__() model_def = copy.deepcopy(model_def) - self.states = {key: value for key, value in model_def['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())} + self.states = { + key: value + for key, value in model_def["Inputs"].items() + if ("closedLoop" in value.keys() or "connect" in value.keys()) + } - self.inputs = model_def['Inputs'] - self.outputs = model_def['Outputs'] - self.relations = model_def['Relations'] - self.params = model_def['Parameters'] - self.constants = model_def['Constants'] - self.sample_time = model_def['Info']['SampleTime'] - self.functions = model_def['Functions'] + self.inputs = model_def["Inputs"] + self.outputs = model_def["Outputs"] + self.relations = model_def["Relations"] + self.params = model_def["Parameters"] + self.constants = model_def["Constants"] + self.sample_time = model_def["Info"]["SampleTime"] + self.functions = model_def["Functions"] - self.minimizers = model_def['Minimizers'] if 'Minimizers' in model_def else {} - self.minimizers_keys = [self.minimizers[key]['A'] for key in self.minimizers] + [self.minimizers[key]['B'] for key in self.minimizers] + self.minimizers = model_def["Minimizers"] if "Minimizers" in model_def else {} + self.minimizers_keys = [ + self.minimizers[key]["A"] for key in self.minimizers + ] + [self.minimizers[key]["B"] for key in self.minimizers] - self.input_ns_backward = {key:value['ns'][0] for key, value in model_def['Inputs'].items()} - self.input_n_samples = {key:value['ntot'] for key, value in model_def['Inputs'].items()} + self.input_ns_backward = { + key: value["ns"][0] for key, value in model_def["Inputs"].items() + } + self.input_n_samples = { + key: value["ntot"] for key, value in model_def["Inputs"].items() + } ## Build the network self.all_parameters = {} @@ -51,55 +63,89 @@ def __init__(self, model_def): ## Define the correct slicing for _, items in self.relations.items(): - if items[0] == 'SamplePart': + if items[0] == "SamplePart": if items[1][0] in self.inputs.keys(): items[3][0] = self.input_ns_backward[items[1][0]] + items[3][0] items[3][1] = self.input_ns_backward[items[1][0]] + items[3][1] - if len(items) > 4: ## Offset + if len(items) > 4: ## Offset items[4] = self.input_ns_backward[items[1][0]] + items[4] - if items[0] == 'TimePart': + if items[0] == "TimePart": if items[1][0] in self.inputs.keys(): - items[3][0] = self.input_ns_backward[items[1][0]] + round(items[3][0]/self.sample_time) - items[3][1] = self.input_ns_backward[items[1][0]] + round(items[3][1]/self.sample_time) - if len(items) > 4: ## Offset - items[4] = self.input_ns_backward[items[1][0]] + round(items[4]/self.sample_time) + items[3][0] = self.input_ns_backward[items[1][0]] + round( + items[3][0] / self.sample_time + ) + items[3][1] = self.input_ns_backward[items[1][0]] + round( + items[3][1] / self.sample_time + ) + if len(items) > 4: ## Offset + items[4] = self.input_ns_backward[items[1][0]] + round( + items[4] / self.sample_time + ) else: - items[3][0] = round(items[3][0]/self.sample_time) - items[3][1] = round(items[3][1]/self.sample_time) - if len(items) > 4: ## Offset - items[4] = round(items[4]/self.sample_time) + items[3][0] = round(items[3][0] / self.sample_time) + items[3][1] = round(items[3][1] / self.sample_time) + if len(items) > 4: ## Offset + items[4] = round(items[4] / self.sample_time) ## Create all the parameters for name, param_data in self.params.items(): - window = 'tw' if 'tw' in param_data.keys() else ('sw' if 'sw' in param_data.keys() else None) - aux_sample_time = self.sample_time if 'tw' == window else 1 - sample_window = round(param_data[window] / aux_sample_time) if window else None + window = ( + "tw" + if "tw" in param_data.keys() + else ("sw" if "sw" in param_data.keys() else None) + ) + aux_sample_time = self.sample_time if "tw" == window else 1 + sample_window = ( + round(param_data[window] / aux_sample_time) if window else None + ) if sample_window is None: - param_size = tuple(param_data['dim']) if type(param_data['dim']) is list else (param_data['dim'],) + param_size = ( + tuple(param_data["dim"]) + if type(param_data["dim"]) is list + else (param_data["dim"],) + ) else: - param_size = (sample_window,)+tuple(param_data['dim']) if type(param_data['dim']) is list else (sample_window, param_data['dim']) - if 'values' in param_data: - self.all_parameters[name] = nn.Parameter(torch.tensor(param_data['values'], dtype=TORCH_DTYPE), requires_grad=True) + param_size = ( + (sample_window,) + tuple(param_data["dim"]) + if type(param_data["dim"]) is list + else (sample_window, param_data["dim"]) + ) + if "values" in param_data: + self.all_parameters[name] = nn.Parameter( + torch.tensor(param_data["values"], dtype=TORCH_DTYPE), + requires_grad=True, + ) # TODO clean code - elif 'init_fun' in param_data: - if 'code' in param_data['init_fun'].keys(): - exec(param_data['init_fun']['code'], globals()) - function_to_call = globals()[param_data['init_fun']['name']] + elif "init_fun" in param_data: + if "code" in param_data["init_fun"].keys(): + exec(param_data["init_fun"]["code"], globals()) + function_to_call = globals()[param_data["init_fun"]["name"]] else: - function_to_call = getattr(initializer, param_data['init_fun']['name']) + function_to_call = getattr( + initializer, param_data["init_fun"]["name"] + ) values = np.zeros(param_size) for indexes in product(*(range(v) for v in param_size)): - if 'params' in param_data['init_fun']: - values[indexes] = function_to_call(indexes, param_size, param_data['init_fun']['params']) + if "params" in param_data["init_fun"]: + values[indexes] = function_to_call( + indexes, param_size, param_data["init_fun"]["params"] + ) else: values[indexes] = function_to_call(indexes, param_size) - self.all_parameters[name] = nn.Parameter(torch.tensor(values.tolist(), dtype=TORCH_DTYPE), requires_grad=True) + self.all_parameters[name] = nn.Parameter( + torch.tensor(values.tolist(), dtype=TORCH_DTYPE), requires_grad=True + ) else: - self.all_parameters[name] = nn.Parameter(torch.rand(size=param_size, dtype=TORCH_DTYPE), requires_grad=True) + self.all_parameters[name] = nn.Parameter( + torch.rand(size=param_size, dtype=TORCH_DTYPE), requires_grad=True + ) ## Create all the constants for name, param_data in self.constants.items(): - self.all_constants[name] = nn.Parameter(torch.tensor(param_data['values'], dtype=TORCH_DTYPE), requires_grad=False) + self.all_constants[name] = nn.Parameter( + torch.tensor(param_data["values"], dtype=TORCH_DTYPE), + requires_grad=False, + ) all_params_and_consts = self.all_parameters | self.all_constants ## Create all the relations @@ -107,27 +153,41 @@ def __init__(self, model_def): ## Take the relation name and the inputs needed to solve the relation rel_name, input_var = inputs[0], inputs[1] ## Create All the Relations - func = getattr(self,rel_name) + func = getattr(self, rel_name) if func: layer_inputs = [] for item in inputs[2:]: - if item in list(self.params.keys()): ## the relation takes parameters + if item in list( + self.params.keys() + ): ## the relation takes parameters layer_inputs.append(self.all_parameters[item]) - elif item in list(self.constants.keys()): ## the relation takes a constant + elif item in list( + self.constants.keys() + ): ## the relation takes a constant layer_inputs.append(self.all_constants[item]) - elif item in list(self.functions.keys()): ## the relation takes a custom function + elif item in list( + self.functions.keys() + ): ## the relation takes a custom function layer_inputs.append(self.functions[item]) - if 'params_and_consts' in self.functions[item].keys() and len(self.functions[item]['params_and_consts']) >= 0: ## Parametric function that takes parameters - layer_inputs.append([all_params_and_consts[par] for par in self.functions[item]['params_and_consts']]) - if 'map_over_dim' in self.functions[item].keys(): - layer_inputs.append(self.functions[item]['map_over_dim']) + if ( + "params_and_consts" in self.functions[item].keys() + and len(self.functions[item]["params_and_consts"]) >= 0 + ): ## Parametric function that takes parameters + layer_inputs.append( + [ + all_params_and_consts[par] + for par in self.functions[item]["params_and_consts"] + ] + ) + if "map_over_dim" in self.functions[item].keys(): + layer_inputs.append(self.functions[item]["map_over_dim"]) else: layer_inputs.append(item) - if rel_name == 'SamplePart': + if rel_name == "SamplePart": if layer_inputs[0] == -1: layer_inputs[0] = self.input_n_samples[input_var[0]] - elif rel_name == 'TimePart': + elif rel_name == "TimePart": if layer_inputs[0] == -1: layer_inputs[0] = self.input_n_samples[input_var[0]] else: @@ -145,60 +205,96 @@ def __init__(self, model_def): self.network_output_predictions = set(self.outputs.values()) ## list of network minimization outputs self.network_output_minimizers = [] - for _,value in self.minimizers.items(): - self.network_output_minimizers.append(self.outputs[value['A']]) if value['A'] in self.outputs.keys() else self.network_output_minimizers.append(value['A']) - self.network_output_minimizers.append(self.outputs[value['B']]) if value['B'] in self.outputs.keys() else self.network_output_minimizers.append(value['B']) + for _, value in self.minimizers.items(): + self.network_output_minimizers.append(self.outputs[value["A"]]) if value[ + "A" + ] in self.outputs.keys() else self.network_output_minimizers.append( + value["A"] + ) + self.network_output_minimizers.append(self.outputs[value["B"]]) if value[ + "B" + ] in self.outputs.keys() else self.network_output_minimizers.append( + value["B"] + ) self.network_output_minimizers = set(self.network_output_minimizers) ## list of all the network Outputs - self.network_outputs = self.network_output_predictions.union(self.network_output_minimizers) + self.network_outputs = self.network_output_predictions.union( + self.network_output_minimizers + ) def forward(self, kwargs): result_dict = {} ## Initially i have only the inputs from the dataset, the parameters, and the constants - available_inputs = [key for key in self.inputs.keys() if key not in self.connect_update.keys()] ## remove connected inputs - available_keys = set(available_inputs + list(self.all_parameters.keys()) + list(self.all_constants.keys())) + available_inputs = [ + key for key in self.inputs.keys() if key not in self.connect_update.keys() + ] ## remove connected inputs + available_keys = set( + available_inputs + + list(self.all_parameters.keys()) + + list(self.all_constants.keys()) + ) ## Forward pass through the relations - while not self.network_outputs.issubset(available_keys): ## i need to climb the relation tree until i get all the outputs + while not self.network_outputs.issubset( + available_keys + ): ## i need to climb the relation tree until i get all the outputs for relation in self.relations.keys(): ## if i have all the variables i can calculate the relation - if set(self.relation_inputs[relation]).issubset(available_keys) and (relation not in available_keys): + if set(self.relation_inputs[relation]).issubset(available_keys) and ( + relation not in available_keys + ): ## Collect all the necessary inputs for the relation layer_inputs = [] for key in self.relation_inputs[relation]: - if key in self.all_constants.keys(): ## relation that takes a constant + if ( + key in self.all_constants.keys() + ): ## relation that takes a constant layer_inputs.append(self.all_constants[key]) elif key in available_inputs: ## relation that takes inputs layer_inputs.append(kwargs[key]) - elif key in self.all_parameters.keys(): ## relation that takes parameters + elif ( + key in self.all_parameters.keys() + ): ## relation that takes parameters layer_inputs.append(self.all_parameters[key]) - else: ## relation than takes another relation or a connect variable + else: ## relation than takes another relation or a connect variable layer_inputs.append(result_dict[key]) ## Execute the current relation - result_dict[relation] = self.relation_forward[relation](*layer_inputs) + result_dict[relation] = self.relation_forward[relation]( + *layer_inputs + ) available_keys.add(relation) ## Check if the relation is inside the connect for connect_input, connect_rel in self.connect_update.items(): if relation == connect_rel: - result_dict[connect_input] = update_state(kwargs[connect_input], result_dict[relation]) + result_dict[connect_input] = update_state( + kwargs[connect_input], result_dict[relation] + ) available_keys.add(connect_input) ## Return a dictionary with all the connected inputs - connect_update_dict = {key: result_dict[key] for key in self.connect_update.keys()} + connect_update_dict = { + key: result_dict[key] for key in self.connect_update.keys() + } ## Return a dictionary with all the relations that updates the state variables - closed_loop_update_dict = {key: result_dict[value] for key, value in self.closed_loop_update.items()} + closed_loop_update_dict = { + key: result_dict[value] for key, value in self.closed_loop_update.items() + } ## Return a dictionary with all the outputs final values output_dict = {key: result_dict[value] for key, value in self.outputs.items()} ## Return a dictionary with the minimization relations minimize_dict = {} for key in self.minimizers_keys: - minimize_dict[key] = result_dict[self.outputs[key]] if key in self.outputs.keys() else result_dict[key] + minimize_dict[key] = ( + result_dict[self.outputs[key]] + if key in self.outputs.keys() + else result_dict[key] + ) return output_dict, minimize_dict, closed_loop_update_dict, connect_update_dict - def update(self, *, closed_loop = {}, connect = {}, disconnect = False): + def update(self, *, closed_loop={}, connect={}, disconnect=False): self.closed_loop_update = {} self.connect_update = {} @@ -206,15 +302,23 @@ def update(self, *, closed_loop = {}, connect = {}, disconnect = False): return for key, state in self.states.items(): - if 'connect' in state.keys(): - self.connect_update[key] = state['connect'] - elif 'closedLoop' in state.keys(): - self.closed_loop_update[key] = state['closedLoop'] + if "connect" in state.keys(): + self.connect_update[key] = state["connect"] + elif "closedLoop" in state.keys(): + self.closed_loop_update[key] = state["closedLoop"] # Get relation from outputs for connect_in, connect_rel in connect.items(): - set_relation = self.outputs[connect_rel] if connect_rel in self.outputs.keys() else connect_rel + set_relation = ( + self.outputs[connect_rel] + if connect_rel in self.outputs.keys() + else connect_rel + ) self.connect_update[connect_in] = set_relation for close_in, close_rel in closed_loop.items(): - set_relation = self.outputs[close_rel] if close_rel in self.outputs.keys() else close_rel + set_relation = ( + self.outputs[close_rel] + if close_rel in self.outputs.keys() + else close_rel + ) self.closed_loop_update[close_in] = set_relation diff --git a/nnodely/basic/modeldef.py b/nnodely/basic/modeldef.py index 438771c1..9c9e4956 100644 --- a/nnodely/basic/modeldef.py +++ b/nnodely/basic/modeldef.py @@ -3,24 +3,31 @@ import numpy as np from nnodely.support.utils import check, check_and_get_list -from nnodely.support.jsonutils import merge, subjson_from_model, subjson_from_minimize, check_model, get_models_json +from nnodely.support.jsonutils import ( + merge, + subjson_from_model, + subjson_from_minimize, + check_model, + get_models_json, +) from nnodely.basic.relation import MAIN_JSON, Stream, check_names from nnodely.layers.output import Output from nnodely.layers.input import Input from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.INFO) class ModelDef: - def __init__(self, model_def = MAIN_JSON): + def __init__(self, model_def=MAIN_JSON): # Models definition self.__json_base = copy.deepcopy(model_def) # Initialize the model definition self.__json = copy.deepcopy(self.__json_base) - if "SampleTime" in self.__json['Info']: - self.__sample_time = self.__json['Info']["SampleTime"] + if "SampleTime" in self.__json["Info"]: + self.__sample_time = self.__json["Info"]["SampleTime"] else: self.__sample_time = None @@ -38,15 +45,19 @@ def __setitem__(self, key, value): def __rebuild_json(self, models_names, minimizers): models_json = subjson_from_model(self.__json, list(models_names)) - if 'Minimizers' in self.__json and len(minimizers) > 0: + if "Minimizers" in self.__json and len(minimizers) > 0: minimizers_json = subjson_from_minimize(self.__json, list(minimizers)) models_json = merge(models_json, minimizers_json) return copy.deepcopy(models_json) def recurrentInputs(self): - return {key:value for key, value in self.__json['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())} + return { + key: value + for key, value in self.__json["Inputs"].items() + if ("closedLoop" in value.keys() or "connect" in value.keys()) + } - def getJson(self, models:list|str|None = None) -> dict: + def getJson(self, models: list | str | None = None) -> dict: if models is None: return copy.deepcopy(self.__json) else: @@ -55,47 +66,76 @@ def getJson(self, models:list|str|None = None) -> dict: return copy.deepcopy(json) def getSampleTime(self): - check(self.__sample_time is not None, AttributeError, "Sample time is not defined the model is not neuralized!") + check( + self.__sample_time is not None, + AttributeError, + "Sample time is not defined the model is not neuralized!", + ) return self.__sample_time def isDefined(self): return self.__json is not None - def addConnection(self, stream_out:str|Output|Stream, input_in:str|Input, type:str, local:bool = False): - outputs = self.__json['Outputs'] + def addConnection( + self, + stream_out: str | Output | Stream, + input_in: str | Input, + type: str, + local: bool = False, + ): + outputs = self.__json["Outputs"] if isinstance(stream_out, (Output, Stream)): - stream_name = outputs[stream_out.name] if stream_out.name in outputs.keys() else stream_out.name + stream_name = ( + outputs[stream_out.name] + if stream_out.name in outputs.keys() + else stream_out.name + ) else: - output_name = check_and_get_list(stream_out, set(outputs.keys()), - lambda name: f"The name {name} is not part of the available Outputs")[0] + output_name = check_and_get_list( + stream_out, + set(outputs.keys()), + lambda name: f"The name {name} is not part of the available Outputs", + )[0] stream_name = outputs[output_name] if isinstance(input_in, Input): input_name = input_in.name else: - input_name = input_in #TODO Add tests - - input_name = check_and_get_list(input_name, set(self.__json['Inputs'].keys()), - lambda name: f"The name {name} is not part of the available Inputs")[0] - stream_name = check_and_get_list(stream_name, set(self.__json['Relations'].keys()), - lambda name: f"The name {name} is not part of the available Relations")[0] - self.__json['Inputs'][input_name][type] = stream_name - self.__json['Inputs'][input_name]['local'] = int(local) - - def removeConnection(self, name_list:str|list[str]): - name_list = check_and_get_list(name_list, set(self.__json['Inputs'].keys()), lambda name: f"The name {name} is not part of the available Inputs") + input_name = input_in # TODO Add tests + + input_name = check_and_get_list( + input_name, + set(self.__json["Inputs"].keys()), + lambda name: f"The name {name} is not part of the available Inputs", + )[0] + stream_name = check_and_get_list( + stream_name, + set(self.__json["Relations"].keys()), + lambda name: f"The name {name} is not part of the available Relations", + )[0] + self.__json["Inputs"][input_name][type] = stream_name + self.__json["Inputs"][input_name]["local"] = int(local) + + def removeConnection(self, name_list: str | list[str]): + name_list = check_and_get_list( + name_list, + set(self.__json["Inputs"].keys()), + lambda name: f"The name {name} is not part of the available Inputs", + ) for input_in in name_list: - if 'closedLoop' in self.__json['Inputs'][input_in].keys(): - del self.__json['Inputs'][input_in]['closedLoop'] - del self.__json['Inputs'][input_in]['local'] - elif 'connect' in self.__json['Inputs'][input_in].keys(): - del self.__json['Inputs'][input_in]['connect'] - del self.__json['Inputs'][input_in]['local'] + if "closedLoop" in self.__json["Inputs"][input_in].keys(): + del self.__json["Inputs"][input_in]["closedLoop"] + del self.__json["Inputs"][input_in]["local"] + elif "connect" in self.__json["Inputs"][input_in].keys(): + del self.__json["Inputs"][input_in]["connect"] + del self.__json["Inputs"][input_in]["local"] else: - raise ValueError(f"The input '{input_in}' has no connection or closed loop defined") + raise ValueError( + f"The input '{input_in}' has no connection or closed loop defined" + ) - def addModel(self, name:str, stream_list): + def addModel(self, name: str, stream_list): if isinstance(stream_list, Output): stream_list = [stream_list] @@ -104,127 +144,202 @@ def addModel(self, name:str, stream_list): json = merge(json, stream.json) check_model(json) - if 'Models' not in self.__json: + if "Models" not in self.__json: self.__json = merge(self.__json, json) - self.__json['Models'] = name + self.__json["Models"] = name else: - models_names = set((self.__json['Models'],)) if type(self.__json['Models']) is str else set(self.__json['Models'].keys()) - check_names(name, models_names, 'Models') - if type(self.__json['Models']) is str: - self.__json['Models'] = {self.__json['Models']: get_models_json(self.__json)} + models_names = ( + set((self.__json["Models"],)) + if type(self.__json["Models"]) is str + else set(self.__json["Models"].keys()) + ) + check_names(name, models_names, "Models") + if type(self.__json["Models"]) is str: + self.__json["Models"] = { + self.__json["Models"]: get_models_json(self.__json) + } self.__json = merge(self.__json, json) - self.__json['Models'][name] = get_models_json(json) + self.__json["Models"][name] = get_models_json(json) def removeModel(self, name_list): - if 'Models' not in self.__json: + if "Models" not in self.__json: raise ValueError("No Models are defined") - models_names = {self.__json['Models']} if type(self.__json['Models']) is str else set(self.__json['Models'].keys()) - name_list = check_and_get_list(name_list, models_names, lambda name: f"The name {name} is not part of the available models") + models_names = ( + {self.__json["Models"]} + if type(self.__json["Models"]) is str + else set(self.__json["Models"].keys()) + ) + name_list = check_and_get_list( + name_list, + models_names, + lambda name: f"The name {name} is not part of the available models", + ) models_names -= set(name_list) - minimizers = set(self.__json['Minimizers'].keys()) if 'Minimizers' in self.__json else None + minimizers = ( + set(self.__json["Minimizers"].keys()) + if "Minimizers" in self.__json + else None + ) self.__json = self.__rebuild_json(models_names, minimizers) - def addMinimize(self, name, streamA, streamB, loss_function='mse'): - if 'Minimizers' not in self.__json: - self.__json['Minimizers'] = {} - check_names(name, set(self.__json['Minimizers'].keys()), 'Minimizers') + def addMinimize(self, name, streamA, streamB, loss_function="mse"): + if "Minimizers" not in self.__json: + self.__json["Minimizers"] = {} + check_names(name, set(self.__json["Minimizers"].keys()), "Minimizers") if isinstance(streamA, str): streamA_name = streamA else: - check(isinstance(streamA, (Output, Stream)), TypeError, 'streamA must be an instance of Output or Stream') - streamA_name = streamA.json['Outputs'][streamA.name] if isinstance(streamA, Output) else streamA.name + check( + isinstance(streamA, (Output, Stream)), + TypeError, + "streamA must be an instance of Output or Stream", + ) + streamA_name = ( + streamA.json["Outputs"][streamA.name] + if isinstance(streamA, Output) + else streamA.name + ) self.__json = merge(self.__json, streamA.json) if isinstance(streamB, str): streamB_name = streamB else: - check(isinstance(streamB, (Output, Stream)), TypeError, 'streamA must be an instance of Output or Stream') - streamB_name = streamB.json['Outputs'][streamB.name] if isinstance(streamB, Output) else streamB.name + check( + isinstance(streamB, (Output, Stream)), + TypeError, + "streamA must be an instance of Output or Stream", + ) + streamB_name = ( + streamB.json["Outputs"][streamB.name] + if isinstance(streamB, Output) + else streamB.name + ) self.__json = merge(self.__json, streamB.json) - #check(streamA.dim == streamB.dim, ValueError, f'Dimension of streamA={streamA.dim} and streamB={streamB.dim} are not equal.') + # check(streamA.dim == streamB.dim, ValueError, f'Dimension of streamA={streamA.dim} and streamB={streamB.dim} are not equal.') - self.__json['Minimizers'][name] = {} - self.__json['Minimizers'][name]['A'] = streamA_name - self.__json['Minimizers'][name]['B'] = streamB_name - self.__json['Minimizers'][name]['loss'] = loss_function + self.__json["Minimizers"][name] = {} + self.__json["Minimizers"][name]["A"] = streamA_name + self.__json["Minimizers"][name]["B"] = streamB_name + self.__json["Minimizers"][name]["loss"] = loss_function def removeMinimize(self, name_list): - if 'Minimizers' not in self.__json: + if "Minimizers" not in self.__json: raise ValueError("No Minimizers are defined") - name_list = check_and_get_list(name_list, self.__json['Minimizers'].keys(), lambda name: f"The name {name} is not part of the available minimizers") - models_names = {self.__json['Models']} if type(self.__json['Models']) is str else set(self.__json['Models'].keys()) - remaining_minimizers = set(self.__json['Minimizers'].keys()) - set(name_list) if 'Minimizers' in self.__json else None + name_list = check_and_get_list( + name_list, + self.__json["Minimizers"].keys(), + lambda name: f"The name {name} is not part of the available minimizers", + ) + models_names = ( + {self.__json["Models"]} + if type(self.__json["Models"]) is str + else set(self.__json["Models"].keys()) + ) + remaining_minimizers = ( + set(self.__json["Minimizers"].keys()) - set(name_list) + if "Minimizers" in self.__json + else None + ) self.__json = self.__rebuild_json(models_names, remaining_minimizers) - def setBuildWindow(self, sample_time = None): + def setBuildWindow(self, sample_time=None): check(self.__json is not None, RuntimeError, "No model is defined!") if sample_time is not None: - check(sample_time > 0, RuntimeError, 'Sample time must be strictly positive!') + check( + sample_time > 0, RuntimeError, "Sample time must be strictly positive!" + ) self.__sample_time = sample_time else: if self.__sample_time is None: self.__sample_time = 1 - self.__json['Info'] = {"SampleTime": self.__sample_time} - if 'SampleTime' in self.__json['Constants']: - self.__json['Constants']['SampleTime'] = {'dim': 1, 'values': self.__sample_time} + self.__json["Info"] = {"SampleTime": self.__sample_time} + if "SampleTime" in self.__json["Constants"]: + self.__json["Constants"]["SampleTime"] = { + "dim": 1, + "values": self.__sample_time, + } - check(self.__json['Inputs'] != {}, RuntimeError, "No model is defined!") - json_inputs = self.__json['Inputs'] + check(self.__json["Inputs"] != {}, RuntimeError, "No model is defined!") + json_inputs = self.__json["Inputs"] input_ns_backward, input_ns_forward = {}, {} for key, value in json_inputs.items(): - if 'sw' not in value and 'tw' not in value: + if "sw" not in value and "tw" not in value: assert False, f"Input '{key}' has no time window or sample window" - if 'sw' not in value and self.__sample_time is not None: + if "sw" not in value and self.__sample_time is not None: ## check if value['tw'] is a multiple of sample_time - absolute_tw = abs(value['tw'][0]) + abs(value['tw'][1]) - check(round(absolute_tw % self.__sample_time) == 0, ValueError, - f"Time window of input '{key}' is not a multiple of sample time. This network cannot be neuralized") - input_ns_backward[key] = round(-value['tw'][0] / self.__sample_time) - input_ns_forward[key] = round(value['tw'][1] / self.__sample_time) + absolute_tw = abs(value["tw"][0]) + abs(value["tw"][1]) + check( + round(absolute_tw % self.__sample_time) == 0, + ValueError, + f"Time window of input '{key}' is not a multiple of sample time. This network cannot be neuralized", + ) + input_ns_backward[key] = round(-value["tw"][0] / self.__sample_time) + input_ns_forward[key] = round(value["tw"][1] / self.__sample_time) elif self.__sample_time is not None: - if 'tw' in value: - input_ns_backward[key] = max(round(-value['tw'][0] / self.__sample_time), -value['sw'][0]) - input_ns_forward[key] = max(round(value['tw'][1] / self.__sample_time), value['sw'][1]) + if "tw" in value: + input_ns_backward[key] = max( + round(-value["tw"][0] / self.__sample_time), -value["sw"][0] + ) + input_ns_forward[key] = max( + round(value["tw"][1] / self.__sample_time), value["sw"][1] + ) else: - input_ns_backward[key] = -value['sw'][0] - input_ns_forward[key] = value['sw'][1] + input_ns_backward[key] = -value["sw"][0] + input_ns_forward[key] = value["sw"][1] else: - check(value['tw'] == [0,0], RuntimeError, f"Sample time is not defined for input '{key}'") - input_ns_backward[key] = -value['sw'][0] - input_ns_forward[key] = value['sw'][1] - value['ns'] = [input_ns_backward[key], input_ns_forward[key]] - value['ntot'] = sum(value['ns']) - - self.__json['Info']['ns'] = [max(input_ns_backward.values()), max(input_ns_forward.values())] - self.__json['Info']['ntot'] = sum(self.__json['Info']['ns']) - if self.__json['Info']['ns'][0] < 0: + check( + value["tw"] == [0, 0], + RuntimeError, + f"Sample time is not defined for input '{key}'", + ) + input_ns_backward[key] = -value["sw"][0] + input_ns_forward[key] = value["sw"][1] + value["ns"] = [input_ns_backward[key], input_ns_forward[key]] + value["ntot"] = sum(value["ns"]) + + self.__json["Info"]["ns"] = [ + max(input_ns_backward.values()), + max(input_ns_forward.values()), + ] + self.__json["Info"]["ntot"] = sum(self.__json["Info"]["ns"]) + if self.__json["Info"]["ns"][0] < 0: log.warning( - f"The input is only in the far past the max_samples_backward is: {self.__json['Info']['ns'][0]}") - if self.__json['Info']['ns'][1] < 0: + f"The input is only in the far past the max_samples_backward is: {self.__json['Info']['ns'][0]}" + ) + if self.__json["Info"]["ns"][1] < 0: log.warning( - f"The input is only in the far future the max_sample_forward is: {self.__json['Info']['ns'][1]}") - - for k, v in (self.__json['Parameters'] | self.__json['Constants']).items(): - if 'values' in v: - window = 'tw' if 'tw' in v.keys() else ('sw' if 'sw' in v.keys() else None) - if window == 'tw': - check(np.array(v['values']).shape[0] == v['tw'] / self.__sample_time, ValueError, - f"{k} has a different number of values for this sample time.") - if v['values'] == "SampleTime": - v['values'] = self.__sample_time - - def updateParameters(self, model = None, *, clear_model = False): + f"The input is only in the far future the max_sample_forward is: {self.__json['Info']['ns'][1]}" + ) + + for k, v in (self.__json["Parameters"] | self.__json["Constants"]).items(): + if "values" in v: + window = ( + "tw" if "tw" in v.keys() else ("sw" if "sw" in v.keys() else None) + ) + if window == "tw": + check( + np.array(v["values"]).shape[0] == v["tw"] / self.__sample_time, + ValueError, + f"{k} has a different number of values for this sample time.", + ) + if v["values"] == "SampleTime": + v["values"] = self.__sample_time + + def updateParameters(self, model=None, *, clear_model=False): if clear_model: - for key in self.__json['Parameters'].keys(): - if 'init_values' in self.__json['Parameters'][key]: - self.__json['Parameters'][key]['values'] = self.__json['Parameters'][key]['init_values'] - elif 'values' in self.__json['Parameters'][key]: - del self.__json['Parameters'][key]['values'] + for key in self.__json["Parameters"].keys(): + if "init_values" in self.__json["Parameters"][key]: + self.__json["Parameters"][key]["values"] = self.__json[ + "Parameters" + ][key]["init_values"] + elif "values" in self.__json["Parameters"][key]: + del self.__json["Parameters"][key]["values"] elif model is not None: - for key in self.__json['Parameters'].keys(): + for key in self.__json["Parameters"].keys(): if key in model.all_parameters: - self.__json['Parameters'][key]['values'] = model.all_parameters[key].tolist() - + self.__json["Parameters"][key]["values"] = model.all_parameters[ + key + ].tolist() diff --git a/nnodely/basic/optimizer.py b/nnodely/basic/optimizer.py index 15889697..c800e022 100644 --- a/nnodely/basic/optimizer.py +++ b/nnodely/basic/optimizer.py @@ -3,6 +3,7 @@ from nnodely.support.utils import check + class Optimizer: """ Represents an optimizer for training neural network models. @@ -29,7 +30,8 @@ class Optimizer: params_to_train : list or None A list of parameters to be trained. """ - def __init__(self, name, optimizer_defaults = {}, optimizer_params = []): + + def __init__(self, name, optimizer_defaults={}, optimizer_params=[]): """ Initializes the Optimizer object. @@ -64,9 +66,9 @@ def set_params_to_train(self, all_params, params_to_train): if self.optimizer_params == []: for param_name in self.all_params.keys(): if param_name in self.params_to_train: - self.optimizer_params.append({'params': param_name}) + self.optimizer_params.append({"params": param_name}) else: - self.optimizer_params.append({'params': param_name, 'lr': 0.0}) + self.optimizer_params.append({"params": param_name, "lr": 0.0}) def set_defaults(self, optimizer_defaults): """ @@ -110,18 +112,18 @@ def unfold(self, params): If the params argument is not a list. """ optimizer_params = [] - check(type(params) is list, KeyError, f'The params {params} must be a list') + check(type(params) is list, KeyError, f"The params {params} must be a list") for param in params: - if type(param['params']) is list: + if type(param["params"]) is list: par_copy = copy.deepcopy(param) - del par_copy['params'] - for par in param['params']: - optimizer_params.append({'params':par}|par_copy) + del par_copy["params"] + for par in param["params"]: + optimizer_params.append({"params": par} | par_copy) else: optimizer_params.append(param) return optimizer_params - def add_defaults(self, option_name, params, overwrite = True): + def add_defaults(self, option_name, params, overwrite=True): """ Adds default settings to the optimizer. @@ -140,41 +142,46 @@ def add_defaults(self, option_name, params, overwrite = True): elif option_name not in self.optimizer_defaults: self.optimizer_defaults[option_name] = params - def add_option_to_params(self, option_name, params, overwrite = True): + def add_option_to_params(self, option_name, params, overwrite=True): if params is None: return for key, value in params.items(): - check(self.all_params is not None, RuntimeError, "Call set_params before add_option_to_params") + check( + self.all_params is not None, + RuntimeError, + "Call set_params before add_option_to_params", + ) old_key = False for param in self.optimizer_params: - if param['params'] == key: + if param["params"] == key: old_key = True if overwrite: param[option_name] = value elif option_name not in param: param[option_name] = value if old_key == False: - self.optimizer_params.append({'params': key, option_name: value}) + self.optimizer_params.append({"params": key, option_name: value}) def replace_key_with_params(self): params = copy.deepcopy(self.optimizer_params) for param in params: - if type(param['params']) is list: - for ind, par in enumerate(param['params']): - param['params'][ind] = self.all_params[par] + if type(param["params"]) is list: + for ind, par in enumerate(param["params"]): + param["params"][ind] = self.all_params[par] else: - param['params'] = self.all_params[param['params']] + param["params"] = self.all_params[param["params"]] return params def get_torch_optimizer(self): - raise NotImplemented('The function get_torch_optimizer must be implemented.') + raise NotImplemented("The function get_torch_optimizer must be implemented.") + class SGD(Optimizer): """ Stochastic Gradient Descent (SGD) optimizer. See also: - Official PyTorch SGD documentation: + Official PyTorch SGD documentation: `torch.optim.SGD `_ Parameters @@ -201,18 +208,22 @@ class SGD(Optimizer): nesterov : bool, optional Enables Nesterov momentum. Default is False. """ - def __init__(self, optimizer_defaults = {}, optimizer_params = []): - super(SGD, self).__init__('SGD', optimizer_defaults, optimizer_params) + + def __init__(self, optimizer_defaults={}, optimizer_params=[]): + super(SGD, self).__init__("SGD", optimizer_defaults, optimizer_params) def get_torch_optimizer(self): - return torch.optim.SGD(self.replace_key_with_params(), **self.optimizer_defaults) + return torch.optim.SGD( + self.replace_key_with_params(), **self.optimizer_defaults + ) + class Adam(Optimizer): """ Stochastic Gradient Descent (SGD) optimizer. See also: - Official PyTorch Adam documentation: + Official PyTorch Adam documentation: `torch.optim.Adam `_ Parameters @@ -239,8 +250,11 @@ class Adam(Optimizer): amsgrad : bool, optional Whether to use the AMSGrad variant of this algorithm. Default is False. """ - def __init__(self, optimizer_defaults = {}, optimizer_params = []): - super(Adam, self).__init__('Adam', optimizer_defaults, optimizer_params) + + def __init__(self, optimizer_defaults={}, optimizer_params=[]): + super(Adam, self).__init__("Adam", optimizer_defaults, optimizer_params) def get_torch_optimizer(self): - return torch.optim.Adam(self.replace_key_with_params(), **self.optimizer_defaults) \ No newline at end of file + return torch.optim.Adam( + self.replace_key_with_params(), **self.optimizer_defaults + ) diff --git a/nnodely/basic/relation.py b/nnodely/basic/relation.py index b99a7d50..8e4e2011 100644 --- a/nnodely/basic/relation.py +++ b/nnodely/basic/relation.py @@ -6,42 +6,56 @@ from nnodely.support.jsonutils import merge, stream_to_str from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) MAIN_JSON = { - 'Info' : {}, - 'Inputs' : {}, - 'Constants': {}, - 'Parameters' : {}, - 'Functions' : {}, - 'Relations': {}, - 'Outputs': {} - } + "Info": {}, + "Inputs": {}, + "Constants": {}, + "Parameters": {}, + "Functions": {}, + "Relations": {}, + "Outputs": {}, +} CHECK_NAMES = False if is_notebook() else True + def toStream(obj): from nnodely.layers.parameter import Parameter, Constant - if type(obj) in (int,float,list,np.ndarray): - obj = Constant('Constant'+str(NeuObj.count), obj) - #obj = Stream(obj, MAIN_JSON, {'dim': 1}) if type(obj) in (int, float) else obj + + if type(obj) in (int, float, list, np.ndarray): + obj = Constant("Constant" + str(NeuObj.count), obj) + # obj = Stream(obj, MAIN_JSON, {'dim': 1}) if type(obj) in (int, float) else obj if type(obj) is Parameter or type(obj) is Constant: obj = Stream(obj.name, obj.json, obj.dim) return obj -def check_names(name:str, name_list, list_type): - check(name not in ForbiddenTags, NameError, f"The name '{name}' is a forbidden tag.") + +def check_names(name: str, name_list, list_type): + check( + name not in ForbiddenTags, NameError, f"The name '{name}' is a forbidden tag." + ) if CHECK_NAMES == True: - check(name not in name_list, NameError, f"The name '{name}' is already used as {list_type}.") + check( + name not in name_list, + NameError, + f"The name '{name}' is already used as {list_type}.", + ) elif name in name_list: - log.warning(f"The name '{name}' is already in defined as {list_type} but it is overwritten.") + log.warning( + f"The name '{name}' is already in defined as {list_type} but it is overwritten." + ) -class NeuObj(): + +class NeuObj: count = 0 names = [] + @classmethod @enforce_types - def clearNames(cls, names:str|list|None=None): + def clearNames(cls, names: str | list | None = None): if names is None: NeuObj.count = 0 NeuObj.names = [] @@ -54,10 +68,10 @@ def clearNames(cls, names:str|list|None=None): if names in NeuObj.names: NeuObj.names.remove(names) - def __init__(self, name='', json={}, dim=0): + def __init__(self, name="", json={}, dim=0): NeuObj.count += 1 - if name == '': - name = 'Auto'+str(NeuObj.count) + if name == "": + name = "Auto" + str(NeuObj.count) check_names(name, NeuObj.names, "NeuObj") NeuObj.names.append(name) self.name = name @@ -67,63 +81,82 @@ def __init__(self, name='', json={}, dim=0): else: self.json = copy.deepcopy(MAIN_JSON) -class Relation(): + +class Relation: def __add__(self, obj): from nnodely.layers.arithmetic import Add + return Add(self, obj) def __radd__(self, obj): from nnodely.layers.arithmetic import Add + return Add(obj, self) def __sub__(self, obj): from nnodely.layers.arithmetic import Sub + return Sub(self, obj) def __rsub__(self, obj): from nnodely.layers.arithmetic import Sub + return Sub(obj, self) def __truediv__(self, obj): from nnodely.layers.arithmetic import Div + return Div(self, obj) def __rtruediv__(self, obj): from nnodely.layers.arithmetic import Div + return Div(obj, self) def __mul__(self, obj): from nnodely.layers.arithmetic import Mul + return Mul(self, obj) def __rmul__(self, obj): from nnodely.layers.arithmetic import Mul + return Mul(obj, self) def __pow__(self, obj): from nnodely.layers.arithmetic import Pow + return Pow(self, obj) def __rpow__(self, obj): from nnodely.layers.arithmetic import Pow + return Pow(obj, self) def __neg__(self): from nnodely.layers.arithmetic import Neg + return Neg(self) + class Stream(Relation): """ Represents a stream of data inside the neural network. A Stream is automatically create when you operate over a Input, Parameter, or Constant object. """ + count = 0 + @classmethod def resetCount(cls): Stream.count = 0 - def __init__(self, name, json, dim, count = 1): + def __init__(self, name, json, dim, count=1): Stream.count += count - check(name not in ForbiddenTags, NameError, f"The name '{name}' is a forbidden tag.") + check( + name not in ForbiddenTags, + NameError, + f"The name '{name}' is a forbidden tag.", + ) self.name = name self.json = copy.deepcopy(json) self.dim = dim @@ -135,7 +168,13 @@ def __repr__(self): return self.__str__() @enforce_types - def tw(self, tw:float|int|list, offset:float|int|None = None, *, name:str|None = None) -> "Stream": + def tw( + self, + tw: float | int | list, + offset: float | int | None = None, + *, + name: str | None = None, + ) -> "Stream": """ Selects a time window on Stream. It is possible to create a smaller or bigger time window on the stream. The Time Window must be in the past not in the future. @@ -157,19 +196,28 @@ def tw(self, tw:float|int|list, offset:float|int|None = None, *, name:str|None = """ from nnodely.layers.input import Input from nnodely.layers.part import TimePart + if name is None: - name = self.name+"_tw"+str(NeuObj.count) + name = self.name + "_tw" + str(NeuObj.count) if type(tw) is list: - check(0 >= tw[1] > tw[0] and tw[0] < 0, ValueError, "The dimension of the sample window must be in the past.") - if 'tw' not in self.dim: - self.dim['tw'] = 0 + check( + 0 >= tw[1] > tw[0] and tw[0] < 0, + ValueError, + "The dimension of the sample window must be in the past.", + ) + if "tw" not in self.dim: + self.dim["tw"] = 0 if type(tw) is not list: - tw = [-tw,0] - delayed_input = Input(name, dimensions=self.dim['dim']).connect(self).tw([tw[0],0], offset) - return TimePart(delayed_input,tw[0]-tw[0],tw[1]-tw[0]) + tw = [-tw, 0] + delayed_input = ( + Input(name, dimensions=self.dim["dim"]).connect(self).tw([tw[0], 0], offset) + ) + return TimePart(delayed_input, tw[0] - tw[0], tw[1] - tw[0]) @enforce_types - def sw(self, sw:int|list, offset:int|None = None, *, name:str|None = None) -> "Stream": + def sw( + self, sw: int | list, offset: int | None = None, *, name: str | None = None + ) -> "Stream": """ Selects a sample window on Stream. It is possible to create a smaller or bigger window on the stream. The Sample Window must be in the past not in the future. @@ -191,19 +239,26 @@ def sw(self, sw:int|list, offset:int|None = None, *, name:str|None = None) -> "S """ from nnodely.layers.input import Input from nnodely.layers.part import SamplePart + if name is None: - name = self.name+"_sw"+str(NeuObj.count) + name = self.name + "_sw" + str(NeuObj.count) if type(sw) is list: - check(0 >= sw[1] > sw[0] and sw[0] < 0, ValueError, "The dimension of the sample window must be in the past.") - if 'sw' not in self.dim: - self.dim['sw'] = 0 + check( + 0 >= sw[1] > sw[0] and sw[0] < 0, + ValueError, + "The dimension of the sample window must be in the past.", + ) + if "sw" not in self.dim: + self.dim["sw"] = 0 if type(sw) is not list: - sw = [-sw,0] - delayed_input = Input(name, dimensions=self.dim['dim']).connect(self).sw([sw[0],0], offset) - return SamplePart(delayed_input,sw[0]-sw[0],sw[1]-sw[0]) + sw = [-sw, 0] + delayed_input = ( + Input(name, dimensions=self.dim["dim"]).connect(self).sw([sw[0], 0], offset) + ) + return SamplePart(delayed_input, sw[0] - sw[0], sw[1] - sw[0]) @enforce_types - def z(self, delay:int|float, *, name:str|None = None) -> "Stream": + def z(self, delay: int | float, *, name: str | None = None) -> "Stream": # TODO fix the convetion z-1 means a dealy z+1 means unitary advance """ Considering the Zeta transform notation. The function is used to delay a Stream. @@ -220,11 +275,15 @@ def z(self, delay:int|float, *, name:str|None = None) -> "Stream": A Stream representing the delayed Stream """ check(delay > 0, ValueError, "The delay must be a positive integer") - check('sw' in self.dim, TypeError, "The stream is not defined in samples but in time") - return self.sw([-self.dim['sw']-delay,-delay], name = name) + check( + "sw" in self.dim, + TypeError, + "The stream is not defined in samples but in time", + ) + return self.sw([-self.dim["sw"] - delay, -delay], name=name) @enforce_types - def delay(self, delay:int|float, *, name:str|None = None) -> "Stream": + def delay(self, delay: int | float, *, name: str | None = None) -> "Stream": """ The function is used to delay a Stream. The value of the delay can be only positive. @@ -240,11 +299,22 @@ def delay(self, delay:int|float, *, name:str|None = None) -> "Stream": A Stream representing the delayed Stream """ check(delay > 0, ValueError, "The delay must be a positive integer") - check('tw' in self.dim, TypeError, "The stream is not defined in time but in sample") - return self.tw([-self.dim['tw']-delay,-delay], name = name) + check( + "tw" in self.dim, + TypeError, + "The stream is not defined in time but in sample", + ) + return self.tw([-self.dim["tw"] - delay, -delay], name=name) @enforce_types - def s(self, order:int, *, int_name:str|None = None, der_name:str|None = None, method:str = 'euler') -> "Stream": + def s( + self, + order: int, + *, + int_name: str | None = None, + der_name: str | None = None, + method: str = "euler", + ) -> "Stream": """ Considering the Laplace transform notation. The function is used to operate an integral or derivate operation on a Stream. The order of the integral or the derivative operation is indicated by the order parameter. @@ -262,13 +332,20 @@ def s(self, order:int, *, int_name:str|None = None, der_name:str|None = None, me A Stream of the signal represents the integral or derivation operation. """ from nnodely.layers.timeoperation import Differentiate, Integrate - check(order != 0, ValueError, "The order must be a positive or negative integer not a zero") + + check( + order != 0, + ValueError, + "The order must be a positive or negative integer not a zero", + ) if order > 0: for i in range(order): - o = Differentiate(self, der_name = der_name, int_name = int_name, method = method) + o = Differentiate( + self, der_name=der_name, int_name=int_name, method=method + ) elif order < 0: for i in range(-order): - o = Integrate(self, der_name = der_name, int_name = int_name, method = method) + o = Integrate(self, der_name=der_name, int_name=int_name, method=method) return o def connect(self, obj) -> "Stream": @@ -293,13 +370,21 @@ def connect(self, obj) -> "Stream": If the input variable is already connected. """ from nnodely.layers.input import Input - check(type(obj) is Input, TypeError, - f"The {obj} must be a Input and not a {type(obj)}.") + + check( + type(obj) is Input, + TypeError, + f"The {obj} must be a Input and not a {type(obj)}.", + ) self.json = merge(self.json, obj.json) - check('closedLoop' not in self.json['Inputs'][obj.name] or 'connect' not in self.json['Inputs'][obj.name], KeyError, - f"The input variable {obj.name} is already connected.") - self.json['Inputs'][obj.name]['connect'] = self.name - self.json['Inputs'][obj.name]['local'] = 1 + check( + "closedLoop" not in self.json["Inputs"][obj.name] + or "connect" not in self.json["Inputs"][obj.name], + KeyError, + f"The input variable {obj.name} is already connected.", + ) + self.json["Inputs"][obj.name]["connect"] = self.name + self.json["Inputs"][obj.name]["local"] = 1 return self def closedLoop(self, obj) -> "Stream": @@ -324,27 +409,38 @@ def closedLoop(self, obj) -> "Stream": If the input variable is already connected. """ from nnodely.layers.input import Input - check(type(obj) is Input, TypeError, - f"The {obj} must be a Input and not a {type(obj)}.") + + check( + type(obj) is Input, + TypeError, + f"The {obj} must be a Input and not a {type(obj)}.", + ) self.json = merge(self.json, obj.json) - check('closedLoop' not in self.json['Inputs'][obj.name] or 'connect' not in self.json['Inputs'][obj.name], - KeyError, - f"The input variable {obj.name} is already connected.") - self.json['Inputs'][obj.name]['closedLoop'] = self.name - self.json['Inputs'][obj.name]['local'] = 1 + check( + "closedLoop" not in self.json["Inputs"][obj.name] + or "connect" not in self.json["Inputs"][obj.name], + KeyError, + f"The input variable {obj.name} is already connected.", + ) + self.json["Inputs"][obj.name]["closedLoop"] = self.name + self.json["Inputs"][obj.name]["local"] = 1 return self -class ToStream(): + +class ToStream: def __new__(cls, *args, **kwargs): - out = super(ToStream,cls).__new__(cls) + out = super(ToStream, cls).__new__(cls) out.__init__(*args, **kwargs) - return Stream(out.name,out.json,out.dim,0) + return Stream(out.name, out.json, out.dim, 0) + -class AutoToStream(): - def __new__(cls, *args, **kwargs): - if len(args) > 0 and (issubclass(type(args[0]),NeuObj) or type(args[0]) is Stream): +class AutoToStream: + def __new__(cls, *args, **kwargs): + if len(args) > 0 and ( + issubclass(type(args[0]), NeuObj) or type(args[0]) is Stream + ): instance = super().__new__(cls) - #instance.__init__(**kwargs) + # instance.__init__(**kwargs) instance.__init__() return instance(args[0]) instance = super().__new__(cls) diff --git a/nnodely/exporter/emptyexporter.py b/nnodely/exporter/emptyexporter.py index 6c6dbcf6..57406d2e 100644 --- a/nnodely/exporter/emptyexporter.py +++ b/nnodely/exporter/emptyexporter.py @@ -3,9 +3,9 @@ from datetime import datetime from nnodely.visualizer import EmptyVisualizer -class EmptyExporter: - def __init__(self, workspace = None, visualizer = None, save_history = False): +class EmptyExporter: + def __init__(self, workspace=None, visualizer=None, save_history=False): # Export parameters if workspace is not None: self.workspace = workspace @@ -22,26 +22,28 @@ def __init__(self, workspace = None, visualizer = None, save_history = False): else: self.visualizer = EmptyVisualizer() - def saveTorchModel(self, model, name = 'net', model_folder = None): + def saveTorchModel(self, model, name="net", model_folder=None): pass - def loadTorchModel(self, name = 'net', model_folder = None): + def loadTorchModel(self, name="net", model_folder=None): pass - def saveModel(self, model, name = 'net', model_folder = None): + def saveModel(self, model, name="net", model_folder=None): pass - def loadModel(self, name = 'net', model_folder = None): + def loadModel(self, name="net", model_folder=None): pass - def exportPythonModel(self, name = 'net', model_folder = None): + def exportPythonModel(self, name="net", model_folder=None): pass - def importPythonModel(self, name = 'net', model_folder = None): + def importPythonModel(self, name="net", model_folder=None): pass - def onnxInference(self, inputs:dict, name:str='net', model_folder:str|None=None): + def onnxInference( + self, inputs: dict, name: str = "net", model_folder: str | None = None + ): pass - def exportReport(self, name = 'net', model_folder = None): - pass \ No newline at end of file + def exportReport(self, name="net", model_folder=None): + pass diff --git a/nnodely/exporter/export.py b/nnodely/exporter/export.py index fbfa1ce2..59c617c7 100644 --- a/nnodely/exporter/export.py +++ b/nnodely/exporter/export.py @@ -4,53 +4,69 @@ from pprint import PrettyPrinter + class JsonPrettyPrinter(PrettyPrinter): def _format(self, object, *args): if isinstance(object, str): width = self._width self._width = sys.maxsize try: - super()._format(object.replace('\'','_"_'), *args) + super()._format(object.replace("'", '_"_'), *args) finally: self._width = width else: super()._format(object, *args) + def save_model(model, model_path): # Export the dictionary as a JSON file - with open(model_path, 'w') as json_file: + with open(model_path, "w") as json_file: # json.dump(self.model_def, json_file, indent=4) - json_file.write(JsonPrettyPrinter().pformat(model) - .replace('\'', '\"') - .replace('_"_', '\'') - .replace('None', 'null') - .replace('False', 'false') - .replace('True', 'true')) + json_file.write( + JsonPrettyPrinter() + .pformat(model) + .replace("'", '"') + .replace('_"_', "'") + .replace("None", "null") + .replace("False", "false") + .replace("True", "true") + ) # json_file.write(JsonPrettyPrinter().pformat(model).replace('None','null')) # data = json.dumps(self.model_def) # json_file.write(pformat(data).replace('\\\\n', '\\n').replace('\'', '').replace('(','').replace(')','')) # json_file.write(pformat(data).replace('\'', '\"')) + def load_model(model_path): import json - with open(model_path, 'r', encoding='UTF-8') as file: + + with open(model_path, "r", encoding="UTF-8") as file: model_def = json.load(file) return model_def + def export_python_model(model_def, model, model_path): - package_name = __package__.split('.')[0] + package_name = __package__.split(".")[0] # Get the symbolic tracer with torch.no_grad(): trace = symbolic_trace(model) - recurrent_inputs = {key:value for key, value in model_def['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())} - inputs = {key: value for key, value in model_def['Inputs'].items() if ('closedLoop' not in value.keys() and 'connect' not in value.keys())} - attributes = sorted(set([line for line in trace.code.split() if 'self.' in line])) + recurrent_inputs = { + key: value + for key, value in model_def["Inputs"].items() + if ("closedLoop" in value.keys() or "connect" in value.keys()) + } + inputs = { + key: value + for key, value in model_def["Inputs"].items() + if ("closedLoop" not in value.keys() and "connect" not in value.keys()) + } + attributes = sorted(set([line for line in trace.code.split() if "self." in line])) saved_functions = [] - with open(model_path, 'w') as file: + with open(model_path, "w") as file: file.write("import torch\n\n") ## write the connect wrap function @@ -66,118 +82,161 @@ def export_python_model(model_def, model, model_path): file.write(" return data_out\n\n") file.write(f"def {package_name}_basic_model_timeshift(data_in):\n") - file.write(" return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)\n\n") - - for name in model_def['Functions'].keys(): - if 'Fuzzify' in name: - if 'slicing' not in saved_functions: - #file.write("@torch.fx.wrap\n") - file.write(f"def {package_name}_layers_fuzzify_slicing(res, i, x):\n") + file.write( + " return torch.cat((data_in[:, 1:, :], data_in[:, :1, :]), dim=1)\n\n" + ) + + for name in model_def["Functions"].keys(): + if "Fuzzify" in name: + if "slicing" not in saved_functions: + # file.write("@torch.fx.wrap\n") + file.write( + f"def {package_name}_layers_fuzzify_slicing(res, i, x):\n" + ) file.write(" res[:, :, i:i+1] = x\n\n") - saved_functions.append('slicing') + saved_functions.append("slicing") - function_name = model_def['Functions'][name]['names'] - function_code = model_def['Functions'][name]['functions'] + function_name = model_def["Functions"][name]["names"] + function_code = model_def["Functions"][name]["functions"] if isinstance(function_code, list): for i, fun_code in enumerate(function_code): - if fun_code != 'Rectangular' and fun_code != 'Triangular': + if fun_code != "Rectangular" and fun_code != "Triangular": if function_name[i] not in saved_functions: - fun_code = fun_code.replace(f'def {function_name[i]}', - f'def {package_name}_layers_fuzzify_{function_name[i]}') - #file.write("@torch.fx.wrap\n") + fun_code = fun_code.replace( + f"def {function_name[i]}", + f"def {package_name}_layers_fuzzify_{function_name[i]}", + ) + # file.write("@torch.fx.wrap\n") file.write(fun_code) file.write("\n") saved_functions.append(function_name[i]) else: - if (function_name != 'Rectangular') and (function_name != 'Triangular') and (function_name not in saved_functions): - function_code = function_code.replace(f'def {function_name}', - f'def {package_name}_layers_fuzzify_{function_name}') - #file.write("@torch.fx.wrap\n") + if ( + (function_name != "Rectangular") + and (function_name != "Triangular") + and (function_name not in saved_functions) + ): + function_code = function_code.replace( + f"def {function_name}", + f"def {package_name}_layers_fuzzify_{function_name}", + ) + # file.write("@torch.fx.wrap\n") file.write(function_code) file.write("\n") saved_functions.append(function_name) - - elif 'ParamFun' in name: - function_name = model_def['Functions'][name]['name'] + elif "ParamFun" in name: + function_name = model_def["Functions"][name]["name"] if function_name not in saved_functions: - code = model_def['Functions'][name]['code'] - code = code.replace(f'def {function_name}', f'def {package_name}_layers_parametricfunction_{function_name}') + code = model_def["Functions"][name]["code"] + code = code.replace( + f"def {function_name}", + f"def {package_name}_layers_parametricfunction_{function_name}", + ) file.write(code) file.write("\n") saved_functions.append(function_name) - elif 'NeuralODE' in name: - function_name = model_def['Functions'][name]['name'] + elif "NeuralODE" in name: + function_name = model_def["Functions"][name]["name"] if function_name not in saved_functions: - code = model_def['Functions'][name]['code'] - code = code.replace(f'def {function_name}', f'def {package_name}_layers_neuralODE_{function_name}') + code = model_def["Functions"][name]["code"] + code = code.replace( + f"def {function_name}", + f"def {package_name}_layers_neuralODE_{function_name}", + ) file.write(code) file.write("\n") saved_functions.append(function_name) - file.write("class TracerModel(torch.nn.Module):\n") file.write(" def __init__(self):\n") file.write(" super().__init__()\n") file.write(" self.all_parameters = {}\n") file.write(" self.all_constants = {}\n") for attr in attributes: - if 'all_constant' in attr: - key = attr.split('.')[-1] - file.write(f" self.all_constants[\"{key}\"] = torch.tensor({model.all_constants[key].tolist()}, requires_grad=False)\n") - elif 'relation_forward' in attr: - key = attr.split('.')[2] - if 'Fir' in key or 'Linear' in key: - if 'weights' in attr.split('.')[3]: - param = model_def['Relations'][key][2] - value = model.all_parameters[param] - file.write(f" self.all_parameters[\"{param}\"] = torch.nn.Parameter(torch.tensor({value.tolist()}), requires_grad=True)\n") - elif 'bias' in attr.split('.')[3]: - param = model_def['Relations'][key][3] - file.write(f" self.all_parameters[\"{param}\"] = torch.nn.Parameter(torch.tensor({model.all_parameters[param].tolist()}), requires_grad=True)\n") - elif 'dropout' in attr.split('.')[3]: - param = model_def['Relations'][key][4] - file.write(f" self.{key} = torch.nn.Dropout(p={param})\n") + if "all_constant" in attr: + key = attr.split(".")[-1] + file.write( + f' self.all_constants["{key}"] = torch.tensor({model.all_constants[key].tolist()}, requires_grad=False)\n' + ) + elif "relation_forward" in attr: + key = attr.split(".")[2] + if "Fir" in key or "Linear" in key: + if "weights" in attr.split(".")[3]: + param = model_def["Relations"][key][2] + value = model.all_parameters[param] + file.write( + f' self.all_parameters["{param}"] = torch.nn.Parameter(torch.tensor({value.tolist()}), requires_grad=True)\n' + ) + elif "bias" in attr.split(".")[3]: + param = model_def["Relations"][key][3] + file.write( + f' self.all_parameters["{param}"] = torch.nn.Parameter(torch.tensor({model.all_parameters[param].tolist()}), requires_grad=True)\n' + ) + elif "dropout" in attr.split(".")[3]: + param = model_def["Relations"][key][4] + file.write( + f" self.{key} = torch.nn.Dropout(p={param})\n" + ) # param = model_def['Relations'][key][2] if 'weights' in attr.split('.')[3] else model_def['Relations'][key][3] # value = model.all_parameters[param].data.squeeze(0) if 'Linear' in key else model.all_parameters[param].data # file.write(f" self.all_parameters[\"{param}\"] = torch.nn.Parameter(torch.{value}, requires_grad=True)\n") - elif 'Part' in key or 'Select' in key: # any(element in key for element in ['Part', 'Select']): + elif ( + "Part" in key or "Select" in key + ): # any(element in key for element in ['Part', 'Select']): value = model.relation_forward[key].W temp_value = json.dumps(value.tolist()) - file.write(f" self.all_constants[\"{key}\"] = torch.tensor({temp_value}, requires_grad=True)\n") - elif 'all_parameters' in attr: - key = attr.split('.')[-1] - file.write(f" self.all_parameters[\"{key}\"] = torch.nn.Parameter(torch.tensor({model.all_parameters[key].tolist()}), requires_grad=True)\n") - elif '_tensor_constant' in attr: - key = attr.split('.')[-1] - file.write(f" {attr} = torch.tensor({getattr(model,key).item()})\n") - - file.write(" self.all_parameters = torch.nn.ParameterDict(self.all_parameters)\n") - file.write(" self.all_constants = torch.nn.ParameterDict(self.all_constants)\n\n") - file.write(" def update(self, closed_loop={}, connect={}, disconnect=False):\n") + file.write( + f' self.all_constants["{key}"] = torch.tensor({temp_value}, requires_grad=True)\n' + ) + elif "all_parameters" in attr: + key = attr.split(".")[-1] + file.write( + f' self.all_parameters["{key}"] = torch.nn.Parameter(torch.tensor({model.all_parameters[key].tolist()}), requires_grad=True)\n' + ) + elif "_tensor_constant" in attr: + key = attr.split(".")[-1] + file.write( + f" {attr} = torch.tensor({getattr(model, key).item()})\n" + ) + + file.write( + " self.all_parameters = torch.nn.ParameterDict(self.all_parameters)\n" + ) + file.write( + " self.all_constants = torch.nn.ParameterDict(self.all_constants)\n\n" + ) + file.write( + " def update(self, closed_loop={}, connect={}, disconnect=False):\n" + ) file.write(" pass\n") - for line in trace.code.split("\n")[len(saved_functions) + 2:]: - if 'self.relation_forward' in line: - if 'Part' in line or 'Select' in line: - attribute = [x for x in line.split() if 'self.relation_forward' in x][0].split('.')[2] + for line in trace.code.split("\n")[len(saved_functions) + 2 :]: + if "self.relation_forward" in line: + if "Part" in line or "Select" in line: + attribute = [ + x for x in line.split() if "self.relation_forward" in x + ][0].split(".")[2] old_line = f"self.relation_forward.{attribute}.W" new_line = f"self.all_constants.{attribute}" file.write(f" {line.replace(old_line, new_line)}\n") - elif 'dropout' in line: + elif "dropout" in line: attribute = line.split()[0] - layer = attribute.split('_')[2].capitalize() + layer = attribute.split("_")[2].capitalize() old_line = f"self.relation_forward.{layer}.dropout" new_line = f"self.{layer}" file.write(f" {line.replace(old_line, new_line)}\n") else: attribute = line.split()[-1] - relation = attribute.split('.')[2] - relation_type = attribute.split('.')[3] - param = model_def['Relations'][relation][2] if 'weights' == relation_type else \ - model_def['Relations'][relation][3] - new_attribute = f'self.all_parameters.{param}' + relation = attribute.split(".")[2] + relation_type = attribute.split(".")[3] + param = ( + model_def["Relations"][relation][2] + if "weights" == relation_type + else model_def["Relations"][relation][3] + ) + new_attribute = f"self.all_parameters.{param}" file.write(f" {line.replace(attribute, new_attribute)}\n") else: file.write(f" {line}\n") @@ -195,11 +254,13 @@ def export_python_model(model_def, model, model_path): file.write(" self.states = dict()\n") file.write("\n") file.write(" def forward(self, kwargs, n_samples = None):\n") - file.write(" n_samples = n_samples if n_samples else min([kwargs[key].size(0) for key in self.inputs])\n") + file.write( + " n_samples = n_samples if n_samples else min([kwargs[key].size(0) for key in self.inputs])\n" + ) for key in recurrent_inputs.keys(): file.write(f" self.states['{key}'] = kwargs['{key}']\n") result_str = "" - for key, value in model_def['Outputs'].items(): + for key, value in model_def["Outputs"].items(): result_str += f"'{key}':[], " file.write(f" results = {{{result_str}}}\n") file.write(" X = dict()\n") @@ -212,77 +273,108 @@ def export_python_model(model_def, model, model_path): file.write(" for key, value in results.items():\n") file.write(" results[key].append(out[key])\n") file.write(" for key, val in closed_loop.items():\n") - file.write(" self.states[key] = nnodely_basic_model_timeshift(self.states[key])\n") - file.write(" self.states[key] = nnodely_basic_model_update_state(self.states[key], val)\n") + file.write( + " self.states[key] = nnodely_basic_model_timeshift(self.states[key])\n" + ) + file.write( + " self.states[key] = nnodely_basic_model_update_state(self.states[key], val)\n" + ) file.write(" for key, val in connect.items():\n") - file.write(" self.states[key] = nnodely_basic_model_timeshift(val)\n") + file.write( + " self.states[key] = nnodely_basic_model_timeshift(val)\n" + ) file.write(" return results\n") -def export_pythononnx_model(model_def, model_path, model_onnx_path, input_order=None, outputs_order=None): + +def export_pythononnx_model( + model_def, model_path, model_onnx_path, input_order=None, outputs_order=None +): closed_loop_states, connect_states = [], [] - for key, value in model_def['Inputs'].items(): - if 'closedLoop' in value.keys(): + for key, value in model_def["Inputs"].items(): + if "closedLoop" in value.keys(): closed_loop_states.append(key) - if 'connect' in value.keys(): + if "connect" in value.keys(): connect_states.append(key) - - model_inputs = input_order if input_order else list(model_def['Inputs'].keys()) - model_outputs = outputs_order if outputs_order else list(model_def['Outputs'].keys()) + + model_inputs = input_order if input_order else list(model_def["Inputs"].keys()) + model_outputs = ( + outputs_order if outputs_order else list(model_def["Outputs"].keys()) + ) model_losses = [] - if model_def['Minimizers']: - model_losses = [loss_dict['A'] for loss_dict in model_def['Minimizers'].values() if 'A' in loss_dict.keys()] + [loss_dict['B'] for loss_dict in model_def['Minimizers'].values() if 'A' in loss_dict.keys()] - recurrent_inputs = [key for key, value in model_def['Inputs'].items() if ('closedLoop' in value.keys() or 'connect' in value.keys())] + if model_def["Minimizers"]: + model_losses = [ + loss_dict["A"] + for loss_dict in model_def["Minimizers"].values() + if "A" in loss_dict.keys() + ] + [ + loss_dict["B"] + for loss_dict in model_def["Minimizers"].values() + if "A" in loss_dict.keys() + ] + recurrent_inputs = [ + key + for key, value in model_def["Inputs"].items() + if ("closedLoop" in value.keys() or "connect" in value.keys()) + ] inputs = [key for key in model_inputs if key not in recurrent_inputs] # Define the mapping dictionary input trace_mapping_input = {} - forward = 'def forward(self,' + forward = "def forward(self," for i, key in enumerate(model_inputs): - value = f'kwargs[\'{key}\']' + value = f"kwargs['{key}']" trace_mapping_input[value] = key - forward = forward + f' {key}' + (',' if i < len(model_inputs) - 1 else '') - forward = forward + '):' + forward = forward + f" {key}" + ("," if i < len(model_inputs) - 1 else "") + forward = forward + "):" # Define the mapping dictionary output - outputs = ' return (' + outputs = " return (" for i, key in enumerate(model_outputs): - outputs += f'outputs[0][\'{key}\']' + (',' if i < len(model_outputs) - 1 else ',)') - outputs += ', (' + outputs += f"outputs[0]['{key}']" + ( + "," if i < len(model_outputs) - 1 else ",)" + ) + outputs += ", (" for i, key in enumerate(model_losses): - outputs += f'outputs[1][\'{key}\'], '# + (',' if i < len(model_outputs) - 1 else ',)') - outputs += '), (' + outputs += ( + f"outputs[1]['{key}'], " # + (',' if i < len(model_outputs) - 1 else ',)') + ) + outputs += "), (" for key in closed_loop_states: - outputs += f'outputs[2][\'{key}\'], ' - outputs += '), (' + outputs += f"outputs[2]['{key}'], " + outputs += "), (" for key in connect_states: - outputs += f'outputs[3][\'{key}\'], ' - outputs += ')\n' + outputs += f"outputs[3]['{key}'], " + outputs += ")\n" # Open and read the file file_content = [] - with open(model_path, 'r') as file: + with open(model_path, "r") as file: for line in file: - if 'return ({' in line: + if "return ({" in line: file_content.append(line) break file_content.append(line) - file_content = ''.join(file_content) - + file_content = "".join(file_content) + # Replace the forward header - file_content = file_content.replace('def forward(self, kwargs):', forward) + file_content = file_content.replace("def forward(self, kwargs):", forward) # Perform the substitution for key, value in trace_mapping_input.items(): file_content = file_content.replace(key, value) # Write the modified content back to a new file # Replace the return statement - last_return_index = file_content.rfind('return') + last_return_index = file_content.rfind("return") if last_return_index != -1: - file_content = file_content[:last_return_index] + 'outputs =' + file_content[last_return_index + len('return'):] + file_content = ( + file_content[:last_return_index] + + "outputs =" + + file_content[last_return_index + len("return") :] + ) file_content += outputs - with open(model_onnx_path, 'w') as file: + with open(model_onnx_path, "w") as file: file.write(file_content) if len(recurrent_inputs) > 0: - file.write('\n') + file.write("\n") file.write("class RecurrentModel(torch.nn.Module):\n") file.write(" def __init__(self):\n") file.write(" super().__init__()\n") @@ -295,10 +387,14 @@ def export_pythononnx_model(model_def, model_path, model_onnx_path, input_order= file.write(forward_str) if model_inputs: - file.write(" n_samples = min([" + ", ".join([f"{key}.size(0)" for key in inputs]) + "])\n") + file.write( + " n_samples = min([" + + ", ".join([f"{key}.size(0)" for key in inputs]) + + "])\n" + ) else: file.write(" n_samples = 1\n") - + for key in model_outputs: file.write(f" results_{key} = []\n") file.write(" for idx in range(n_samples):\n") @@ -310,18 +406,27 @@ def export_pythononnx_model(model_def, model_path, model_onnx_path, input_order= for idx, key in enumerate(model_outputs): file.write(f" results_{key}.append(out[{idx}])\n") for idx, key in enumerate(closed_loop_states): - file.write(f" {key} = nnodely_basic_model_timeshift({key})\n") - file.write(f" {key} = nnodely_basic_model_update_state({key}, closed_loop[{idx}])\n") + file.write( + f" {key} = nnodely_basic_model_timeshift({key})\n" + ) + file.write( + f" {key} = nnodely_basic_model_update_state({key}, closed_loop[{idx}])\n" + ) for idx, key in enumerate(connect_states): - file.write(f" {key} = nnodely_basic_model_timeshift(connect[{idx}])\n") - #file.write(f" {key} = connect[{idx}]\n") + file.write( + f" {key} = nnodely_basic_model_timeshift(connect[{idx}])\n" + ) + # file.write(f" {key} = connect[{idx}]\n") for idx, key in enumerate(model_outputs): - file.write(f" results_{key} = torch.stack(results_{key}, dim=0)\n") + file.write( + f" results_{key} = torch.stack(results_{key}, dim=0)\n" + ) return_str = " return " for key in model_outputs: return_str += f"results_{key}, " file.write(return_str) + def import_python_model(name, model_folder): sys.path.insert(0, model_folder) module_name = os.path.basename(name) @@ -333,14 +438,23 @@ def import_python_model(name, model_folder): module = importlib.import_module(module_name) return module.TracerModel() -def export_onnx_model(model_def, model, model_path, input_order=None, output_order=None, name='net_onnx'): + +def export_onnx_model( + model_def, model, model_path, input_order=None, output_order=None, name="net_onnx" +): sys.path.insert(0, model_path) module_name = os.path.basename(name) - recurrent_inputs = {key:value for key, value in model_def['Inputs'].items() if - ('closedLoop' in value.keys() or 'connect' in value.keys())} - inputs = {key:value for key, value in model_def['Inputs'].items() if - ('closedLoop' not in value.keys() and 'connect' not in value.keys())} + recurrent_inputs = { + key: value + for key, value in model_def["Inputs"].items() + if ("closedLoop" in value.keys() or "connect" in value.keys()) + } + inputs = { + key: value + for key, value in model_def["Inputs"].items() + if ("closedLoop" not in value.keys() and "connect" not in value.keys()) + } if module_name in sys.modules: # Reload the module if it is already loaded @@ -348,51 +462,59 @@ def export_onnx_model(model_def, model, model_path, input_order=None, output_ord else: # Import the module if it is not loaded module = importlib.import_module(module_name) - model = torch.jit.script(module.RecurrentModel()) if len(recurrent_inputs) > 0 else module.TracerModel() + model = ( + torch.jit.script(module.RecurrentModel()) + if len(recurrent_inputs) > 0 + else module.TracerModel() + ) model.eval() dummy_inputs = [] input_names = [] dynamic_axes = {} - onnx_inputs = input_order if input_order else model_def['Inputs'].keys() + onnx_inputs = input_order if input_order else model_def["Inputs"].keys() for key in onnx_inputs: input_names.append(key) - window_size = model_def['Inputs'][key]['ntot'] - dim = model_def['Inputs'][key]['dim'] + window_size = model_def["Inputs"][key]["ntot"] + dim = model_def["Inputs"][key]["dim"] if len(recurrent_inputs) > 0 != {}: if key in inputs.keys(): dummy_inputs.append(torch.randn(size=(1, 1, window_size, dim))) - dynamic_axes[key] = {0: 'horizon', 1: 'batch_size'} + dynamic_axes[key] = {0: "horizon", 1: "batch_size"} elif key in recurrent_inputs.keys(): dummy_inputs.append(torch.randn(size=(1, window_size, dim))) - dynamic_axes[key] = {0: 'batch_size'} + dynamic_axes[key] = {0: "batch_size"} else: dummy_inputs.append(torch.randn(size=(1, window_size, dim))) - dynamic_axes[key] = {0: 'batch_size'} - output_names = output_order if output_order else list(model_def['Outputs'].keys()) + dynamic_axes[key] = {0: "batch_size"} + output_names = output_order if output_order else list(model_def["Outputs"].keys()) dummy_inputs = tuple(dummy_inputs) torch.onnx.export( - model, # The model to be exported - dummy_inputs, # Tuple of inputs to match the forward signature - model_path, # File path to save the ONNX model - export_params = True, # Store the trained parameters in the model file - opset_version = 17, # ONNX version to export to (you can use 11 or higher) - do_constant_folding=False, # Optimize constant folding for inference - input_names = input_names, # Name each input as they will appear in ONNX - output_names = output_names, # Name the output - dynamic_axes = dynamic_axes, + model, # The model to be exported + dummy_inputs, # Tuple of inputs to match the forward signature + model_path, # File path to save the ONNX model + export_params=True, # Store the trained parameters in the model file + opset_version=17, # ONNX version to export to (you can use 11 or higher) + do_constant_folding=False, # Optimize constant folding for inference + input_names=input_names, # Name each input as they will appear in ONNX + output_names=output_names, # Name the output + dynamic_axes=dynamic_axes, ) + def onnx_inference(inputs, path, optimize_graph=False): import onnxruntime as ort + # Create an ONNX Runtime session # Define session options - ## TODO: Warning when using constant folding in inference CanUpdateImplicitInputNameInSubgraphs] + ## TODO: Warning when using constant folding in inference CanUpdateImplicitInputNameInSubgraphs] ## TODO: Implicit input name Cell.all_constants.Constant75 cannot be safely updated to Cell.all_constants.Constant76 in one of the subgraphs. if optimize_graph == False: session_options = ort.SessionOptions() # Set graph optimization level to disable all optimizations - session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL + session_options.graph_optimization_level = ( + ort.GraphOptimizationLevel.ORT_DISABLE_ALL + ) session = ort.InferenceSession(path, sess_options=session_options) else: session = ort.InferenceSession(path) @@ -403,4 +525,4 @@ def onnx_inference(inputs, path, optimize_graph=False): for item in session.get_inputs(): input_data[item.name] = inputs[item.name] # Run inference - return session.run(output_data, input_data) \ No newline at end of file + return session.run(output_data, input_data) diff --git a/nnodely/exporter/reporter.py b/nnodely/exporter/reporter.py index 31f1e35a..3dbdf181 100644 --- a/nnodely/exporter/reporter.py +++ b/nnodely/exporter/reporter.py @@ -16,39 +16,65 @@ def exportReport(self, report_path): c = canvas.Canvas(report_path, pagesize=letter) width, height = letter - if 'Minimizers' in self.modely._model_def: - for key, value in self.modely._model_def['Minimizers'].items(): + if "Minimizers" in self.modely._model_def: + for key, value in self.modely._model_def["Minimizers"].items(): fig = plt.figure(figsize=(10, 5)) ax = fig.add_subplot(111) - if 'val' in self.modely._training[key]: - plots.plot_training(ax, f"Training Loss of {key}", key, self.modely._training[key]['train'], self.modely._training[key]['val']) + if "val" in self.modely._training[key]: + plots.plot_training( + ax, + f"Training Loss of {key}", + key, + self.modely._training[key]["train"], + self.modely._training[key]["val"], + ) else: - plots.plot_training(ax, f"Training Loss of {key}", key, self.modely._training[key]['train']) + plots.plot_training( + ax, + f"Training Loss of {key}", + key, + self.modely._training[key]["train"], + ) training = io.BytesIO() - plt.savefig(training, format='png') + plt.savefig(training, format="png") training.seek(0) plt.close() c.drawString(100, height - 30, f"Training Loss of {key}") - c.drawImage(ImageReader(training), 50, height - 290, width=500, height=250) + c.drawImage( + ImageReader(training), 50, height - 290, width=500, height=250 + ) c.showPage() if len(self.modely.prediction) > 0: - for key in self.modely._model_def['Minimizers'].keys(): + for key in self.modely._model_def["Minimizers"].keys(): c.drawString(100, height - 30, f"Prediction of {key}") for ind, name_data in enumerate(self.modely.prediction.keys()): fig = plt.figure(figsize=(10, 5)) ax = fig.add_subplot(111) idxs = None - if 'idxs' in self.modely.prediction[name_data]: - idxs = self.modely.prediction[name_data]['idxs'] - plots.plot_results(ax, name_data, key, self.modely.prediction[name_data][key]['A'], - self.modely.prediction[name_data][key]['B'], idxs, self.modely._model_def['Info']["SampleTime"]) + if "idxs" in self.modely.prediction[name_data]: + idxs = self.modely.prediction[name_data]["idxs"] + plots.plot_results( + ax, + name_data, + key, + self.modely.prediction[name_data][key]["A"], + self.modely.prediction[name_data][key]["B"], + idxs, + self.modely._model_def["Info"]["SampleTime"], + ) # Add a text box with correlation coefficient results = io.BytesIO() - plt.savefig(results, format='png') + plt.savefig(results, format="png") results.seek(0) plt.close() - c.drawImage(ImageReader(results), 50, height - 290 - 245*ind, width=500, height=250) + c.drawImage( + ImageReader(results), + 50, + height - 290 - 245 * ind, + width=500, + height=250, + ) c.showPage() else: c.drawString(100, height - 30, f"No Minimize") diff --git a/nnodely/exporter/standardexporter.py b/nnodely/exporter/standardexporter.py index 7db7ba59..687bd931 100644 --- a/nnodely/exporter/standardexporter.py +++ b/nnodely/exporter/standardexporter.py @@ -3,36 +3,64 @@ from nnodely.visualizer import EmptyVisualizer from nnodely.exporter.emptyexporter import EmptyExporter from nnodely.exporter.reporter import Reporter -from nnodely.exporter.export import save_model, load_model, export_python_model, export_pythononnx_model, export_onnx_model, import_python_model, onnx_inference +from nnodely.exporter.export import ( + save_model, + load_model, + export_python_model, + export_pythononnx_model, + export_onnx_model, + import_python_model, + onnx_inference, +) from nnodely.support.utils import check, enforce_types from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.INFO) + class StandardExporter(EmptyExporter): @enforce_types - def __init__(self, workspace:str|None=None, visualizer:EmptyVisualizer|None=None, *, save_history:bool=False): + def __init__( + self, + workspace: str | None = None, + visualizer: EmptyVisualizer | None = None, + *, + save_history: bool = False, + ): super().__init__(workspace, visualizer, save_history) def getWorkspace(self): - return self.workspace_folder if hasattr(self,'workspace_folder') else '.' + return self.workspace_folder if hasattr(self, "workspace_folder") else "." - def saveTorchModel(self, model, name = 'net', model_folder = None): + def saveTorchModel(self, model, name="net", model_folder=None): file_name = name + ".pt" - model_path = os.path.join(self.getWorkspace(), file_name) if model_folder is None else os.path.join(model_folder,file_name) - #TODO check if the folder exist + model_path = ( + os.path.join(self.getWorkspace(), file_name) + if model_folder is None + else os.path.join(model_folder, file_name) + ) + # TODO check if the folder exist torch.save(model.state_dict(), model_path) - self.visualizer.saveModel('Torch Model', model_path) + self.visualizer.saveModel("Torch Model", model_path) - def loadTorchModel(self, model, name = 'net', model_folder = None): + def loadTorchModel(self, model, name="net", model_folder=None): file_name = name + ".pt" - model_path = os.path.join(self.getWorkspace(), file_name) if model_folder is None else os.path.join(model_folder,file_name) - check(os.path.exists(model_path), FileNotFoundError, f"The model {name} it is not found in the folder {model_folder}") + model_path = ( + os.path.join(self.getWorkspace(), file_name) + if model_folder is None + else os.path.join(model_folder, file_name) + ) + check( + os.path.exists(model_path), + FileNotFoundError, + f"The model {name} it is not found in the folder {model_folder}", + ) model.load_state_dict(torch.load(model_path, weights_only=True)) - self.visualizer.loadModel('Torch Model',model_path) - #TODO Update the model parameters.... + self.visualizer.loadModel("Torch Model", model_path) + # TODO Update the model parameters.... - def saveModel(self, model_def, name = 'net', model_folder = None): + def saveModel(self, model_def, name="net", model_folder=None): # Combine the folder path and file name to form the complete file path model_folder = self.getWorkspace() if model_folder is None else model_folder # Specify the JSON file name @@ -40,9 +68,9 @@ def saveModel(self, model_def, name = 'net', model_folder = None): # Combine the folder path and file name to form the complete file path model_path = os.path.join(model_folder, file_name) save_model(model_def, model_path) - self.visualizer.saveModel('JSON Model', model_path) + self.visualizer.saveModel("JSON Model", model_path) - def loadModel(self, name = 'net', model_folder = None): + def loadModel(self, name="net", model_folder=None): # Combine the folder path and file name to form the complete file path model_folder = self.getWorkspace() if model_folder is None else model_folder model_def = None @@ -50,61 +78,112 @@ def loadModel(self, name = 'net', model_folder = None): file_name = name + ".json" model_path = os.path.join(model_folder, file_name) model_def = load_model(model_path) - self.visualizer.loadModel('JSON Model', model_path) + self.visualizer.loadModel("JSON Model", model_path) except Exception as e: - check(False, FileNotFoundError, f"The file {model_path} it is not found or not conformed.\n Error: {e}") + check( + False, + FileNotFoundError, + f"The file {model_path} it is not found or not conformed.\n Error: {e}", + ) return model_def - def exportPythonModel(self, model_def, model, name = 'net', model_folder = None): + def exportPythonModel(self, model_def, model, name="net", model_folder=None): file_name = name + ".py" - model_path = os.path.join(self.getWorkspace(), file_name) if model_folder is None else os.path.join(model_folder, file_name) + model_path = ( + os.path.join(self.getWorkspace(), file_name) + if model_folder is None + else os.path.join(model_folder, file_name) + ) ## Export to python file export_python_model(model_def.getJson(), model, model_path) - self.visualizer.exportModel('Python Torch Model', model_path) + self.visualizer.exportModel("Python Torch Model", model_path) - def importPythonModel(self, name = 'net', model_folder = None): + def importPythonModel(self, name="net", model_folder=None): try: model_folder = self.getWorkspace() if model_folder is None else model_folder model = import_python_model(name, model_folder) - self.visualizer.importModel('Python Torch Model', os.path.join(model_folder,name+'.py')) + self.visualizer.importModel( + "Python Torch Model", os.path.join(model_folder, name + ".py") + ) except Exception as e: model = None - check(False, FileNotFoundError, f"The model {name} it is not found in the folder {model_folder}.\nError: {e}") + check( + False, + FileNotFoundError, + f"The model {name} it is not found in the folder {model_folder}.\nError: {e}", + ) return model - def exportONNX(self, model_def, model, inputs_order=None, outputs_order=None, name = 'net', model_folder = None): + def exportONNX( + self, + model_def, + model, + inputs_order=None, + outputs_order=None, + name="net", + model_folder=None, + ): if inputs_order is None: - log.info(f"The inputs order for the export is not specified, the order will set equal to {set(model_def['Inputs'].keys())}.") - elif set(inputs_order) != set(model_def['Inputs'].keys()): - raise ValueError(f'The inputs are not the same as the model inputs {set(model_def["Inputs"].keys())}.') + log.info( + f"The inputs order for the export is not specified, the order will set equal to {set(model_def['Inputs'].keys())}." + ) + elif set(inputs_order) != set(model_def["Inputs"].keys()): + raise ValueError( + f"The inputs are not the same as the model inputs {set(model_def['Inputs'].keys())}." + ) if outputs_order is None: - log.info(f"The outputs order for the export is not specified, the order will set equal to {set(model_def['Outputs'].keys())}") - elif set(outputs_order) != set(model_def['Outputs'].keys()): - log.info(f'The outputs are not the same as the model outputs {set(model_def["Outputs"].keys())}.') + log.info( + f"The outputs order for the export is not specified, the order will set equal to {set(model_def['Outputs'].keys())}" + ) + elif set(outputs_order) != set(model_def["Outputs"].keys()): + log.info( + f"The outputs are not the same as the model outputs {set(model_def['Outputs'].keys())}." + ) file_name = name + ".py" - model_folder = os.path.join(self.getWorkspace(), 'onnx') if model_folder is None else model_folder + model_folder = ( + os.path.join(self.getWorkspace(), "onnx") + if model_folder is None + else model_folder + ) os.makedirs(model_folder, exist_ok=True) model_path = os.path.join(model_folder, file_name) - onnx_python_model_path = model_path.replace('.py', '_onnx.py') - onnx_model_path = model_path.replace('.py', '.onnx') + onnx_python_model_path = model_path.replace(".py", "_onnx.py") + onnx_model_path = model_path.replace(".py", ".onnx") ## Export to python file (onnx compatible) export_python_model(model_def, model, model_path) - self.visualizer.exportModel('Python Torch Model', model_path) - export_pythononnx_model(model_def, model_path, onnx_python_model_path, inputs_order, outputs_order) - self.visualizer.exportModel('Python Onnx Torch Model', onnx_python_model_path) + self.visualizer.exportModel("Python Torch Model", model_path) + export_pythononnx_model( + model_def, model_path, onnx_python_model_path, inputs_order, outputs_order + ) + self.visualizer.exportModel("Python Onnx Torch Model", onnx_python_model_path) ## Export to onnx file (onnx compatible) - model = import_python_model(file_name.replace('.py', '_onnx'), model_folder) - export_onnx_model(model_def, model, onnx_model_path, inputs_order, outputs_order, name=name+'_onnx') - self.visualizer.exportModel('Onnx Model', onnx_model_path) - - def onnxInference(self, inputs, name:str='net', model_folder:str|None=None): - model_folder = os.path.join(self.getWorkspace(), 'onnx') if model_folder is None else model_folder + model = import_python_model(file_name.replace(".py", "_onnx"), model_folder) + export_onnx_model( + model_def, + model, + onnx_model_path, + inputs_order, + outputs_order, + name=name + "_onnx", + ) + self.visualizer.exportModel("Onnx Model", onnx_model_path) + + def onnxInference(self, inputs, name: str = "net", model_folder: str | None = None): + model_folder = ( + os.path.join(self.getWorkspace(), "onnx") + if model_folder is None + else model_folder + ) file_name = name + ".onnx" onnx_model_path = os.path.join(model_folder, file_name) - check(os.path.exists(onnx_model_path), FileNotFoundError, f"The model {file_name} it is not found in the folder {model_folder}") + check( + os.path.exists(onnx_model_path), + FileNotFoundError, + f"The model {file_name} it is not found in the folder {model_folder}", + ) return onnx_inference(inputs, onnx_model_path) - def exportReport(self, n4m, name = 'net', model_folder = None): + def exportReport(self, n4m, name="net", model_folder=None): # Combine the folder path and file name to form the complete file path model_folder = self.getWorkspace() if model_folder is None else model_folder # Specify the JSON file name @@ -113,5 +192,4 @@ def exportReport(self, n4m, name = 'net', model_folder = None): report_path = os.path.join(model_folder, file_name) reporter = Reporter(n4m) reporter.exportReport(report_path) - self.visualizer.exportReport('Training Results', report_path) - + self.visualizer.exportReport("Training Results", report_path) diff --git a/nnodely/layers/activation.py b/nnodely/layers/activation.py index a6c38482..4c432fa0 100644 --- a/nnodely/layers/activation.py +++ b/nnodely/layers/activation.py @@ -7,80 +7,95 @@ from nnodely.support.utils import check, enforce_types -relu_relation_name = 'Relu' -elu_relation_name = 'ELU' -sigmoid_relation_name = 'Sigmoid' -identity_relation_name = 'Identity' -softmax_relation_name = 'Softmax' +relu_relation_name = "Relu" +elu_relation_name = "ELU" +sigmoid_relation_name = "Sigmoid" +identity_relation_name = "Identity" +softmax_relation_name = "Softmax" + class Relu(Stream, ToStream): """ - Implement the Rectified-Linear Unit (ReLU) relation function. + Implement the Rectified-Linear Unit (ReLU) relation function. - See also: - Official PyTorch ReLU documentation: - `torch.nn.ReLU `_ + See also: + Official PyTorch ReLU documentation: + `torch.nn.ReLU `_ - :param obj: The relation stream. - :type obj: Stream + :param obj: The relation stream. + :type obj: Stream - Example: - -------- - .. include:: /examples_basics/layer_module_ex/activation_module_ex/relu.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/activation_module_ex/relu.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Relu operation.") - super().__init__(relu_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [relu_relation_name,[obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Relu operation.", + ) + super().__init__(relu_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [relu_relation_name, [obj.name]] + class ELU(Stream, ToStream): """ - Implement the Exponential-Linear Unit (ELU) relation function. + Implement the Exponential-Linear Unit (ELU) relation function. - See also: - Official PyTorch ReLU documentation: - `torch.nn.ELU `_ + See also: + Official PyTorch ReLU documentation: + `torch.nn.ELU `_ - :param obj: The relation stream. - :type obj: Stream + :param obj: The relation stream. + :type obj: Stream - Example: - --------- - .. include:: /examples_basics/layer_module_ex/activation_module_ex/elu.rst + Example: + --------- + .. include:: /examples_basics/layer_module_ex/activation_module_ex/elu.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream: obj = toStream(obj) - check(type(obj) is Stream,TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.") - super().__init__(elu_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [elu_relation_name,[obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.", + ) + super().__init__(elu_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [elu_relation_name, [obj.name]] + class Identity(Stream, ToStream): """ Implement the Identity relation function that simply returns the input vector x. See also: - Official PyTorch Identity documentation: + Official PyTorch Identity documentation: `torch.nn.Identity `_ :param obj: The relation stream. - :type obj: Stream + :type obj: Stream Example: --------- .. include:: /examples_basics/layer_module_ex/activation_module_ex/identity.rst """ + @enforce_types - def __init__(self, obj: Stream|Parameter|Constant|float|int) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Identity operation.") + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Identity operation.", + ) super().__init__(identity_relation_name + str(Stream.count), obj.json, obj.dim) - self.json['Relations'][self.name] = [identity_relation_name, [obj.name]] + self.json["Relations"][self.name] = [identity_relation_name, [obj.name]] class Softmax(Stream, ToStream): @@ -98,13 +113,18 @@ class Softmax(Stream, ToStream): --------- .. include:: /examples_basics/layer_module_ex/activation_module_ex/softmax.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Softmax operation.") + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Softmax operation.", + ) super().__init__(softmax_relation_name + str(Stream.count), obj.json, obj.dim) - self.json['Relations'][self.name] = [softmax_relation_name, [obj.name]] + self.json["Relations"][self.name] = [softmax_relation_name, [obj.name]] + class Sigmoid(Stream, ToStream): r""" @@ -125,74 +145,94 @@ class Sigmoid(Stream, ToStream): --------- .. include:: /examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for {sigmoid_relation_name} operation.") + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for {sigmoid_relation_name} operation.", + ) super().__init__(sigmoid_relation_name + str(Stream.count), obj.json, obj.dim) - self.json['Relations'][self.name] = [sigmoid_relation_name, [obj.name]] + self.json["Relations"][self.name] = [sigmoid_relation_name, [obj.name]] + class Relu_Layer(nn.Module): """ - :noindex: + :noindex: """ - def __init__(self,): + + def __init__( + self, + ): super(Relu_Layer, self).__init__() + def forward(self, x): return torch.relu(x) - + + def createRelu(self, *input): """ - :noindex: + :noindex: """ return Relu_Layer() - + def createELU(self, *input): """ - :noindex: + :noindex: """ return nn.ELU() + class Identity_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self, *args): super(Identity_Layer, self).__init__() def forward(self, x: torch.Tensor) -> torch.Tensor: return x - + + def createIdentity(self, *input): """ - :noindex: + :noindex: """ return Identity_Layer() class Sigmoid_Layer(nn.Module): """ - :noindex: + :noindex: """ - def __init__(self,): + + def __init__( + self, + ): super(Sigmoid_Layer, self).__init__() + def forward(self, x): - return 1/(1+torch.exp(-x)) - + return 1 / (1 + torch.exp(-x)) + + def createSigmoid(self, *input): """ - :noindex: + :noindex: """ return Sigmoid_Layer() + def createSoftmax(self, *input): """ - :noindex: + :noindex: """ return nn.Softmax(dim=-1) + setattr(Model, relu_relation_name, createRelu) setattr(Model, elu_relation_name, createELU) setattr(Model, sigmoid_relation_name, createSigmoid) diff --git a/nnodely/layers/arithmetic.py b/nnodely/layers/arithmetic.py index 0b2f5877..240116e2 100644 --- a/nnodely/layers/arithmetic.py +++ b/nnodely/layers/arithmetic.py @@ -9,174 +9,229 @@ # Binary operators -add_relation_name = 'Add' -sub_relation_name = 'Sub' -mul_relation_name = 'Mul' -div_relation_name = 'Div' -pow_relation_name = 'Pow' +add_relation_name = "Add" +sub_relation_name = "Sub" +mul_relation_name = "Mul" +div_relation_name = "Div" +pow_relation_name = "Pow" # Unary operators -neg_relation_name = 'Neg' -sign_relation_name = 'Sign' +neg_relation_name = "Neg" +sign_relation_name = "Sign" # Merge operator -sum_relation_name = 'Sum' +sum_relation_name = "Sum" + class Add(Stream, ToStream): """ - Implement the addition function between two tensors. - (it is also possible to use the classical math operator '+') + Implement the addition function between two tensors. + (it is also possible to use the classical math operator '+') - See also: - Official PyTorch Add documentation: - `torch.add `_ + See also: + Official PyTorch Add documentation: + `torch.add `_ - :param input1: the first element of the addition - :type obj: Tensor - :param input2: the second element of the addition - :type obj: Tensor + :param input1: the first element of the addition + :type obj: Tensor + :param input2: the second element of the addition + :type obj: Tensor - Example: - -------- - .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/add.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/add.rst """ + @enforce_types - def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream: - obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'addition operators (+)') - super().__init__(add_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim) - self.json['Relations'][self.name] = [add_relation_name,[obj1.name,obj2.name]] + def __init__( + self, + obj1: Stream | Parameter | Constant | int | float, + obj2: Stream | Parameter | Constant | int | float, + ) -> Stream: + obj1, obj2, dim = binary_cheks(self, obj1, obj2, "addition operators (+)") + super().__init__( + add_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim + ) + self.json["Relations"][self.name] = [add_relation_name, [obj1.name, obj2.name]] + ## TODO: check the scalar dimension, helpful for the offset class Sub(Stream, ToStream): """ - Implement the subtraction function between two tensors. - (it is also possible to use the classical math operator '-') + Implement the subtraction function between two tensors. + (it is also possible to use the classical math operator '-') - :param input1: the first element of the subtraction - :type obj: Tensor - :param input2: the second element of the subtraction - :type obj: Tensor + :param input1: the first element of the subtraction + :type obj: Tensor + :param input2: the second element of the subtraction + :type obj: Tensor - Example: - -------- - .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst """ + @enforce_types - def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream: - obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'subtraction operators (-)') - super().__init__(sub_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim) - self.json['Relations'][self.name] = [sub_relation_name,[obj1.name,obj2.name]] + def __init__( + self, + obj1: Stream | Parameter | Constant | int | float, + obj2: Stream | Parameter | Constant | int | float, + ) -> Stream: + obj1, obj2, dim = binary_cheks(self, obj1, obj2, "subtraction operators (-)") + super().__init__( + sub_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim + ) + self.json["Relations"][self.name] = [sub_relation_name, [obj1.name, obj2.name]] + class Mul(Stream, ToStream): """ - Implement the multiplication function between two tensors. - (it is also possible to use the classical math operator '*') + Implement the multiplication function between two tensors. + (it is also possible to use the classical math operator '*') - :param input1: the first element of the multiplication - :type obj: Tensor - :param input2: the second element of the multiplication - :type obj: Tensor + :param input1: the first element of the multiplication + :type obj: Tensor + :param input2: the second element of the multiplication + :type obj: Tensor - Example: - -------- - .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst """ + @enforce_types - def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream: - obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'multiplication operators (*)') - super().__init__(mul_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim) - self.json['Relations'][self.name] = [mul_relation_name,[obj1.name,obj2.name]] + def __init__( + self, + obj1: Stream | Parameter | Constant | int | float, + obj2: Stream | Parameter | Constant | int | float, + ) -> Stream: + obj1, obj2, dim = binary_cheks(self, obj1, obj2, "multiplication operators (*)") + super().__init__( + mul_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim + ) + self.json["Relations"][self.name] = [mul_relation_name, [obj1.name, obj2.name]] + class Div(Stream, ToStream): """ - Implement the division function between two tensors. - (it is also possible to use the classical math operator '/') + Implement the division function between two tensors. + (it is also possible to use the classical math operator '/') - :param input1: the numerator of the division - :type obj: Tensor - :param input2: the denominator of the division - :type obj: Tensor + :param input1: the numerator of the division + :type obj: Tensor + :param input2: the denominator of the division + :type obj: Tensor - Example: - -------- - .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/div.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/div.rst """ + @enforce_types - def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream: - obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'division operators (/) ') - super().__init__(div_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim) - self.json['Relations'][self.name] = [div_relation_name,[obj1.name,obj2.name]] + def __init__( + self, + obj1: Stream | Parameter | Constant | int | float, + obj2: Stream | Parameter | Constant | int | float, + ) -> Stream: + obj1, obj2, dim = binary_cheks(self, obj1, obj2, "division operators (/) ") + super().__init__( + div_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim + ) + self.json["Relations"][self.name] = [div_relation_name, [obj1.name, obj2.name]] + class Pow(Stream, ToStream): """ - Implement the power function given an input and an exponent. - (it is also possible to use the classical math operator '**') + Implement the power function given an input and an exponent. + (it is also possible to use the classical math operator '**') - See also: - Official PyTorch pow documentation: - `torch.pow `_ + See also: + Official PyTorch pow documentation: + `torch.pow `_ - :param input: the base of the power function - :type obj: Tensor - :param exp: the exponent of the power function - :type obj: float or Tensor + :param input: the base of the power function + :type obj: Tensor + :param exp: the exponent of the power function + :type obj: float or Tensor - Example: - -------- - .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst """ + @enforce_types - def __init__(self, obj1:Stream|Parameter|Constant|int|float, obj2:Stream|Parameter|Constant|int|float) -> Stream: - obj1, obj2, dim = binary_cheks(self, obj1, obj2, 'pow operators (**)') - super().__init__(pow_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim) - self.json['Relations'][self.name] = [pow_relation_name,[obj1.name,obj2.name]] + def __init__( + self, + obj1: Stream | Parameter | Constant | int | float, + obj2: Stream | Parameter | Constant | int | float, + ) -> Stream: + obj1, obj2, dim = binary_cheks(self, obj1, obj2, "pow operators (**)") + super().__init__( + pow_relation_name + str(Stream.count), merge(obj1.json, obj2.json), dim + ) + self.json["Relations"][self.name] = [pow_relation_name, [obj1.name, obj2.name]] + class Neg(Stream, ToStream): """ - Implement the negate function given an input. + Implement the negate function given an input. - :param input: the input to negate - :type obj: Tensor + :param input: the input to negate + :type obj: Tensor - Example: - -------- - .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for neg operation.") - super().__init__(neg_relation_name+str(Stream.count), obj.json, obj.dim) - self.json['Relations'][self.name] = [neg_relation_name,[obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for neg operation.", + ) + super().__init__(neg_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [neg_relation_name, [obj.name]] + class Sign(Stream, ToStream): """ - Implement the sign function given an input. + Implement the sign function given an input. - :param input: the input for the sign function - :type obj: Tensor + :param input: the input for the sign function + :type obj: Tensor - Example: - >>> x = Sign(x) + Example: + >>> x = Sign(x) """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for sign operation.") - super().__init__(sign_relation_name+str(Stream.count), obj.json, obj.dim) - self.json['Relations'][self.name] = [sign_relation_name,[obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for sign operation.", + ) + super().__init__(sign_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [sign_relation_name, [obj.name]] + class Sum(Stream, ToStream): @enforce_types - def __init__(self, obj:Stream|Parameter|Constant) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for sum operation.") - obj.dim['dim'] = 1 - super().__init__(sum_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [sum_relation_name,[obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for sum operation.", + ) + obj.dim["dim"] = 1 + super().__init__(sum_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [sum_relation_name, [obj.name]] + class Add_Layer(nn.Module): #: :noindex: @@ -189,16 +244,19 @@ def forward(self, *inputs): results = results + input return results + def createAdd(name, *inputs): """ - :noindex: + :noindex: """ return Add_Layer() + class Sub_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self): super(Sub_Layer, self).__init__() @@ -209,17 +267,19 @@ def forward(self, *inputs): results = results - input return results + def createSub(self, *inputs): """ - :noindex: + :noindex: """ return Sub_Layer() class Mul_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self): super(Mul_Layer, self).__init__() @@ -229,16 +289,19 @@ def forward(self, *inputs): results = results * input return results + def createMul(name, *inputs): """ - :noindex: + :noindex: """ return Mul_Layer() + class Div_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self): super(Div_Layer, self).__init__() @@ -248,76 +311,90 @@ def forward(self, *inputs): results = results / input return results + def createDiv(name, *inputs): """ - :noindex: + :noindex: """ return Div_Layer() + class Pow_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self): super(Pow_Layer, self).__init__() def forward(self, *inputs): return torch.pow(inputs[0], inputs[1]) + def createPow(name, *inputs): """ - :noindex: + :noindex: """ return Pow_Layer() + class Neg_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self): super(Neg_Layer, self).__init__() def forward(self, x): return -x + def createNeg(self, *inputs): """ - :noindex: + :noindex: """ return Neg_Layer() + class Sign_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self): super(Sign_Layer, self).__init__() def forward(self, x): return torch.sign(x) + def createSign(self, *inputs): """ - :noindex: + :noindex: """ return Sign_Layer() + class Sum_Layer(nn.Module): """ - :noindex: + :noindex: """ + def __init__(self): super(Sum_Layer, self).__init__() def forward(self, inputs): - return torch.sum(inputs, dim = 2, keepdim = True) + return torch.sum(inputs, dim=2, keepdim=True) + def createSum(name, *inputs): """ - :noindex: + :noindex: """ return Sum_Layer() + setattr(Model, add_relation_name, createAdd) setattr(Model, sub_relation_name, createSub) setattr(Model, mul_relation_name, createMul) @@ -328,5 +405,3 @@ def createSum(name, *inputs): setattr(Model, sign_relation_name, createSign) setattr(Model, sum_relation_name, createSum) - - diff --git a/nnodely/layers/equationlearner.py b/nnodely/layers/equationlearner.py index fa6d1dbf..b3db8ed8 100644 --- a/nnodely/layers/equationlearner.py +++ b/nnodely/layers/equationlearner.py @@ -11,16 +11,35 @@ from nnodely.layers.trigonometric import Sin, Cos, Tan, Tanh, Cosh, Sech from nnodely.layers.arithmetic import Add, Mul, Sub, Neg, Pow, Sum -equationlearner_relation_name = 'EquationLearner' -Available_functions = [Sin, Cos, Tan, Cosh, Tanh, Sech, Add, Mul, Sub, Neg, Pow, Sum, Concatenate, Relu, ELU, Identity, Sigmoid] +equationlearner_relation_name = "EquationLearner" +Available_functions = [ + Sin, + Cos, + Tan, + Cosh, + Tanh, + Sech, + Add, + Mul, + Sub, + Neg, + Pow, + Sum, + Concatenate, + Relu, + ELU, + Identity, + Sigmoid, +] Initialized_functions = [ParamFun, Fuzzify] + class EquationLearner(NeuObj): """ Represents a nnodely implementation of the Task-Parametrized Equation Learner block. See also: - Task-Parametrized Equation Learner official paper: + Task-Parametrized Equation Learner official paper: `Equation Learner `_ Parameters @@ -52,8 +71,15 @@ class EquationLearner(NeuObj): .. include:: /examples_basics/layer_module_ex/eql.rst """ + @enforce_types - def __init__(self, functions:list, *, linear_in:Linear|None = None, linear_out:Linear|None = None) -> Stream: + def __init__( + self, + functions: list, + *, + linear_in: Linear | None = None, + linear_out: Linear | None = None, + ) -> Stream: self.relation_name = equationlearner_relation_name self.linear_in = linear_in self.linear_out = linear_out @@ -64,38 +90,77 @@ def __init__(self, functions:list, *, linear_in:Linear|None = None, linear_out:L self.func_parameters = {} for func_idx, func in enumerate(self.functions): - check(callable(func), TypeError, 'The activation functions must be callable') + check( + callable(func), TypeError, "The activation functions must be callable" + ) if type(func) in Initialized_functions: if type(func) == ParamFun: funinfo = inspect.getfullargspec(func.param_fun) - num_args = len(funinfo.args) - len(func.parameters_and_constants) if func.parameters_and_constants else len(funinfo.args) + num_args = ( + len(funinfo.args) - len(func.parameters_and_constants) + if func.parameters_and_constants + else len(funinfo.args) + ) elif type(func) == Fuzzify: - init_signature = inspect.signature(func.__call__) + init_signature = inspect.signature(func.__call__) parameters = list(init_signature.parameters.values()) - num_args = len([param for param in parameters if param.name != "self"]) + num_args = len( + [param for param in parameters if param.name != "self"] + ) else: - check(func in Available_functions, ValueError, f'The function {func} is not available for the EquationLearner operation') - init_signature = inspect.signature(func.__init__) + check( + func in Available_functions, + ValueError, + f"The function {func} is not available for the EquationLearner operation", + ) + init_signature = inspect.signature(func.__init__) parameters = list(init_signature.parameters.values()) num_args = len([param for param in parameters if param.name != "self"]) self.func_parameters[func_idx] = num_args self.n_activations = sum(self.func_parameters.values()) - check(self.n_activations > 0, ValueError, 'At least one activation function must be provided') + check( + self.n_activations > 0, + ValueError, + "At least one activation function must be provided", + ) def __call__(self, inputs): if type(inputs) is not tuple: inputs = (inputs,) - check(len(set([x.dim['sw'] if 'sw' in x.dim.keys() else x.dim['tw'] for x in inputs])) == 1, ValueError, 'All inputs must have the same time dimension') + check( + len( + set( + [ + x.dim["sw"] if "sw" in x.dim.keys() else x.dim["tw"] + for x in inputs + ] + ) + ) + == 1, + ValueError, + "All inputs must have the same time dimension", + ) concatenated_input = inputs[0] for inp in inputs[1:]: concatenated_input = Concatenate(concatenated_input, inp) - linear_layer = self.linear_in(concatenated_input) if self.linear_in else Linear(output_dimension=self.n_activations, b=True)(concatenated_input) + linear_layer = ( + self.linear_in(concatenated_input) + if self.linear_in + else Linear(output_dimension=self.n_activations, b=True)(concatenated_input) + ) idx, out = 0, None for func_idx, func in enumerate(self.functions): - arguments = [Select(linear_layer,idx+arg_idx) for arg_idx in range(self.func_parameters[func_idx])] + arguments = [ + Select(linear_layer, idx + arg_idx) + for arg_idx in range(self.func_parameters[func_idx]) + ] idx += self.func_parameters[func_idx] - out = func(*arguments) if func_idx == 0 else Concatenate(out, func(*arguments)) + out = ( + func(*arguments) + if func_idx == 0 + else Concatenate(out, func(*arguments)) + ) if self.linear_out: out = self.linear_out(out) return out diff --git a/nnodely/layers/fir.py b/nnodely/layers/fir.py index b340dbe0..b1778694 100644 --- a/nnodely/layers/fir.py +++ b/nnodely/layers/fir.py @@ -11,9 +11,11 @@ from nnodely.support.jsonutils import merge from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) -fir_relation_name = 'Fir' +fir_relation_name = "Fir" + class Fir(NeuObj, AutoToStream): """ @@ -71,21 +73,26 @@ class Fir(NeuObj, AutoToStream): output_dimension : int The output dimension of the FIR relation. - Examples + Examples -------- .. include:: /examples_basics/layer_module_ex/fir.rst """ + @enforce_types - def __init__(self, output_dimension:int|None = None, *, - W_init:Callable|str|None = None, - W_init_params:dict|None = None, - b_init:Callable|str|None = None, - b_init_params:dict|None = None, - W:Parameter|str|None = None, - b:bool|str|Parameter|None = None, - dropout:int|float = 0): + def __init__( + self, + output_dimension: int | None = None, + *, + W_init: Callable | str | None = None, + W_init_params: dict | None = None, + b_init: Callable | str | None = None, + b_init_params: dict | None = None, + W: Parameter | str | None = None, + b: bool | str | Parameter | None = None, + dropout: int | float = 0, + ): self.W = W self.b = b @@ -93,72 +100,124 @@ def __init__(self, output_dimension:int|None = None, *, self.bname = None self.dropout = dropout - super().__init__('P'+fir_relation_name + str(NeuObj.count)) + super().__init__("P" + fir_relation_name + str(NeuObj.count)) if type(self.W) is Parameter: - check('tw' in self.W.dim or 'sw' in self.W.dim, TypeError, f'The "W" Parameter must have a time dimension or a sample dimension but got {self.W.dim}.') - #check(len(self.W.dim) == 2,ValueError,f"The values of the parameters must have two dimensions [tw/sample_rate,output_dimension] or [sw,output_dimension].") + check( + "tw" in self.W.dim or "sw" in self.W.dim, + TypeError, + f'The "W" Parameter must have a time dimension or a sample dimension but got {self.W.dim}.', + ) + # check(len(self.W.dim) == 2,ValueError,f"The values of the parameters must have two dimensions [tw/sample_rate,output_dimension] or [sw,output_dimension].") if output_dimension is None: - check(type(self.W.dim['dim']) is int, TypeError, 'Dimension of the parameter must be an integer for the Fir') - self.output_dimension = self.W.dim['dim'] + check( + type(self.W.dim["dim"]) is int, + TypeError, + "Dimension of the parameter must be an integer for the Fir", + ) + self.output_dimension = self.W.dim["dim"] else: self.output_dimension = output_dimension - check(self.W.dim['dim'] == self.output_dimension, - ValueError, - 'output_dimension must be equal to dim of the Parameter') + check( + self.W.dim["dim"] == self.output_dimension, + ValueError, + "output_dimension must be equal to dim of the Parameter", + ) self.Wname = self.W.name W_json = self.W.json else: ## Create a new default parameter self.output_dimension = 1 if output_dimension is None else output_dimension - self.Wname = W if type(W) is str else self.name + 'W' - W_json = Parameter(name=self.Wname, dimensions=self.output_dimension, init=W_init, init_params=W_init_params).json - self.json = merge(self.json,W_json) + self.Wname = W if type(W) is str else self.name + "W" + W_json = Parameter( + name=self.Wname, + dimensions=self.output_dimension, + init=W_init, + init_params=W_init_params, + ).json + self.json = merge(self.json, W_json) if self.b is not None and self.b is not False: if type(self.b) is Parameter: - check('tw' not in self.b.dim and 'sw' not in self.b.dim, TypeError, f'The "bias" must no have a time dimensions but got {self.b.dim}.') - check(type(self.b.dim['dim']) is int, ValueError, 'The "bias" dimensions must be an integer.') - check(self.b.dim['dim'] == self.output_dimension, ValueError, 'output_dimension must be equal to the dim of the "bias".') + check( + "tw" not in self.b.dim and "sw" not in self.b.dim, + TypeError, + f'The "bias" must no have a time dimensions but got {self.b.dim}.', + ) + check( + type(self.b.dim["dim"]) is int, + ValueError, + 'The "bias" dimensions must be an integer.', + ) + check( + self.b.dim["dim"] == self.output_dimension, + ValueError, + 'output_dimension must be equal to the dim of the "bias".', + ) self.bname = self.b.name b_json = self.b.json else: - self.bname = b if type(self.b) is str else self.name + 'b' - b_json = Parameter(name=self.bname, dimensions=self.output_dimension, init=b_init, init_params=b_init_params).json - self.json = merge(self.json,b_json) + self.bname = b if type(self.b) is str else self.name + "b" + b_json = Parameter( + name=self.bname, + dimensions=self.output_dimension, + init=b_init, + init_params=b_init_params, + ).json + self.json = merge(self.json, b_json) self.json_stream = {} @enforce_types - def __call__(self, obj:Stream) -> Stream: + def __call__(self, obj: Stream) -> Stream: stream_name = fir_relation_name + str(Stream.count) - check('dim' in obj.dim and obj.dim['dim'] == 1, - ValueError, - f"Input dimension is {obj.dim['dim']} and not scalar") - window = 'tw' if 'tw' in obj.dim else ('sw' if 'sw' in obj.dim else None) + check( + "dim" in obj.dim and obj.dim["dim"] == 1, + ValueError, + f"Input dimension is {obj.dim['dim']} and not scalar", + ) + window = "tw" if "tw" in obj.dim else ("sw" if "sw" in obj.dim else None) json_stream_name = window + str(obj.dim[window]) if json_stream_name not in self.json_stream: if len(self.json_stream) > 0: - log.warning(f"The Fir {self.name} was called with inputs with different dimensions. If both Fir enter in the model an error will be raised.") + log.warning( + f"The Fir {self.name} was called with inputs with different dimensions. If both Fir enter in the model an error will be raised." + ) self.json_stream[json_stream_name] = copy.deepcopy(self.json) - self.json_stream[json_stream_name]['Parameters'][self.Wname][window] = obj.dim[window] + self.json_stream[json_stream_name]["Parameters"][self.Wname][window] = obj.dim[ + window + ] if window: if type(self.W) is Parameter: - check(window in self.json['Parameters'][self.Wname], - TypeError, - f"The window \'{window}\' of the input is not in the W") - check(self.json['Parameters'][self.Wname][window] == obj.dim[window], - ValueError, - f"The window \'{window}\' of the input must be the same of the W") + check( + window in self.json["Parameters"][self.Wname], + TypeError, + f"The window '{window}' of the input is not in the W", + ) + check( + self.json["Parameters"][self.Wname][window] == obj.dim[window], + ValueError, + f"The window '{window}' of the input must be the same of the W", + ) else: if type(self.W) is Parameter: - cond = 'sw' not in self.json_stream[json_stream_name]['Parameters'][self.Wname] and 'tw' not in \ - self.json_stream[json_stream_name]['Parameters'][self.Wname] - check(cond, KeyError, 'The W have a time window and the input no') - - stream_json = merge(self.json_stream[json_stream_name],obj.json) - stream_json['Relations'][stream_name] = [fir_relation_name, [obj.name], self.Wname, self.bname, self.dropout] - return Stream(stream_name, stream_json,{'dim':self.output_dimension, 'sw': 1}) + cond = ( + "sw" + not in self.json_stream[json_stream_name]["Parameters"][self.Wname] + and "tw" + not in self.json_stream[json_stream_name]["Parameters"][self.Wname] + ) + check(cond, KeyError, "The W have a time window and the input no") + + stream_json = merge(self.json_stream[json_stream_name], obj.json) + stream_json["Relations"][stream_name] = [ + fir_relation_name, + [obj.name], + self.Wname, + self.bname, + self.dropout, + ] + return Stream(stream_name, stream_json, {"dim": self.output_dimension, "sw": 1}) class Fir_Layer(nn.Module): @@ -186,7 +245,9 @@ def forward(self, x): x = self.dropout(x) return x + def createFir(self, *inputs): return Fir_Layer(weights=inputs[0], bias=inputs[1], dropout=inputs[2]) + setattr(Model, fir_relation_name, createFir) diff --git a/nnodely/layers/fuzzify.py b/nnodely/layers/fuzzify.py index 2e65ad8f..a09bd9b8 100644 --- a/nnodely/layers/fuzzify.py +++ b/nnodely/layers/fuzzify.py @@ -10,7 +10,8 @@ from nnodely.support.utils import check, enforce_types from nnodely.support.jsonutils import merge -fuzzify_relation_name = 'Fuzzify' +fuzzify_relation_name = "Fuzzify" + class Fuzzify(NeuObj): """ @@ -27,7 +28,7 @@ class Fuzzify(NeuObj): The `output_dimension` will be inferred from the number of centers provided. functions : str, list, or Callable, optional The fuzzy functions to use. Can be a string specifying a predefined function type, a custom function, or a list of callable functions. Default is 'Triangular'. - + Notes ----- .. note:: @@ -48,85 +49,122 @@ class Fuzzify(NeuObj): .. include:: /examples_basics/layer_module_ex/fuzzy.rst """ + @enforce_types - def __init__(self, output_dimension: int | None = None, - range: list | None = None, *, - centers: list | None = None, - functions: str | list | Callable = 'Triangular'): + def __init__( + self, + output_dimension: int | None = None, + range: list | None = None, + *, + centers: list | None = None, + functions: str | list | Callable = "Triangular", + ): self.relation_name = fuzzify_relation_name - super().__init__('F' + fuzzify_relation_name + str(NeuObj.count)) - self.json['Functions'][self.name] = {} + super().__init__("F" + fuzzify_relation_name + str(NeuObj.count)) + self.json["Functions"][self.name] = {} if output_dimension is not None: - check(range is not None, ValueError, 'if "output_dimension" is not None, "range" must be not setted') - check(centers is None, ValueError, - 'if "output_dimension" and "range" are not None, then "centers" must be None') - self.output_dimension = {'dim': output_dimension} - interval = ((range[1] - range[0]) / (output_dimension - 1)) - self.json['Functions'][self.name]['centers'] = np.arange(range[0], range[1] + interval, interval).tolist() + check( + range is not None, + ValueError, + 'if "output_dimension" is not None, "range" must be not setted', + ) + check( + centers is None, + ValueError, + 'if "output_dimension" and "range" are not None, then "centers" must be None', + ) + self.output_dimension = {"dim": output_dimension} + interval = (range[1] - range[0]) / (output_dimension - 1) + self.json["Functions"][self.name]["centers"] = np.arange( + range[0], range[1] + interval, interval + ).tolist() else: - check(centers is not None, ValueError, 'if "output_dimension" is None and "centers" must be setted') - self.output_dimension = {'dim': len(centers)} - self.json['Functions'][self.name]['centers'] = np.array(centers).tolist() - self.json['Functions'][self.name]['dim_out'] = copy.deepcopy(self.output_dimension) + check( + centers is not None, + ValueError, + 'if "output_dimension" is None and "centers" must be setted', + ) + self.output_dimension = {"dim": len(centers)} + self.json["Functions"][self.name]["centers"] = np.array(centers).tolist() + self.json["Functions"][self.name]["dim_out"] = copy.deepcopy( + self.output_dimension + ) if type(functions) is str: - self.json['Functions'][self.name]['functions'] = functions - self.json['Functions'][self.name]['names'] = functions + self.json["Functions"][self.name]["functions"] = functions + self.json["Functions"][self.name]["names"] = functions elif type(functions) is list: - self.json['Functions'][self.name]['functions'] = [] - self.json['Functions'][self.name]['names'] = [] + self.json["Functions"][self.name]["functions"] = [] + self.json["Functions"][self.name]["names"] = [] for func in functions: - code = textwrap.dedent(inspect.getsource(func)).replace('\"', '\'') - self.json['Functions'][self.name]['functions'].append(code) - self.json['Functions'][self.name]['names'].append(func.__name__) + code = textwrap.dedent(inspect.getsource(func)).replace('"', "'") + self.json["Functions"][self.name]["functions"].append(code) + self.json["Functions"][self.name]["names"].append(func.__name__) else: - code = textwrap.dedent(inspect.getsource(functions)).replace('\"', '\'') - self.json['Functions'][self.name]['functions'] = code - self.json['Functions'][self.name]['names'] = functions.__name__ + code = textwrap.dedent(inspect.getsource(functions)).replace('"', "'") + self.json["Functions"][self.name]["functions"] = code + self.json["Functions"][self.name]["names"] = functions.__name__ @enforce_types def __call__(self, obj: Stream) -> Stream: stream_name = fuzzify_relation_name + str(Stream.count) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Fuzzify operation.") - check('dim' in obj.dim and obj.dim['dim'] == 1, ValueError, 'Input dimension must be scalar') + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Fuzzify operation.", + ) + check( + "dim" in obj.dim and obj.dim["dim"] == 1, + ValueError, + "Input dimension must be scalar", + ) output_dimension = copy.deepcopy(obj.dim) output_dimension.update(self.output_dimension) stream_json = merge(self.json, obj.json) - stream_json['Relations'][stream_name] = [fuzzify_relation_name, [obj.name], self.name] + stream_json["Relations"][stream_name] = [ + fuzzify_relation_name, + [obj.name], + self.name, + ] return Stream(stream_name, stream_json, output_dimension) + def return_fuzzify(json, xlim=None, num_points=1000): if xlim is not None: x = torch.from_numpy(np.linspace(xlim[0], xlim[1], num=num_points)) else: - x = torch.from_numpy(np.linspace(json['centers'][0] - 2, json['centers'][-1] + 2, num=num_points)) - chan_centers = np.array(json['centers']) + x = torch.from_numpy( + np.linspace(json["centers"][0] - 2, json["centers"][-1] + 2, num=num_points) + ) + chan_centers = np.array(json["centers"]) activ_fun = {} - if isinstance(json['names'], list): - n_func = len(json['names']) + if isinstance(json["names"], list): + n_func = len(json["names"]) else: n_func = 1 for i in range(len(chan_centers)): - if json['functions'] == 'Triangular': + if json["functions"] == "Triangular": activ_fun[i] = triangular(x, i, chan_centers).tolist() - elif json['functions'] == 'Rectangular': + elif json["functions"] == "Rectangular": activ_fun[i] = rectangular(x, i, chan_centers).tolist() else: - if isinstance(json['names'], list): + if isinstance(json["names"], list): if i >= n_func: func_idx = i - round(n_func * (i // n_func)) else: func_idx = i - exec(json['functions'][func_idx], globals()) - function_to_call = globals()[json['names'][func_idx]] + exec(json["functions"][func_idx], globals()) + function_to_call = globals()[json["names"][func_idx]] else: - exec(json['functions'], globals()) - function_to_call = globals()[json['names']] - activ_fun[i] = custom_function(function_to_call, x, i, chan_centers).tolist() + exec(json["functions"], globals()) + function_to_call = globals()[json["names"]] + activ_fun[i] = custom_function( + function_to_call, x, i, chan_centers + ).tolist() return x.tolist(), activ_fun + def triangular(x, idx_channel, chan_centers): # Compute the number of channels num_channels = len(chan_centers) @@ -134,19 +172,33 @@ def triangular(x, idx_channel, chan_centers): if idx_channel == 0: if num_channels != 1: ampl = chan_centers[1] - chan_centers[0] - act_fcn = torch.minimum(torch.maximum(-(x - chan_centers[0]) / ampl + 1, torch.tensor(0.0)), torch.tensor(1.0)) + act_fcn = torch.minimum( + torch.maximum(-(x - chan_centers[0]) / ampl + 1, torch.tensor(0.0)), + torch.tensor(1.0), + ) else: # In case the user only wants one channel act_fcn = 1 elif idx_channel != 0 and idx_channel == (num_channels - 1): ampl = chan_centers[-1] - chan_centers[-2] - act_fcn = torch.minimum(torch.maximum((x - chan_centers[-2]) / ampl, torch.tensor(0.0)), torch.tensor(1.0)) + act_fcn = torch.minimum( + torch.maximum((x - chan_centers[-2]) / ampl, torch.tensor(0.0)), + torch.tensor(1.0), + ) else: ampl_1 = chan_centers[idx_channel] - chan_centers[idx_channel - 1] ampl_2 = chan_centers[idx_channel + 1] - chan_centers[idx_channel] - act_fcn = torch.minimum(torch.maximum((x - chan_centers[idx_channel - 1]) / ampl_1, torch.tensor(0.0)), torch.maximum(-(x - chan_centers[idx_channel]) / ampl_2 + 1, torch.tensor(0.0))) + act_fcn = torch.minimum( + torch.maximum( + (x - chan_centers[idx_channel - 1]) / ampl_1, torch.tensor(0.0) + ), + torch.maximum( + -(x - chan_centers[idx_channel]) / ampl_2 + 1, torch.tensor(0.0) + ), + ) return act_fcn + def rectangular(x, idx_channel, chan_centers): ## compute number of channels num_channels = len(chan_centers) @@ -162,9 +214,18 @@ def rectangular(x, idx_channel, chan_centers): width = abs(chan_centers[idx_channel] - chan_centers[idx_channel - 1]) / 2 act_fcn = torch.where(x >= chan_centers[idx_channel] - width, 1.0, 0.0) else: - width_forward = abs(chan_centers[idx_channel + 1] - chan_centers[idx_channel]) / 2 - width_backward = abs(chan_centers[idx_channel] - chan_centers[idx_channel - 1]) / 2 - act_fcn = torch.where((x >= chan_centers[idx_channel] - width_backward) & (x < chan_centers[idx_channel] + width_forward), 1.0, 0.0) + width_forward = ( + abs(chan_centers[idx_channel + 1] - chan_centers[idx_channel]) / 2 + ) + width_backward = ( + abs(chan_centers[idx_channel] - chan_centers[idx_channel - 1]) / 2 + ) + act_fcn = torch.where( + (x >= chan_centers[idx_channel] - width_backward) + & (x < chan_centers[idx_channel] + width_forward), + 1.0, + 0.0, + ) return act_fcn @@ -172,39 +233,48 @@ def custom_function(func, x, idx_channel, chan_centers): act_fcn = func(x - chan_centers[idx_channel]) return act_fcn + class Fuzzify_Layer(nn.Module): def __init__(self, params): super().__init__() - self.centers = params['centers'] - self.function = params['functions'] - self.dimension = params['dim_out']['dim'] - self.name = params['names'] + self.centers = params["centers"] + self.function = params["functions"] + self.dimension = params["dim_out"]["dim"] + self.name = params["names"] if type(self.name) is list: self.n_func = len(self.name) for func, name in zip(self.function, self.name): ## Add the function to the globals try: - code = 'import torch\n@torch.fx.wrap\n' + func + code = "import torch\n@torch.fx.wrap\n" + func exec(code, globals()) except Exception as e: - check(False, RuntimeError, f"An error occurred when running the function '{name}':\n {e}") + check( + False, + RuntimeError, + f"An error occurred when running the function '{name}':\n {e}", + ) else: self.n_func = 1 - if self.name not in ['Triangular', 'Rectangular']: ## custom function + if self.name not in ["Triangular", "Rectangular"]: ## custom function ## Add the function to the globals try: - code = 'import torch\n@torch.fx.wrap\n' + self.function + code = "import torch\n@torch.fx.wrap\n" + self.function exec(code, globals()) except Exception as e: - check(False, RuntimeError, f"An error occurred when running the function '{self.name}':\n {e}") + check( + False, + RuntimeError, + f"An error occurred when running the function '{self.name}':\n {e}", + ) def forward(self, x): res = torch.zeros_like(x).repeat(1, 1, self.dimension) - if self.function == 'Triangular': + if self.function == "Triangular": for i in range(len(self.centers)): slicing(res, torch.tensor(i), triangular(x, i, self.centers)) - elif self.function == 'Rectangular': + elif self.function == "Rectangular": for i in range(len(self.centers)): slicing(res, torch.tensor(i), rectangular(x, i, self.centers)) else: ## Custom_function @@ -212,7 +282,11 @@ def forward(self, x): # Retrieve the function object from the globals dictionary function_to_call = globals()[self.name] for i in range(len(self.centers)): - slicing(res, torch.tensor(i), custom_function(function_to_call, x, i, self.centers)) + slicing( + res, + torch.tensor(i), + custom_function(function_to_call, x, i, self.centers), + ) else: ## we have multiple functions for i in range(len(self.centers)): if i >= self.n_func: @@ -220,14 +294,21 @@ def forward(self, x): else: func_idx = i function_to_call = globals()[self.name[func_idx]] - slicing(res, torch.tensor(i), custom_function(function_to_call, x, i, self.centers)) + slicing( + res, + torch.tensor(i), + custom_function(function_to_call, x, i, self.centers), + ) return res + @torch.fx.wrap def slicing(res, i, x): - res[:, :, i:i + 1] = x + res[:, :, i : i + 1] = x + def createFuzzify(self, *params): return Fuzzify_Layer(params[0]) + setattr(Model, fuzzify_relation_name, createFuzzify) diff --git a/nnodely/layers/input.py b/nnodely/layers/input.py index 061f955d..7b7a91ef 100644 --- a/nnodely/layers/input.py +++ b/nnodely/layers/input.py @@ -6,6 +6,7 @@ from nnodely.layers.part import SamplePart, TimePart from nnodely.layers.timeoperation import Differentiate, Integrate + class Input(NeuObj): """ Represents an Input in the neural network model. @@ -30,7 +31,8 @@ class Input(NeuObj): json : dict A dictionary containing the configuration of the Input. """ - def __init__(self, name:str, *, dimensions:int = 1): + + def __init__(self, name: str, *, dimensions: int = 1): """ Initializes the Input object. @@ -44,12 +46,12 @@ def __init__(self, name:str, *, dimensions:int = 1): The number of dimensions for the Input. Default is 1. """ NeuObj.__init__(self, name) - check(type(dimensions) == int, TypeError,"The dimensions must be a integer") - self.json['Inputs'][self.name] = {'dim': dimensions } - self.dim = {'dim': dimensions} + check(type(dimensions) == int, TypeError, "The dimensions must be a integer") + self.json["Inputs"][self.name] = {"dim": dimensions} + self.dim = {"dim": dimensions} @enforce_types - def tw(self, tw:int|float|list, offset:int|float|None = None) -> Stream: + def tw(self, tw: int | float | list, offset: int | float | None = None) -> Stream: """ Selects a time window for the Input. @@ -75,23 +77,39 @@ def tw(self, tw:int|float|list, offset:int|float|None = None) -> Stream: dim = copy.deepcopy(self.dim) json = copy.deepcopy(self.json) if type(tw) is list: - check(len(tw) == 2, TypeError, "The time window must be a list of two elements.") - check(tw[1] > tw[0], ValueError, "The dimension of the sample window must be positive") - json['Inputs'][self.name]['tw'] = tw + check( + len(tw) == 2, + TypeError, + "The time window must be a list of two elements.", + ) + check( + tw[1] > tw[0], + ValueError, + "The dimension of the sample window must be positive", + ) + json["Inputs"][self.name]["tw"] = tw tw = tw[1] - tw[0] else: - json['Inputs'][self.name]['tw'] = [-tw, 0] + json["Inputs"][self.name]["tw"] = [-tw, 0] check(tw > 0, ValueError, "The time window must be positive") - dim['tw'] = tw + dim["tw"] = tw if offset is not None: - check(json['Inputs'][self.name]['tw'][0] <= offset < json['Inputs'][self.name]['tw'][1], - IndexError, - "The offset must be inside the time window") - return TimePart(Stream(self.name, json, dim), json['Inputs'][self.name]['tw'][0], json['Inputs'][self.name]['tw'][1], offset) - + check( + json["Inputs"][self.name]["tw"][0] + <= offset + < json["Inputs"][self.name]["tw"][1], + IndexError, + "The offset must be inside the time window", + ) + return TimePart( + Stream(self.name, json, dim), + json["Inputs"][self.name]["tw"][0], + json["Inputs"][self.name]["tw"][1], + offset, + ) @enforce_types - def sw(self, sw:int|list, offset:int|None = None) -> Stream: + def sw(self, sw: int | list, offset: int | None = None) -> Stream: """ Selects a sample window for the Input. @@ -119,24 +137,45 @@ def sw(self, sw:int|list, offset:int|None = None) -> Stream: dim = copy.deepcopy(self.dim) json = copy.deepcopy(self.json) if type(sw) is list: - check(len(sw) == 2, TypeError, "The sample window must be a list of two elements.") - check(type(sw[0]) == int and type(sw[1]) == int, TypeError, "The sample window must be integer") - check(sw[1] > sw[0], ValueError, "The dimension of the sample window must be positive") - json['Inputs'][self.name]['sw'] = sw + check( + len(sw) == 2, + TypeError, + "The sample window must be a list of two elements.", + ) + check( + type(sw[0]) == int and type(sw[1]) == int, + TypeError, + "The sample window must be integer", + ) + check( + sw[1] > sw[0], + ValueError, + "The dimension of the sample window must be positive", + ) + json["Inputs"][self.name]["sw"] = sw sw = sw[1] - sw[0] else: check(type(sw) == int, TypeError, "The sample window must be integer") - json['Inputs'][self.name]['sw'] = [-sw, 0] + json["Inputs"][self.name]["sw"] = [-sw, 0] check(sw > 0, ValueError, "The sample window must be positive") - dim['sw'] = sw + dim["sw"] = sw if offset is not None: - check(json['Inputs'][self.name]['sw'][0] <= offset < json['Inputs'][self.name]['sw'][1], - IndexError, - "The offset must be inside the sample window") - return SamplePart(Stream(self.name, json, dim), json['Inputs'][self.name]['sw'][0], json['Inputs'][self.name]['sw'][1], offset) + check( + json["Inputs"][self.name]["sw"][0] + <= offset + < json["Inputs"][self.name]["sw"][1], + IndexError, + "The offset must be inside the sample window", + ) + return SamplePart( + Stream(self.name, json, dim), + json["Inputs"][self.name]["sw"][0], + json["Inputs"][self.name]["sw"][1], + offset, + ) @enforce_types - def z(self, delay:int) -> Stream: + def z(self, delay: int) -> Stream: """ Considering the Zeta transform notation. The function is used to selects a unitary delay from the Input. @@ -157,9 +196,14 @@ def z(self, delay:int) -> Stream: dim = copy.deepcopy(self.dim) json = copy.deepcopy(self.json) sw = [(-delay) - 1, (-delay)] - json['Inputs'][self.name]['sw'] = sw - dim['sw'] = sw[1] - sw[0] - return SamplePart(Stream(self.name, json, dim), json['Inputs'][self.name]['sw'][0], json['Inputs'][self.name]['sw'][1], None) + json["Inputs"][self.name]["sw"] = sw + dim["sw"] = sw[1] - sw[0] + return SamplePart( + Stream(self.name, json, dim), + json["Inputs"][self.name]["sw"][0], + json["Inputs"][self.name]["sw"][1], + None, + ) @enforce_types def last(self) -> Stream: @@ -186,7 +230,14 @@ def next(self) -> Stream: return self.z(-1) @enforce_types - def s(self, order:int, *, der_name:str|None = None, int_name:str|None = None, method:str = 'euler') -> Stream: + def s( + self, + order: int, + *, + der_name: str | None = None, + int_name: str | None = None, + method: str = "euler", + ) -> Stream: """ Considering the Laplace transform notation. The function is used to operate an integral or derivate operation on the input. The order of the integral or the derivative operation is indicated by the order parameter. @@ -203,19 +254,25 @@ def s(self, order:int, *, der_name:str|None = None, int_name:str|None = None, me Stream A Stream of the signal represents the integral or derivation operation. """ - check(order != 0, ValueError, "The order must be a positive or negative integer not a zero") + check( + order != 0, + ValueError, + "The order must be a positive or negative integer not a zero", + ) if order > 0: o = self.last() for i in range(order): - o = Differentiate(o, der_name = der_name, int_name = int_name, method = method) + o = Differentiate( + o, der_name=der_name, int_name=int_name, method=method + ) elif order < 0: o = self.last() for i in range(-order): - o = Integrate(o, der_name = der_name, int_name = int_name, method = method) + o = Integrate(o, der_name=der_name, int_name=int_name, method=method) return o @enforce_types - def connect(self, obj:Stream) -> "Input": + def connect(self, obj: Stream) -> "Input": """ Update and return the current Input with a given Stream object. @@ -236,17 +293,24 @@ def connect(self, obj:Stream) -> "Input": KeyError If the Input variable is already connected. """ - check(type(obj) is Stream, TypeError, - f"The {obj} must be a Stream and not a {type(obj)}.") + check( + type(obj) is Stream, + TypeError, + f"The {obj} must be a Stream and not a {type(obj)}.", + ) self.json = merge(self.json, obj.json) - check('closedLoop' not in self.json['Inputs'][self.name] or 'connect' not in self.json['Inputs'][self.name], KeyError, - f"The Input variable {self.name} is already connected.") - self.json['Inputs'][self.name]['connect'] = obj.name - self.json['Inputs'][self.name]['local'] = 1 + check( + "closedLoop" not in self.json["Inputs"][self.name] + or "connect" not in self.json["Inputs"][self.name], + KeyError, + f"The Input variable {self.name} is already connected.", + ) + self.json["Inputs"][self.name]["connect"] = obj.name + self.json["Inputs"][self.name]["local"] = 1 return self @enforce_types - def closedLoop(self, obj:Stream) -> "Input": + def closedLoop(self, obj: Stream) -> "Input": """ Update and return the current Input in a closed loop with a given Stream object. @@ -268,40 +332,58 @@ def closedLoop(self, obj:Stream) -> "Input": If the Input variable is already connected. """ from nnodely.layers.input import Input - check(type(obj) is Stream, TypeError, - f"The {obj} must be a Stream and not a {type(obj)}.") + + check( + type(obj) is Stream, + TypeError, + f"The {obj} must be a Stream and not a {type(obj)}.", + ) self.json = merge(self.json, obj.json) - check('closedLoop' not in self.json['Inputs'][self.name] or 'connect' not in self.json['Inputs'][self.name], - KeyError, - f"The Input variable {self.name} is already connected.") - self.json['Inputs'][self.name]['closedLoop'] = self.name - self.json['Inputs'][self.name]['local'] = 1 + check( + "closedLoop" not in self.json["Inputs"][self.name] + or "connect" not in self.json["Inputs"][self.name], + KeyError, + f"The Input variable {self.name} is already connected.", + ) + self.json["Inputs"][self.name]["closedLoop"] = self.name + self.json["Inputs"][self.name]["local"] = 1 return self def __str__(self): - return stream_to_str(self, 'Input') + return stream_to_str(self, "Input") def __repr__(self): return self.__str__() + # connect operation -connect_name = 'connect' -closedloop_name = 'closedLoop' +connect_name = "connect" +closedloop_name = "closedLoop" + class Connect(Stream, ToStream): @enforce_types - def __init__(self, obj1:Stream, obj2:Input, *, local:bool=False) -> Stream: - super().__init__(obj1.name,merge(obj1.json, obj2.json),obj1.dim) - check(closedloop_name not in self.json['Inputs'][obj2.name] or connect_name not in self.json['Inputs'][obj2.name], - KeyError,f"The input variable {obj2.name} is already connected.") - self.json['Inputs'][obj2.name][connect_name] = obj1.name - self.json['Inputs'][obj2.name]['local'] = int(local) + def __init__(self, obj1: Stream, obj2: Input, *, local: bool = False) -> Stream: + super().__init__(obj1.name, merge(obj1.json, obj2.json), obj1.dim) + check( + closedloop_name not in self.json["Inputs"][obj2.name] + or connect_name not in self.json["Inputs"][obj2.name], + KeyError, + f"The input variable {obj2.name} is already connected.", + ) + self.json["Inputs"][obj2.name][connect_name] = obj1.name + self.json["Inputs"][obj2.name]["local"] = int(local) + class ClosedLoop(Stream, ToStream): @enforce_types - def __init__(self, obj1:Stream, obj2:Input, *, local:bool=False) -> Stream: + def __init__(self, obj1: Stream, obj2: Input, *, local: bool = False) -> Stream: super().__init__(obj1.name, merge(obj1.json, obj2.json), obj1.dim) - check(closedloop_name not in self.json['Inputs'][obj2.name] or connect_name not in self.json['Inputs'][obj2.name], - KeyError, f"The input variable {obj2.name} is already connected.") - self.json['Inputs'][obj2.name][closedloop_name] = obj1.name - self.json['Inputs'][obj2.name]['local'] = int(local) \ No newline at end of file + check( + closedloop_name not in self.json["Inputs"][obj2.name] + or connect_name not in self.json["Inputs"][obj2.name], + KeyError, + f"The input variable {obj2.name} is already connected.", + ) + self.json["Inputs"][obj2.name][closedloop_name] = obj1.name + self.json["Inputs"][obj2.name]["local"] = int(local) diff --git a/nnodely/layers/interpolation.py b/nnodely/layers/interpolation.py index 8f1e90c0..7214df22 100644 --- a/nnodely/layers/interpolation.py +++ b/nnodely/layers/interpolation.py @@ -7,7 +7,9 @@ from nnodely.support.utils import check, enforce_types from nnodely.support.jsonutils import merge -interpolation_relation_name = 'Interpolation' +interpolation_relation_name = "Interpolation" + + class Interpolation(NeuObj): """ Represents an Interpolation relation in the neural network model. @@ -35,40 +37,64 @@ class Interpolation(NeuObj): >>> x = Input('x') >>> rel1 = Interpolation(x_points=x_points,y_points=y_points, mode='linear')(x.last()) - + >>> out = Output('out',rel1) """ @enforce_types - def __init__(self, x_points:list, - y_points:list, *, - mode:str|None = 'linear'): + def __init__(self, x_points: list, y_points: list, *, mode: str | None = "linear"): self.relation_name = interpolation_relation_name self.x_points = x_points self.y_points = y_points self.mode = mode - self.available_modes = ['linear', 'polynomial'] - - super().__init__('P' + interpolation_relation_name + str(NeuObj.count)) - check(len(x_points) == len(y_points), ValueError, 'The x_points and y_points must have the same length.') - check(mode in self.available_modes, ValueError, f'The mode must be one of {self.available_modes}.') - check(len(torch.tensor(x_points).shape) == 1, ValueError, 'The x_points must be a 1D tensor.') - check(len(torch.tensor(y_points).shape) == 1, ValueError, 'The y_points must be a 1D tensor.') + self.available_modes = ["linear", "polynomial"] + + super().__init__("P" + interpolation_relation_name + str(NeuObj.count)) + check( + len(x_points) == len(y_points), + ValueError, + "The x_points and y_points must have the same length.", + ) + check( + mode in self.available_modes, + ValueError, + f"The mode must be one of {self.available_modes}.", + ) + check( + len(torch.tensor(x_points).shape) == 1, + ValueError, + "The x_points must be a 1D tensor.", + ) + check( + len(torch.tensor(y_points).shape) == 1, + ValueError, + "The y_points must be a 1D tensor.", + ) @enforce_types - def __call__(self, obj:Stream) -> Stream: + def __call__(self, obj: Stream) -> Stream: stream_name = interpolation_relation_name + str(Stream.count) - check(type(obj) is Stream, TypeError, f"The type of {obj} is {type(obj)} and is not supported for Interpolation operation.") - - stream_json = merge(self.json,obj.json) - stream_json['Relations'][stream_name] = [interpolation_relation_name, [obj.name], self.x_points, self.y_points, self.mode] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Interpolation operation.", + ) + + stream_json = merge(self.json, obj.json) + stream_json["Relations"][stream_name] = [ + interpolation_relation_name, + [obj.name], + self.x_points, + self.y_points, + self.mode, + ] return Stream(stream_name, stream_json, obj.dim) class Interpolation_Layer(nn.Module): - def __init__(self, x_points, y_points, mode='linear'): + def __init__(self, x_points, y_points, mode="linear"): super(Interpolation_Layer, self).__init__() self.mode = mode ## Sort the points @@ -83,13 +109,13 @@ def __init__(self, x_points, y_points, mode='linear'): self.y_points = self.y_points.unsqueeze(-1) def forward(self, x): - if self.mode == 'linear': + if self.mode == "linear": return self.linear_interpolation(x) else: raise NotImplementedError - + def linear_interpolation(self, x): - # Inputs: + # Inputs: # x: query point, a tensor of shape torch.Size([N, 1, 1]) # x_data: map of x values, sorted in ascending order, a tensor of shape torch.Size([Q, 1]) # y_data: map of y values, a tensor of shape torch.Size([Q, 1]) @@ -97,14 +123,18 @@ def linear_interpolation(self, x): # y: interpolated value at x, a tensor of shape torch.Size([N, 1, 1]) # Saturate x to the range of x_data - x = torch.min(torch.max(x,self.x_points[0]),self.x_points[-1]) + x = torch.min(torch.max(x, self.x_points[0]), self.x_points[-1]) # Find the index of the closest value in x_data - idx = torch.argmin(torch.abs(self.x_points[:-1] - x),dim=1) + idx = torch.argmin(torch.abs(self.x_points[:-1] - x), dim=1) # Linear interpolation - y = self.y_points[idx] + (self.y_points[idx+1] - self.y_points[idx])/(self.x_points[idx+1] - self.x_points[idx])*(x - self.x_points[idx]) + y = self.y_points[idx] + (self.y_points[idx + 1] - self.y_points[idx]) / ( + self.x_points[idx + 1] - self.x_points[idx] + ) * (x - self.x_points[idx]) return y + def createInterpolation(self, *inputs): return Interpolation_Layer(x_points=inputs[0], y_points=inputs[1], mode=inputs[2]) -setattr(Model, interpolation_relation_name, createInterpolation) \ No newline at end of file + +setattr(Model, interpolation_relation_name, createInterpolation) diff --git a/nnodely/layers/linear.py b/nnodely/layers/linear.py index 0a6ac3ed..d7511107 100644 --- a/nnodely/layers/linear.py +++ b/nnodely/layers/linear.py @@ -12,9 +12,11 @@ from nnodely.support.jsonutils import merge from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) -linear_relation_name = 'Linear' +linear_relation_name = "Linear" + class Linear(NeuObj, AutoToStream): """ @@ -77,14 +79,18 @@ class Linear(NeuObj, AutoToStream): """ @enforce_types - def __init__(self, output_dimension:int|None = None, *, - W_init:Callable|str|None = None, - W_init_params:dict|None = None, - b_init:Callable|str|None = None, - b_init_params:dict|None = None, - W:Parameter|str|None = None, - b:bool|str|Parameter|None = None, - dropout:int|float = 0): + def __init__( + self, + output_dimension: int | None = None, + *, + W_init: Callable | str | None = None, + W_init_params: dict | None = None, + b_init: Callable | str | None = None, + b_init_params: dict | None = None, + W: Parameter | str | None = None, + b: bool | str | Parameter | None = None, + dropout: int | float = 0, + ): self.W = W self.b = b @@ -92,57 +98,113 @@ def __init__(self, output_dimension:int|None = None, *, self.Wname = None self.dropout = dropout - super().__init__('P' + linear_relation_name + str(NeuObj.count)) + super().__init__("P" + linear_relation_name + str(NeuObj.count)) if type(self.W) is Parameter: - check('tw' not in self.W.dim.keys() and 'sw' not in self.W.dim.keys(), TypeError, f'The "W" must no have time dimension but was {W.dim}.') - check(len(self.W.dim['dim']) == 2, ValueError,'The "W" dimensions must be a list of 2.') - self.output_dimension = self.W.dim['dim'][1] + check( + "tw" not in self.W.dim.keys() and "sw" not in self.W.dim.keys(), + TypeError, + f'The "W" must no have time dimension but was {W.dim}.', + ) + check( + len(self.W.dim["dim"]) == 2, + ValueError, + 'The "W" dimensions must be a list of 2.', + ) + self.output_dimension = self.W.dim["dim"][1] if output_dimension is not None: - check(self.W.dim['dim'][1] == output_dimension, ValueError, 'output_dimension must be equal to the second dim of "W".') + check( + self.W.dim["dim"][1] == output_dimension, + ValueError, + 'output_dimension must be equal to the second dim of "W".', + ) self.Wname = self.W.name W_json = W.json else: self.output_dimension = 1 if output_dimension is None else output_dimension - self.Wname = W if type(W) is str else self.name + 'W' - W_json = Parameter(name=self.Wname, dimensions=self.output_dimension, init=W_init, init_params=W_init_params).json - self.json = merge(self.json,W_json) + self.Wname = W if type(W) is str else self.name + "W" + W_json = Parameter( + name=self.Wname, + dimensions=self.output_dimension, + init=W_init, + init_params=W_init_params, + ).json + self.json = merge(self.json, W_json) if self.b is not None and self.b is not False: if type(self.b) is Parameter: - check('tw' not in self.b.dim and 'sw' not in self.b.dim, TypeError, f'The "bias" must no have a time dimensions but got {self.b.dim}.') - check(type(self.b.dim['dim']) is int, TypeError, 'The "b" dimensions must be an integer.') - check(self.b.dim['dim'] == self.output_dimension, ValueError,'output_dimension must be equal to the dim of the "b".') + check( + "tw" not in self.b.dim and "sw" not in self.b.dim, + TypeError, + f'The "bias" must no have a time dimensions but got {self.b.dim}.', + ) + check( + type(self.b.dim["dim"]) is int, + TypeError, + 'The "b" dimensions must be an integer.', + ) + check( + self.b.dim["dim"] == self.output_dimension, + ValueError, + 'output_dimension must be equal to the dim of the "b".', + ) self.bname = self.b.name b_json = self.b.json else: - self.bname = b if type(self.b) is str else self.name + 'b' - b_json = Parameter(name=self.bname, dimensions=self.output_dimension, init=b_init, init_params=b_init_params).json - self.json = merge(self.json,b_json) + self.bname = b if type(self.b) is str else self.name + "b" + b_json = Parameter( + name=self.bname, + dimensions=self.output_dimension, + init=b_init, + init_params=b_init_params, + ).json + self.json = merge(self.json, b_json) self.json_stream = {} @enforce_types - def __call__(self, obj:Stream) -> Stream: + def __call__(self, obj: Stream) -> Stream: stream_name = linear_relation_name + str(Stream.count) - check(type(obj) is Stream, TypeError,f"The type of {obj} is {type(obj)} and is not supported for Linear operation.") - window = 'tw' if 'tw' in obj.dim else ('sw' if 'sw' in obj.dim else None) - - json_stream_name = obj.dim['dim'] - if obj.dim['dim'] not in self.json_stream: + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Linear operation.", + ) + window = "tw" if "tw" in obj.dim else ("sw" if "sw" in obj.dim else None) + + json_stream_name = obj.dim["dim"] + if obj.dim["dim"] not in self.json_stream: if len(self.json_stream) > 0: - log.warning(f"The Linear {self.name} was called with inputs with different dimensions. If both Linear enter in the model an error will be raised.") + log.warning( + f"The Linear {self.name} was called with inputs with different dimensions. If both Linear enter in the model an error will be raised." + ) self.json_stream[json_stream_name] = copy.deepcopy(self.json) - self.json_stream[json_stream_name]['Parameters'][self.Wname]['dim'] = [obj.dim['dim'],self.output_dimension,] + self.json_stream[json_stream_name]["Parameters"][self.Wname]["dim"] = [ + obj.dim["dim"], + self.output_dimension, + ] if type(self.W) is Parameter: - check(self.json['Parameters'][self.Wname]['dim'][0] == obj.dim['dim'], ValueError, - 'the input dimension must be equal to the first dim of the parameter') - - stream_json = merge(self.json_stream[json_stream_name],obj.json) - stream_json['Relations'][stream_name] = [linear_relation_name, [obj.name], self.Wname, self.bname, self.dropout] - return Stream(stream_name, stream_json,{'dim': self.output_dimension, window:obj.dim[window]}) + check( + self.json["Parameters"][self.Wname]["dim"][0] == obj.dim["dim"], + ValueError, + "the input dimension must be equal to the first dim of the parameter", + ) + + stream_json = merge(self.json_stream[json_stream_name], obj.json) + stream_json["Relations"][stream_name] = [ + linear_relation_name, + [obj.name], + self.Wname, + self.bname, + self.dropout, + ] + return Stream( + stream_name, + stream_json, + {"dim": self.output_dimension, window: obj.dim[window]}, + ) class Linear_Layer(nn.Module): @@ -155,15 +217,19 @@ def __init__(self, weights, bias=None, dropout=0): def forward(self, x): # x is expected to be of shape [batch, window, input_dimension] # Using torch.einsum for batch matrix multiplication - y = torch.einsum('bwi,io->bwo', x, self.weights) # y will have shape [batch, window, output_features] + y = torch.einsum( + "bwi,io->bwo", x, self.weights + ) # y will have shape [batch, window, output_features] if self.bias is not None: - y += self.bias + y += self.bias # Add dropout if necessary if self.dropout is not None: y = self.dropout(y) return y + def createLinear(self, *inputs): return Linear_Layer(weights=inputs[0], bias=inputs[1], dropout=inputs[2]) + setattr(Model, linear_relation_name, createLinear) diff --git a/nnodely/layers/localmodel.py b/nnodely/layers/localmodel.py index 095cd0b9..adab79bd 100644 --- a/nnodely/layers/localmodel.py +++ b/nnodely/layers/localmodel.py @@ -6,7 +6,8 @@ from nnodely.layers.part import Select from nnodely.support.utils import check, enforce_types -localmodel_relation_name = 'LocalModel' +localmodel_relation_name = "LocalModel" + class LocalModel(NeuObj): """ @@ -15,9 +16,9 @@ class LocalModel(NeuObj): Parameters ---------- input_function : Callable, optional - A callable function to process the inputs. + A callable function to process the inputs. output_function : Callable, optional - A callable function to process the outputs. + A callable function to process the outputs. pass_indexes : bool, optional A boolean indicating whether to pass indexes to the functions. Default is False. @@ -37,39 +38,54 @@ class LocalModel(NeuObj): .. include:: /examples_basics/layer_module_ex/localmodel.rst """ + @enforce_types - def __init__(self, input_function:Callable|None = None, - output_function:Callable|None = None, *, - pass_indexes:bool = False): + def __init__( + self, + input_function: Callable | None = None, + output_function: Callable | None = None, + *, + pass_indexes: bool = False, + ): self.relation_name = localmodel_relation_name self.pass_indexes = pass_indexes super().__init__(localmodel_relation_name + str(NeuObj.count)) - self.json['Functions'][self.name] = {} + self.json["Functions"][self.name] = {} if input_function is not None: - check(callable(input_function), TypeError, 'The input_function must be callable') + check( + callable(input_function), + TypeError, + "The input_function must be callable", + ) self.input_function = input_function if output_function is not None: - check(callable(output_function), TypeError, 'The output_function must be callable') + check( + callable(output_function), + TypeError, + "The output_function must be callable", + ) self.output_function = output_function @enforce_types - def __call__(self, inputs:Stream|tuple, activations:Stream|tuple= None): + def __call__(self, inputs: Stream | tuple, activations: Stream | tuple = None): out_sum = [] if type(activations) is not tuple: activations = (activations,) - self.___activations_matrix(activations,inputs,out_sum) + self.___activations_matrix(activations, inputs, out_sum) out = out_sum[0] - for ind in range(1,len(out_sum)): + for ind in range(1, len(out_sum)): out = out + out_sum[ind] return out # Definisci una funzione ricorsiva per annidare i cicli for def ___activations_matrix(self, activations, inputs, out, idx=0, idx_list=[]): if idx != len(activations): - for i in range(activations[idx].dim['dim']): - self.___activations_matrix(activations, inputs, out, idx+1, idx_list+[i]) + for i in range(activations[idx].dim["dim"]): + self.___activations_matrix( + activations, inputs, out, idx + 1, idx_list + [i] + ) else: if self.input_function is not None: if len(inspect.signature(self.input_function).parameters) == 0: @@ -89,12 +105,16 @@ def ___activations_matrix(self, activations, inputs, out, idx=0, idx_list=[]): else: out_in = self.input_function(inputs) else: - check(type(inputs) is not tuple, TypeError, 'The input cannot be a tuple without input_function') + check( + type(inputs) is not tuple, + TypeError, + "The input cannot be a tuple without input_function", + ) out_in = inputs act = Select(activations[0], idx_list[0]) - for ind, i in enumerate(idx_list[1:]): - act = act * Select(activations[ind+1], i) + for ind, i in enumerate(idx_list[1:]): + act = act * Select(activations[ind + 1], i) prod = out_in * act diff --git a/nnodely/layers/neuralODE.py b/nnodely/layers/neuralODE.py index 787498ff..265ec1ed 100644 --- a/nnodely/layers/neuralODE.py +++ b/nnodely/layers/neuralODE.py @@ -9,9 +9,11 @@ from nnodely.support.jsonutils import merge from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) -ode_relation_name = 'NeuralODE' +ode_relation_name = "NeuralODE" + class NeuralODE(NeuObj): """Neural ODE layer implementing continuous-depth models. @@ -24,13 +26,15 @@ class NeuralODE(NeuObj): """ @enforce_types - def __init__(self, - func: ParamFun, - dt: float, - rtol: float = 1e-7, - atol: float = 1e-9, - method: str = 'dopri5') -> Stream: - super().__init__('F'+ode_relation_name + str(NeuObj.count)) + def __init__( + self, + func: ParamFun, + dt: float, + rtol: float = 1e-7, + atol: float = 1e-9, + method: str = "dopri5", + ) -> Stream: + super().__init__("F" + ode_relation_name + str(NeuObj.count)) self.func = func self.dt = dt @@ -38,21 +42,25 @@ def __init__(self, self.atol = atol self.method = method - code = textwrap.dedent(inspect.getsource(func.param_fun)).replace('\"', '\'') - code = 'def ' + f'{self.name}' + '(state, *weights):\n' + \ - ' from nnodely.support.odeint.adjoint import odeint_adjoint as odeint\n ' + \ - code.replace('\n', '\n ') + \ - '\n' + \ - f' ans = odeint(lambda t, y: {func.param_fun.__name__}(t, y, *weights), state, t=torch.tensor([0.0, {self.dt}]), rtol={self.rtol}, atol={self.atol}, method=\'{self.method}\', adjoint_params=list(weights))' + \ - f'\n return ans[-1]\n' - - self.json['Functions'][self.name] = { - 'code' : code, - 'name' : f'{self.name}', - 'rtol' : rtol, - 'atol' : atol, - 'method' : method, - 'dt' : dt + code = textwrap.dedent(inspect.getsource(func.param_fun)).replace('"', "'") + code = ( + "def " + + f"{self.name}" + + "(state, *weights):\n" + + " from nnodely.support.odeint.adjoint import odeint_adjoint as odeint\n " + + code.replace("\n", "\n ") + + "\n" + + f" ans = odeint(lambda t, y: {func.param_fun.__name__}(t, y, *weights), state, t=torch.tensor([0.0, {self.dt}]), rtol={self.rtol}, atol={self.atol}, method='{self.method}', adjoint_params=list(weights))" + + f"\n return ans[-1]\n" + ) + + self.json["Functions"][self.name] = { + "code": code, + "name": f"{self.name}", + "rtol": rtol, + "atol": atol, + "method": method, + "dt": dt, } self.json_stream = {} @@ -63,28 +71,34 @@ def __call__(self, *obj: Stream) -> Stream: input_names = [] for ind, o in enumerate(obj): o = toStream(o) - check(type(o) is Stream, TypeError, - f"The type of {o} is {type(o)} and is not supported for ParamFun operation.") + check( + type(o) is Stream, + TypeError, + f"The type of {o} is {type(o)} and is not supported for ParamFun operation.", + ) stream_json = merge(stream_json, o.json) input_names.append(o.name) - stream_json['Relations'][stream_name] = [ode_relation_name, input_names, self.name] + stream_json["Relations"][stream_name] = [ + ode_relation_name, + input_names, + self.name, + ] return Stream(stream_name, stream_json, obj[0].dim) class ODE_Layer(nn.Module): - def __init__(self, func): super().__init__() - self.name = func['name'] - self.dt = func['dt'] - self.rtol = func['rtol'] - self.atol = func['atol'] - self.method = func['method'] + self.name = func["name"] + self.dt = func["dt"] + self.rtol = func["rtol"] + self.atol = func["atol"] + self.method = func["method"] ## Add the function to the globals try: - code = 'import torch\n@torch.fx.wrap\n' + func['code'] - #print(f"Defining ODE function:\n{code}") + code = "import torch\n@torch.fx.wrap\n" + func["code"] + # print(f"Defining ODE function:\n{code}") exec(code, globals()) except Exception as e: print(f"An error occurred: {e}") @@ -95,10 +109,12 @@ def forward(self, *inputs): weights = list(inputs)[1:] return function_to_call(list(inputs)[0], *weights) # Return the last state + def createODE(self, *func_params): # for key, value in func_params[0].items(): # print(f"{key}: {value}") - + return ODE_Layer(func_params[0]) -setattr(Model, ode_relation_name, createODE) \ No newline at end of file + +setattr(Model, ode_relation_name, createODE) diff --git a/nnodely/layers/output.py b/nnodely/layers/output.py index f80f3670..9a28295a 100644 --- a/nnodely/layers/output.py +++ b/nnodely/layers/output.py @@ -5,8 +5,10 @@ from nnodely.support.jsonutils import stream_to_str from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.INFO) + class Output(NeuObj): """ Represents an output in the neural network model. This relation is what the network will give as output during inference. @@ -27,8 +29,9 @@ class Output(NeuObj): dim : dict A dictionary containing the dimensions of the output. """ + @enforce_types - def __init__(self, name:str, relation:Stream): + def __init__(self, name: str, relation: Stream): """ Initializes the Output object. @@ -41,12 +44,12 @@ def __init__(self, name:str, relation:Stream): """ super().__init__(name, relation.json, relation.dim) log.debug(f"Output {name}") - self.json['Outputs'][name] = {} - self.json['Outputs'][name] = relation.name - log.debug("\n"+pformat(self.json)) + self.json["Outputs"][name] = {} + self.json["Outputs"][name] = relation.name + log.debug("\n" + pformat(self.json)) def __str__(self): - return stream_to_str(self, 'Output') + return stream_to_str(self, "Output") def __repr__(self): - return self.__str__() \ No newline at end of file + return self.__str__() diff --git a/nnodely/layers/parameter.py b/nnodely/layers/parameter.py index c3bc129f..ae359ae1 100644 --- a/nnodely/layers/parameter.py +++ b/nnodely/layers/parameter.py @@ -10,6 +10,7 @@ def is_numpy_float(var): return isinstance(var, (np.float16, np.float32, np.float64)) + class Constant(NeuObj, Relation): """ Represents a constant value in the neural network model. @@ -39,11 +40,16 @@ class Constant(NeuObj, Relation): .. include:: /examples_basics/parameter_module_ex/constant.rst """ + @enforce_types - def __init__(self, name:str, - values:list|float|int|np.ndarray, *, - tw:float|int|None = None, - sw:int|None = None): + def __init__( + self, + name: str, + values: list | float | int | np.ndarray, + *, + tw: float | int | None = None, + sw: int | None = None, + ): NeuObj.__init__(self, name) values = np.array(values, dtype=NP_DTYPE) @@ -52,26 +58,45 @@ def __init__(self, name:str, self.dim = {} if tw is not None: - check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if tw is set.") + check( + len(shape) >= 2, + ValueError, + "The dimension must be at least 2 if tw is set.", + ) check(sw is None, ValueError, "If tw is set sw must be None") dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:]) - self.dim['tw'] = tw + self.dim["tw"] = tw elif sw is not None: - check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if sw is set.") - self.dim['sw'] = sw - check(shape[0] == self.dim['sw'],ValueError, f"The sw = {sw} is different from sw = {shape[0]} of the values.") + check( + len(shape) >= 2, + ValueError, + "The dimension must be at least 2 if sw is set.", + ) + self.dim["sw"] = sw + check( + shape[0] == self.dim["sw"], + ValueError, + f"The sw = {sw} is different from sw = {shape[0]} of the values.", + ) dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:]) else: - dimensions = 1 if len(shape[0:]) == 0 else shape[0] if len(shape[0:]) == 1 else list(shape[0:]) + dimensions = ( + 1 + if len(shape[0:]) == 0 + else shape[0] + if len(shape[0:]) == 1 + else list(shape[0:]) + ) - self.dim['dim'] = dimensions + self.dim["dim"] = dimensions # deepcopy dimention information inside Parameters - self.json['Constants'][self.name] = copy.deepcopy(self.dim) - if type(values) in (float,int): - self.json['Constants'][self.name]['values'] = [values] + self.json["Constants"][self.name] = copy.deepcopy(self.dim) + if type(values) in (float, int): + self.json["Constants"][self.name]["values"] = [values] else: - self.json['Constants'][self.name]['values'] = values + self.json["Constants"][self.name]["values"] = values + class Parameter(NeuObj, Relation): """ @@ -113,29 +138,34 @@ class Parameter(NeuObj, Relation): .. include:: /examples_basics/parameter_module_ex/parameter.rst """ + @enforce_types - def __init__(self, name:str, - dimensions:int|list|tuple|None = None, *, - tw:float|int|None = None, - sw:int|None = None, - values:list|float|int|np.ndarray|None = None, - init:Callable|str|None = None, - init_params:dict|None = None): + def __init__( + self, + name: str, + dimensions: int | list | tuple | None = None, + *, + tw: float | int | None = None, + sw: int | None = None, + values: list | float | int | np.ndarray | None = None, + init: Callable | str | None = None, + init_params: dict | None = None, + ): NeuObj.__init__(self, name) dimensions = list(dimensions) if type(dimensions) is tuple else dimensions if values is None: if dimensions is None: dimensions = 1 - self.dim = {'dim': dimensions} + self.dim = {"dim": dimensions} if tw is not None: check(sw is None, ValueError, "If tw is set sw must be None") - self.dim['tw'] = tw + self.dim["tw"] = tw elif sw is not None: - self.dim['sw'] = sw + self.dim["sw"] = sw # deepcopy dimention information inside Parameters - self.json['Parameters'][self.name] = copy.deepcopy(self.dim) + self.json["Parameters"][self.name] = copy.deepcopy(self.dim) else: values = np.array(values, dtype=NP_DTYPE) shape = values.shape @@ -143,41 +173,68 @@ def __init__(self, name:str, self.dim = {} if tw is not None: - check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if tw is set.") + check( + len(shape) >= 2, + ValueError, + "The dimension must be at least 2 if tw is set.", + ) check(sw is None, ValueError, "If tw is set sw must be None") dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:]) - self.dim['tw'] = tw + self.dim["tw"] = tw elif sw is not None: - check(len(shape) >= 2, ValueError, "The dimension must be at least 2 if sw is set.") - self.dim['sw'] = sw - check(shape[0] == self.dim['sw'], ValueError, - f"The sw = {sw} is different from sw = {shape[0]} of the values.") + check( + len(shape) >= 2, + ValueError, + "The dimension must be at least 2 if sw is set.", + ) + self.dim["sw"] = sw + check( + shape[0] == self.dim["sw"], + ValueError, + f"The sw = {sw} is different from sw = {shape[0]} of the values.", + ) dimensions = shape[1] if len(shape[1:]) == 1 else list(shape[1:]) else: - dimensions = 1 if len(shape[0:]) == 0 else shape[0] if len(shape[0:]) == 1 else list(shape[0:]) + dimensions = ( + 1 + if len(shape[0:]) == 0 + else shape[0] + if len(shape[0:]) == 1 + else list(shape[0:]) + ) - self.dim['dim'] = dimensions + self.dim["dim"] = dimensions # deepcopy dimention information inside Parameters - self.json['Parameters'][self.name] = copy.deepcopy(self.dim) + self.json["Parameters"][self.name] = copy.deepcopy(self.dim) if type(values) in (int, float): - self.json['Parameters'][self.name]['init_values'] = [values] + self.json["Parameters"][self.name]["init_values"] = [values] else: - self.json['Parameters'][self.name]['init_values'] = values - self.json['Parameters'][self.name]['values'] = self.json['Parameters'][self.name]['init_values'] + self.json["Parameters"][self.name]["init_values"] = values + self.json["Parameters"][self.name]["values"] = self.json["Parameters"][ + self.name + ]["init_values"] if init is not None: - check('values' not in self.json['Parameters'][self.name], ValueError, f"The parameter {self.name} is already initialized.") - #check(inspect.isfunction(init), ValueError,f"The init parameter must be a function.") + check( + "values" not in self.json["Parameters"][self.name], + ValueError, + f"The parameter {self.name} is already initialized.", + ) + # check(inspect.isfunction(init), ValueError,f"The init parameter must be a function.") if inspect.isfunction(init): - code = textwrap.dedent(inspect.getsource(init)).replace('\"', '\'') - self.json['Parameters'][self.name]['init_fun'] = { 'code' : code, 'name' : init.__name__} + code = textwrap.dedent(inspect.getsource(init)).replace('"', "'") + self.json["Parameters"][self.name]["init_fun"] = { + "code": code, + "name": init.__name__, + } elif type(init) is str: - self.json['Parameters'][self.name]['init_fun'] = { 'name' : init } + self.json["Parameters"][self.name]["init_fun"] = {"name": init} if init_params is not None: - self.json['Parameters'][self.name]['init_fun']['params'] = init_params - -class SampleTime(): + self.json["Parameters"][self.name]["init_fun"]["params"] = init_params + + +class SampleTime: """ Represents a constant value that is equal to the sample time. @@ -194,10 +251,14 @@ class SampleTime(): ------- .. include:: /examples_basics/parameter_module_ex/sample_time.rst """ - name = 'SampleTime' + + name = "SampleTime" g = Constant(name, values=0) + def __new__(cls): - SampleTime.g.dim = {'dim': 1} - SampleTime.g.json['Constants'][SampleTime.name] = copy.deepcopy(SampleTime.g.dim) - SampleTime.g.json['Constants'][SampleTime.name]['values'] = SampleTime.name + SampleTime.g.dim = {"dim": 1} + SampleTime.g.json["Constants"][SampleTime.name] = copy.deepcopy( + SampleTime.g.dim + ) + SampleTime.g.json["Constants"][SampleTime.name]["values"] = SampleTime.name return SampleTime.g diff --git a/nnodely/layers/parametricfunction.py b/nnodely/layers/parametricfunction.py index 8cc6d714..83573aa8 100644 --- a/nnodely/layers/parametricfunction.py +++ b/nnodely/layers/parametricfunction.py @@ -13,10 +13,12 @@ from nnodely.support.jsonutils import merge from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) -paramfun_relation_name = 'ParamFun' +paramfun_relation_name = "ParamFun" + class ParamFun(NeuObj): """ @@ -56,13 +58,18 @@ class ParamFun(NeuObj): Examples -------- - + .. include:: /examples_basics/layer_module_ex/paramfun.rst """ + @enforce_types - def __init__(self, param_fun:Callable, - parameters_and_constants:list|dict|None = None, *, - map_over_batch:bool = False) -> Stream: + def __init__( + self, + param_fun: Callable, + parameters_and_constants: list | dict | None = None, + *, + map_over_batch: bool = False, + ) -> Stream: self.relation_name = paramfun_relation_name @@ -72,13 +79,13 @@ def __init__(self, param_fun:Callable, self.map_over_batch = map_over_batch self.output_dimension = {} - super().__init__('F'+paramfun_relation_name + str(NeuObj.count)) - code = textwrap.dedent(inspect.getsource(param_fun)).replace('\"', '\'') - self.json['Functions'][self.name] = { - 'code' : code, - 'name' : param_fun.__name__, + super().__init__("F" + paramfun_relation_name + str(NeuObj.count)) + code = textwrap.dedent(inspect.getsource(param_fun)).replace('"', "'") + self.json["Functions"][self.name] = { + "code": code, + "name": param_fun.__name__, } - self.json['Functions'][self.name]['params_and_consts'] = [] + self.json["Functions"][self.name]["params_and_consts"] = [] funinfo = inspect.getfullargspec(self.param_fun) @@ -86,7 +93,9 @@ def __init__(self, param_fun:Callable, if type(self.parameters_and_constants) is list: n_pc = len(self.parameters_and_constants) n_input = len(funinfo.args) - for pc, pc_name in zip(self.parameters_and_constants,funinfo.args[n_input-n_pc:]): + for pc, pc_name in zip( + self.parameters_and_constants, funinfo.args[n_input - n_pc :] + ): self.__create_parameter(pc, pc_name) # Create the parameters and constants from list @@ -99,13 +108,17 @@ def __init__(self, param_fun:Callable, self.__create_parameter(pc, key) elif first == True: p = Parameter(name=self.name + key, dimensions=1) - self.json['Functions'][self.name]['params_and_consts'].append(p.name) + self.json["Functions"][self.name]["params_and_consts"].append( + p.name + ) self.json = merge(self.json, p.json) self.json_stream = {} @enforce_types - def __call__(self, *obj:Union[Stream|Parameter|Constant|float|int]) -> Stream: + def __call__( + self, *obj: Union[Stream | Parameter | Constant | float | int] + ) -> Stream: stream_name = paramfun_relation_name + str(Stream.count) funinfo = inspect.getfullargspec(self.param_fun) @@ -121,78 +134,124 @@ def __call__(self, *obj:Union[Stream|Parameter|Constant|float|int]) -> Stream: else: obj_type = type(o) o = toStream(o) - check(type(o) is Stream, TypeError, - f"The type of {o} is {type(o)} and is not supported for ParamFun operation.") + check( + type(o) is Stream, + TypeError, + f"The type of {o} is {type(o)} and is not supported for ParamFun operation.", + ) input_types.append(obj_type) input_dimensions.append(o.dim) if n_call_input not in self.json_stream: if len(self.json_stream) > 0: - log.warning(f"The function {self.name} was called with a different number of inputs. If both functions enter in the model an error will be raised.") + log.warning( + f"The function {self.name} was called with a different number of inputs. If both functions enter in the model an error will be raised." + ) self.json_stream[n_call_input] = copy.deepcopy(self.json) - self.json_stream[n_call_input]['Functions'][self.name]['n_input'] = n_call_input + self.json_stream[n_call_input]["Functions"][self.name]["n_input"] = ( + n_call_input + ) # Create the missing parameters - n_created_parameters = len(self.json_stream[n_call_input]['Functions'][self.name]['params_and_consts']) + n_created_parameters = len( + self.json_stream[n_call_input]["Functions"][self.name][ + "params_and_consts" + ] + ) n_missing_parameters = n_parameters - n_created_parameters - check(n_missing_parameters >= 0, ValueError, f"The function is called with too many parameter and inputs.") - self.__create_missing_parameters(self.json_stream[n_call_input], n_call_input, n_missing_parameters) - - self.json_stream[n_call_input]['Functions'][self.name]['in_dim'] = copy.deepcopy(input_dimensions) - self.json_stream[n_call_input]['Functions'][self.name]['map_over_dim'] = self.__infer_map_over_batch(input_types, n_parameters) - output_dimension = self.__infer_output_dimensions(self.json_stream[n_call_input], input_types, input_dimensions) + check( + n_missing_parameters >= 0, + ValueError, + f"The function is called with too many parameter and inputs.", + ) + self.__create_missing_parameters( + self.json_stream[n_call_input], n_call_input, n_missing_parameters + ) + + self.json_stream[n_call_input]["Functions"][self.name]["in_dim"] = ( + copy.deepcopy(input_dimensions) + ) + self.json_stream[n_call_input]["Functions"][self.name]["map_over_dim"] = ( + self.__infer_map_over_batch(input_types, n_parameters) + ) + output_dimension = self.__infer_output_dimensions( + self.json_stream[n_call_input], input_types, input_dimensions + ) else: map_over_batch = self.__infer_map_over_batch(input_types, n_parameters) - check(map_over_batch == self.json_stream[n_call_input]['Functions'][self.name]['map_over_dim'], ValueError, f"The function {self.name} was called with different type of input using map_over_batch=True.") - output_dimension = self.__infer_output_dimensions(self.json_stream[n_call_input], input_types, input_dimensions) + check( + map_over_batch + == self.json_stream[n_call_input]["Functions"][self.name][ + "map_over_dim" + ], + ValueError, + f"The function {self.name} was called with different type of input using map_over_batch=True.", + ) + output_dimension = self.__infer_output_dimensions( + self.json_stream[n_call_input], input_types, input_dimensions + ) # Save the all the input dimension used for call the parametric function - in_dim = self.json_stream[n_call_input]['Functions'][self.name]['in_dim'] + in_dim = self.json_stream[n_call_input]["Functions"][self.name]["in_dim"] if type(in_dim[0]) is dict: if in_dim != input_dimensions: in_dim = [in_dim, input_dimensions] - log.warning(f"The function {self.name} was called with inputs with different dimensions.") + log.warning( + f"The function {self.name} was called with inputs with different dimensions." + ) elif input_dimensions not in in_dim: in_dim.append(input_dimensions) - log.warning(f"The function {self.name} was called with inputs with different dimensions.") - self.json_stream[n_call_input]['Functions'][self.name]['in_dim'] = in_dim + log.warning( + f"The function {self.name} was called with inputs with different dimensions." + ) + self.json_stream[n_call_input]["Functions"][self.name]["in_dim"] = in_dim stream_json = copy.deepcopy(self.json_stream[n_call_input]) input_names = [] for ind, o in enumerate(obj): o = toStream(o) - check(type(o) is Stream, TypeError, - f"The type of {o} is {type(o)} and is not supported for ParamFun operation.") + check( + type(o) is Stream, + TypeError, + f"The type of {o} is {type(o)} and is not supported for ParamFun operation.", + ) stream_json = merge(stream_json, o.json) input_names.append(o.name) - stream_json['Relations'][stream_name] = [paramfun_relation_name, input_names, self.name] + stream_json["Relations"][stream_name] = [ + paramfun_relation_name, + input_names, + self.name, + ] return Stream(stream_name, stream_json, output_dimension) def __create_parameter(self, pc, pc_name): if type(pc) is Parameter: - self.json['Functions'][self.name]['params_and_consts'].append(pc.name) + self.json["Functions"][self.name]["params_and_consts"].append(pc.name) self.json = merge(self.json, pc.json) elif type(pc) is str: # TODO to remove! there is no reason to give a name to the parameter. The name of the parameter is the name of the function parameter p = Parameter(name=pc, dimensions=1) - self.json['Functions'][self.name]['params_and_consts'].append(p.name) + self.json["Functions"][self.name]["params_and_consts"].append(p.name) self.json = merge(self.json, p.json) elif type(pc) is tuple: p = Parameter(name=self.name + pc_name, dimensions=list(pc)) - self.json['Functions'][self.name]['params_and_consts'].append(p.name) + self.json["Functions"][self.name]["params_and_consts"].append(p.name) self.json = merge(self.json, p.json) elif type(pc) is Constant: - self.json['Functions'][self.name]['params_and_consts'].append(pc.name) + self.json["Functions"][self.name]["params_and_consts"].append(pc.name) self.json = merge(self.json, pc.json) elif type(pc) in (float, int, list): c = Constant(name=self.name + pc_name, values=pc) - self.json['Functions'][self.name]['params_and_consts'].append(c.name) + self.json["Functions"][self.name]["params_and_consts"].append(c.name) self.json = merge(self.json, c.json) else: - check(type(pc) in (Parameter, str, tuple, Constant, float, int, list), TypeError, - f'The element inside the \"parameters_and_constants\" list or dict must be a Parameter, str, tuple to build a Parameter or Constant, int, float or list to build a Constant but was {type(pc)}.') + check( + type(pc) in (Parameter, str, tuple, Constant, float, int, list), + TypeError, + f'The element inside the "parameters_and_constants" list or dict must be a Parameter, str, tuple to build a Parameter or Constant, int, float or list to build a Constant but was {type(pc)}.', + ) def __infer_map_over_batch(self, input_types, n_constants_and_params): input_map_dim = () @@ -211,21 +270,24 @@ def __infer_map_over_batch(self, input_types, n_constants_and_params): else: return False - def __create_missing_parameters(self, stream_json, n_call_input, n_missing_parameters): + def __create_missing_parameters( + self, stream_json, n_call_input, n_missing_parameters + ): funinfo = inspect.getfullargspec(self.param_fun) for i in range(n_missing_parameters): - p_name = self.name + funinfo.args[n_call_input+i] - stream_json['Functions'][self.name]['params_and_consts'].insert(i, p_name) - stream_json['Parameters'][p_name] = {'dim': 1} + p_name = self.name + funinfo.args[n_call_input + i] + stream_json["Functions"][self.name]["params_and_consts"].insert(i, p_name) + stream_json["Parameters"][p_name] = {"dim": 1} def __infer_output_dimensions(self, stream_json, input_types, input_dimensions): import torch + batch_dim = 5 all_inputs_dim = copy.deepcopy(input_dimensions) all_inputs_type = copy.deepcopy(input_types) - params_and_consts = stream_json['Constants'] | stream_json['Parameters'] - for name in stream_json['Functions'][self.name]['params_and_consts']: + params_and_consts = stream_json["Constants"] | stream_json["Parameters"] + for name in stream_json["Functions"][self.name]["params_and_consts"]: all_inputs_dim.append(params_and_consts[name]) all_inputs_type.append(Constant) @@ -233,7 +295,11 @@ def __infer_output_dimensions(self, stream_json, input_types, input_dimensions): is_int = False while is_int == False: n_samples_sec *= 10 - vect_input_time = [math.isclose(d['tw']*n_samples_sec,round(d['tw']*n_samples_sec)) for d in all_inputs_dim if 'tw' in d] + vect_input_time = [ + math.isclose(d["tw"] * n_samples_sec, round(d["tw"] * n_samples_sec)) + for d in all_inputs_dim + if "tw" in d + ] if len(vect_input_time) == 0: is_int = True else: @@ -244,67 +310,96 @@ def __infer_output_dimensions(self, stream_json, input_types, input_dimensions): inputs_win_type = [] inputs_win = [] - for t, dim in zip(all_inputs_type,all_inputs_dim): - window = 'tw' if 'tw' in dim else ('sw' if 'sw' in dim else None) - if window == 'tw': + for t, dim in zip(all_inputs_type, all_inputs_dim): + window = "tw" if "tw" in dim else ("sw" if "sw" in dim else None) + if window == "tw": dim_win = round(dim[window] * n_samples_sec) - elif window == 'sw': + elif window == "sw": dim_win = dim[window] else: dim_win = None if t in (Parameter, Constant) else 1 if t in (Parameter, Constant): - if type(dim['dim']) is list: + if type(dim["dim"]) is list: if dim_win is not None: - inputs.append(torch.rand(size=(dim_win,) + tuple(dim['dim']))) + inputs.append(torch.rand(size=(dim_win,) + tuple(dim["dim"]))) else: - inputs.append(torch.rand(size=tuple(dim['dim']))) + inputs.append(torch.rand(size=tuple(dim["dim"]))) else: if dim_win is not None: - inputs.append(torch.rand(size=(dim_win, dim['dim']))) + inputs.append(torch.rand(size=(dim_win, dim["dim"]))) else: - inputs.append(torch.rand(size=(dim['dim'],))) + inputs.append(torch.rand(size=(dim["dim"],))) else: - inputs.append(torch.rand(size=(batch_dim, dim_win, dim['dim']))) + inputs.append(torch.rand(size=(batch_dim, dim_win, dim["dim"]))) inputs_win_type.append(window) inputs_win.append(dim_win) if self.map_over_batch: - function_to_call = torch.func.vmap(self.param_fun,in_dims=tuple(stream_json['Functions'][self.name]['map_over_dim'])) + function_to_call = torch.func.vmap( + self.param_fun, + in_dims=tuple(stream_json["Functions"][self.name]["map_over_dim"]), + ) else: function_to_call = self.param_fun out = function_to_call(*inputs) out_shape = out.shape - check(out_shape[0] == batch_dim, ValueError, "The batch output dimension it is not correct.") + check( + out_shape[0] == batch_dim, + ValueError, + "The batch output dimension it is not correct.", + ) out_dim = list(out_shape[2:]) - check(len(out_dim) == 1, ValueError, "The output dimension of the function is bigger than a vector.") - out_win_type = 'sw' + check( + len(out_dim) == 1, + ValueError, + "The output dimension of the function is bigger than a vector.", + ) + out_win_type = "sw" out_win = out_shape[1] for idx, win in enumerate(inputs_win): - if out_shape[1] == win and all_inputs_type[idx] not in (Parameter, Constant): + if out_shape[1] == win and all_inputs_type[idx] not in ( + Parameter, + Constant, + ): out_win_type = inputs_win_type[idx] out_win = all_inputs_dim[idx][out_win_type] - return { 'dim': out_dim[0], out_win_type : out_win } + return {"dim": out_dim[0], out_win_type: out_win} -def return_standard_inputs(json, model_def, xlim = None, num_points = 1000): - check(json['n_input'] == 1 or json['n_input'] == 2, ValueError, "The function must have only one or two inputs.") + +def return_standard_inputs(json, model_def, xlim=None, num_points=1000): + check( + json["n_input"] == 1 or json["n_input"] == 2, + ValueError, + "The function must have only one or two inputs.", + ) fun_inputs = tuple() - for i in range(json['n_input']): - dim = json['in_dim'][i] - check(dim['dim'] == 1, ValueError, "The input dimension must be 1.") - if 'tw' in dim: - check(dim['tw'] == model_def['Info']['SampleTime'], ValueError, f"The input window must be 1 but was {dim['tw']}.") - elif 'sw' in dim: - check(dim['sw'] == 1, ValueError, "The input window must be 1.") + for i in range(json["n_input"]): + dim = json["in_dim"][i] + check(dim["dim"] == 1, ValueError, "The input dimension must be 1.") + if "tw" in dim: + check( + dim["tw"] == model_def["Info"]["SampleTime"], + ValueError, + f"The input window must be 1 but was {dim['tw']}.", + ) + elif "sw" in dim: + check(dim["sw"] == 1, ValueError, "The input window must be 1.") if xlim is not None: - if json['n_input'] == 2: - check(np.array(xlim).shape == (json['n_input'], 2), ValueError, - "The xlim must have the same shape as the number of inputs.") + if json["n_input"] == 2: + check( + np.array(xlim).shape == (json["n_input"], 2), + ValueError, + "The xlim must have the same shape as the number of inputs.", + ) x_value = np.linspace(xlim[i][0], xlim[i][1], num=num_points) else: - check(np.array(xlim).shape == (2,), ValueError, - "The xlim must have the same shape as the number of inputs.") + check( + np.array(xlim).shape == (2,), + ValueError, + "The xlim must have the same shape as the number of inputs.", + ) x_value = np.linspace(xlim[0], xlim[1], num=num_points) else: x_value = np.linspace(0, 1, num=num_points) @@ -313,34 +408,45 @@ def return_standard_inputs(json, model_def, xlim = None, num_points = 1000): else: x1_value = torch.from_numpy(x_value) - if json['n_input'] == 2: - x0_value, x1_value = torch.meshgrid(x0_value,x1_value,indexing="xy") + if json["n_input"] == 2: + x0_value, x1_value = torch.meshgrid(x0_value, x1_value, indexing="xy") x0_value = x0_value.flatten().unsqueeze(1).unsqueeze(1) x1_value = x1_value.flatten().unsqueeze(1).unsqueeze(1) - fun_inputs += (x0_value,x1_value,) + fun_inputs += ( + x0_value, + x1_value, + ) else: x0_value = x0_value.unsqueeze(1).unsqueeze(1) fun_inputs += (x0_value,) - for key in json['params_and_consts']: - val = model_def['Parameters'][key] if key in model_def['Parameters'] else model_def['Constants'][key] - fun_inputs += tuple([torch.from_numpy(np.array(val['values']))]) # The vector is transform in a tuple + for key in json["params_and_consts"]: + val = ( + model_def["Parameters"][key] + if key in model_def["Parameters"] + else model_def["Constants"][key] + ) + fun_inputs += tuple( + [torch.from_numpy(np.array(val["values"]))] + ) # The vector is transform in a tuple return fun_inputs + def return_function(json, fun_inputs): - exec(json['code'], globals()) - function_to_call = globals()[json['name']] + exec(json["code"], globals()) + function_to_call = globals()[json["name"]] output = function_to_call(*fun_inputs) check(output.shape[1] == 1, ValueError, "The output dimension must be 1.") check(output.shape[2] == 1, ValueError, "The output window must be 1.") funinfo = inspect.getfullargspec(function_to_call) return output, funinfo.args + class Parametric_Layer(nn.Module): def __init__(self, func, params_and_consts, map_over_batch): super().__init__() - self.name = func['name'] + self.name = func["name"] self.params_and_consts = params_and_consts if type(map_over_batch) is list: self.map_over_batch = True @@ -349,7 +455,7 @@ def __init__(self, func, params_and_consts, map_over_batch): self.map_over_batch = False ## Add the function to the globals try: - code = 'import torch\n@torch.fx.wrap\n' + func['code'] + code = "import torch\n@torch.fx.wrap\n" + func["code"] exec(code, globals()) except Exception as e: print(f"An error occurred: {e}") @@ -360,11 +466,19 @@ def forward(self, *inputs): function_to_call = globals()[self.name] # Call the function using the retrieved function object if self.map_over_batch: - function_to_call = torch.func.vmap(function_to_call,in_dims=self.input_map_dim) + function_to_call = torch.func.vmap( + function_to_call, in_dims=self.input_map_dim + ) result = function_to_call(*args) return result + def createParamFun(self, *func_params): - return Parametric_Layer(func=func_params[0], params_and_consts=func_params[1], map_over_batch=func_params[2]) + return Parametric_Layer( + func=func_params[0], + params_and_consts=func_params[1], + map_over_batch=func_params[2], + ) + setattr(Model, paramfun_relation_name, createParamFun) diff --git a/nnodely/layers/part.py b/nnodely/layers/part.py index b86bd4cd..b9ee48c2 100644 --- a/nnodely/layers/part.py +++ b/nnodely/layers/part.py @@ -8,17 +8,16 @@ from nnodely.support.utils import check, enforce_types from nnodely.support.jsonutils import merge -part_relation_name = 'Part' -select_relation_name = 'Select' -concatenate_relation_name = 'Concatenate' +part_relation_name = "Part" +select_relation_name = "Select" +concatenate_relation_name = "Concatenate" -timepart_relation_name = 'TimePart' -timeselect_relation_name = 'TimeSelect' -timeconcatenate_relation_name = 'TimeConcatenate' - -samplepart_relation_name = 'SamplePart' -sampleselect_relation_name = 'SampleSelect' +timepart_relation_name = "TimePart" +timeselect_relation_name = "TimeSelect" +timeconcatenate_relation_name = "TimeConcatenate" +samplepart_relation_name = "SamplePart" +sampleselect_relation_name = "SampleSelect" class Part(Stream, ToStream): @@ -50,7 +49,7 @@ class Part(Stream, ToStream): Examples -------- - + .. include:: /examples_basics/layer_module_ex/part_module/part.rst Raises @@ -58,17 +57,26 @@ class Part(Stream, ToStream): IndexError If the indices i and j are out of range. """ + @enforce_types - def __init__(self, obj:Stream, i:int, j:int): + def __init__(self, obj: Stream, i: int, j: int): # check(type(obj) is Stream, TypeError, # f"The type of {obj} is {type(obj)} and is not supported for Part operation.") - check(i >= 0 and j > 0 and i < obj.dim['dim'] and j <= obj.dim['dim'], - IndexError, - f"i={i} or j={j} are not in the range [0,{obj.dim['dim']}]") + check( + i >= 0 and j > 0 and i < obj.dim["dim"] and j <= obj.dim["dim"], + IndexError, + f"i={i} or j={j} are not in the range [0,{obj.dim['dim']}]", + ) dim = copy.deepcopy(obj.dim) - dim['dim'] = j - i - super().__init__(part_relation_name + str(Stream.count),obj.json,dim) - self.json['Relations'][self.name] = [part_relation_name,[obj.name],obj.dim['dim'],[i,j]] + dim["dim"] = j - i + super().__init__(part_relation_name + str(Stream.count), obj.json, dim) + self.json["Relations"][self.name] = [ + part_relation_name, + [obj.name], + obj.dim["dim"], + [i, j], + ] + class Select(Stream, ToStream): """ @@ -97,67 +105,101 @@ class Select(Stream, ToStream): Examples -------- - + .. include:: /examples_basics/layer_module_ex/part_module/select.rst - + Raises ------ IndexError If the index i is out of range. """ + @enforce_types - def __init__(self, obj:Stream, i:int): + def __init__(self, obj: Stream, i: int): # check(type(obj) is Stream, TypeError, # f"The type of {obj} is {type(obj)} and is not supported for Select operation.") - check(i >= 0 and i < obj.dim['dim'], - IndexError, - f"i={i} are not in the range [0,{obj.dim['dim']}]") + check( + i >= 0 and i < obj.dim["dim"], + IndexError, + f"i={i} are not in the range [0,{obj.dim['dim']}]", + ) dim = copy.deepcopy(obj.dim) - dim['dim'] = 1 - super().__init__(select_relation_name + str(Stream.count),obj.json,dim) - self.json['Relations'][self.name] = [select_relation_name,[obj.name],obj.dim['dim'],i] + dim["dim"] = 1 + super().__init__(select_relation_name + str(Stream.count), obj.json, dim) + self.json["Relations"][self.name] = [ + select_relation_name, + [obj.name], + obj.dim["dim"], + i, + ] + class Concatenate(Stream, ToStream): """ - Implement the concatenate function between two tensors. + Implement the concatenate function between two tensors. - See also: - Official PyTorch Cat documentation: - `torch.cat `_ + See also: + Official PyTorch Cat documentation: + `torch.cat `_ - :param input1: the first relation to concatenate - :type obj: Tensor - :param input2: the second relation to concatenate - :type obj: Tensor + :param input1: the first relation to concatenate + :type obj: Tensor + :param input2: the second relation to concatenate + :type obj: Tensor - Examples - -------- - .. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb - :alt: Open in Colab + Examples + -------- + .. image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb + :alt: Open in Colab - Example: - >>> cat = Concatenate(relation1, relation2) + Example: + >>> cat = Concatenate(relation1, relation2) """ + @enforce_types - def __init__(self, obj1:Stream, obj2:Stream) -> Stream: - obj1,obj2 = toStream(obj1),toStream(obj2) - check(type(obj1) is Stream,TypeError, - f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.") - check(type(obj2) is Stream,TypeError, - f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.") - #check(obj1.dim == obj2.dim or obj1.dim == {'dim':1} or obj2.dim == {'dim':1}, ValueError, + def __init__(self, obj1: Stream, obj2: Stream) -> Stream: + obj1, obj2 = toStream(obj1), toStream(obj2) + check( + type(obj1) is Stream, + TypeError, + f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.", + ) + check( + type(obj2) is Stream, + TypeError, + f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.", + ) + # check(obj1.dim == obj2.dim or obj1.dim == {'dim':1} or obj2.dim == {'dim':1}, ValueError, # f"For addition operators (+) the dimension of {obj1.name} = {obj1.dim} must be the same of {obj2.name} = {obj2.dim}.") dim = copy.deepcopy(obj1.dim) - dim['dim'] = obj1.dim['dim']+obj2.dim['dim'] - if 'tw' in obj1.dim.keys() and 'tw' in obj2.dim.keys(): - check(obj1.dim['tw'] == obj2.dim['tw'], ValueError, 'The time window of the two inputs must be the same') - elif 'sw' in obj1.dim.keys() and 'sw' in obj2.dim.keys(): - check(obj1.dim['sw'] == obj2.dim['sw'], ValueError, 'The sample window of the two inputs must be the same') + dim["dim"] = obj1.dim["dim"] + obj2.dim["dim"] + if "tw" in obj1.dim.keys() and "tw" in obj2.dim.keys(): + check( + obj1.dim["tw"] == obj2.dim["tw"], + ValueError, + "The time window of the two inputs must be the same", + ) + elif "sw" in obj1.dim.keys() and "sw" in obj2.dim.keys(): + check( + obj1.dim["sw"] == obj2.dim["sw"], + ValueError, + "The sample window of the two inputs must be the same", + ) else: - raise(ValueError('The two inputs have different time or sample dimensions')) - super().__init__(concatenate_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim) - self.json['Relations'][self.name] = [concatenate_relation_name,[obj1.name,obj2.name]] + raise ( + ValueError("The two inputs have different time or sample dimensions") + ) + super().__init__( + concatenate_relation_name + str(Stream.count), + merge(obj1.json, obj2.json), + dim, + ) + self.json["Relations"][self.name] = [ + concatenate_relation_name, + [obj1.name, obj2.name], + ] + class SamplePart(Stream, ToStream): """ @@ -190,7 +232,7 @@ class SamplePart(Stream, ToStream): Examples -------- - + .. include:: /examples_basics/layer_module_ex/part_module/sample_part.rst Raises @@ -202,34 +244,48 @@ class SamplePart(Stream, ToStream): IndexError If the offset is not within the sample window. """ + @enforce_types - def __init__(self, obj:Stream, i:int, j:int, offset:int|None = None): + def __init__(self, obj: Stream, i: int, j: int, offset: int | None = None): # check(type(obj) is Stream, TypeError, # f"The type of {obj} is {type(obj)} and is not supported for SamplePart operation.") - check('sw' in obj.dim, KeyError, 'Input must have a sample window') - check(i < j, ValueError, 'i must be smaller than j') - all_inputs = obj.json['Inputs'] + check("sw" in obj.dim, KeyError, "Input must have a sample window") + check(i < j, ValueError, "i must be smaller than j") + all_inputs = obj.json["Inputs"] if obj.name in all_inputs: - backward_idx = all_inputs[obj.name]['sw'][0] - forward_idx = all_inputs[obj.name]['sw'][1] + backward_idx = all_inputs[obj.name]["sw"][0] + forward_idx = all_inputs[obj.name]["sw"][1] else: backward_idx = 0 - forward_idx = obj.dim['sw'] - check(i >= backward_idx and i < forward_idx, ValueError, 'i must be in the sample window of the input') - check(j > backward_idx and j <= forward_idx, ValueError, 'j must be in the sample window of the input') + forward_idx = obj.dim["sw"] + check( + i >= backward_idx and i < forward_idx, + ValueError, + "i must be in the sample window of the input", + ) + check( + j > backward_idx and j <= forward_idx, + ValueError, + "j must be in the sample window of the input", + ) dim = copy.deepcopy(obj.dim) - dim['sw'] = j - i + dim["sw"] = j - i name = samplepart_relation_name + str(Stream.count) - super().__init__(name,obj.json,dim) + super().__init__(name, obj.json, dim) if obj.name in all_inputs: - rel = [samplepart_relation_name,[obj.name],-1,[i,j]] + rel = [samplepart_relation_name, [obj.name], -1, [i, j]] else: - rel = [samplepart_relation_name,[obj.name],obj.dim['sw'],[i,j]] - #rel = [samplepart_relation_name,[obj.name],[i,j]] + rel = [samplepart_relation_name, [obj.name], obj.dim["sw"], [i, j]] + # rel = [samplepart_relation_name,[obj.name],[i,j]] if offset is not None: - check(i <= offset < j, IndexError,"The offset must be inside the sample window") + check( + i <= offset < j, + IndexError, + "The offset must be inside the sample window", + ) rel.append(offset) - self.json['Relations'][self.name] = rel + self.json["Relations"][self.name] = rel + class SampleSelect(Stream, ToStream): """ @@ -258,9 +314,9 @@ class SampleSelect(Stream, ToStream): Examples -------- - + .. include:: /examples_basics/layer_module_ex/part_module/sample_select.rst - + Raises ------ IndexError @@ -270,18 +326,29 @@ class SampleSelect(Stream, ToStream): IndexError If the offset is not within the sample window. """ + @enforce_types - def __init__(self, obj:Stream, i:int): + def __init__(self, obj: Stream, i: int): # check(type(obj) is Stream, TypeError, # f"The type of {obj} is {type(obj)} and is not supported for SampleSelect operation.") - check('sw' in obj.dim, KeyError, 'Input must have a sample window') + check("sw" in obj.dim, KeyError, "Input must have a sample window") backward_idx = 0 - forward_idx = obj.dim['sw'] - check(i >= backward_idx and i < forward_idx, ValueError, 'i must be in the sample window of the input') + forward_idx = obj.dim["sw"] + check( + i >= backward_idx and i < forward_idx, + ValueError, + "i must be in the sample window of the input", + ) dim = copy.deepcopy(obj.dim) - dim['sw'] = 1 - super().__init__(sampleselect_relation_name + str(Stream.count),obj.json,dim) - self.json['Relations'][self.name] = [sampleselect_relation_name,[obj.name],obj.dim['sw'],i] + dim["sw"] = 1 + super().__init__(sampleselect_relation_name + str(Stream.count), obj.json, dim) + self.json["Relations"][self.name] = [ + sampleselect_relation_name, + [obj.name], + obj.dim["sw"], + i, + ] + class TimePart(Stream, ToStream): """ @@ -309,7 +376,7 @@ class TimePart(Stream, ToStream): Examples -------- - + .. include:: /examples_basics/layer_module_ex/part_module/time_part.rst Raises @@ -321,79 +388,113 @@ class TimePart(Stream, ToStream): IndexError If the offset is not within the time window. """ + @enforce_types - def __init__(self, obj:Stream, i:int|float, j:int|float, offset:int|float|None = None): - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for TimePart operation.") - check('tw' in obj.dim, KeyError, 'Input must have a time window') - check(i < j, ValueError, 'i must be smaller than j') - all_inputs = obj.json['Inputs'] + def __init__( + self, + obj: Stream, + i: int | float, + j: int | float, + offset: int | float | None = None, + ): + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for TimePart operation.", + ) + check("tw" in obj.dim, KeyError, "Input must have a time window") + check(i < j, ValueError, "i must be smaller than j") + all_inputs = obj.json["Inputs"] if obj.name in all_inputs: - backward_idx = all_inputs[obj.name]['tw'][0] - forward_idx = all_inputs[obj.name]['tw'][1] + backward_idx = all_inputs[obj.name]["tw"][0] + forward_idx = all_inputs[obj.name]["tw"][1] else: backward_idx = 0 - forward_idx = obj.dim['tw'] - check(i >= backward_idx and i < forward_idx, ValueError, 'i must be in the time window of the input') - check(j > backward_idx and j <= forward_idx, ValueError, 'j must be in the time window of the input') + forward_idx = obj.dim["tw"] + check( + i >= backward_idx and i < forward_idx, + ValueError, + "i must be in the time window of the input", + ) + check( + j > backward_idx and j <= forward_idx, + ValueError, + "j must be in the time window of the input", + ) dim = copy.deepcopy(obj.dim) - dim['tw'] = j - i - super().__init__(timepart_relation_name + str(Stream.count),obj.json,dim) + dim["tw"] = j - i + super().__init__(timepart_relation_name + str(Stream.count), obj.json, dim) if obj.name in all_inputs: - rel = [timepart_relation_name,[obj.name],-1,[i,j]] + rel = [timepart_relation_name, [obj.name], -1, [i, j]] else: - rel = [timepart_relation_name,[obj.name],obj.dim['tw'],[i,j]] - #rel = [timepart_relation_name,[obj.name],[i,j]] + rel = [timepart_relation_name, [obj.name], obj.dim["tw"], [i, j]] + # rel = [timepart_relation_name,[obj.name],[i,j]] if offset is not None: - check(i <= offset < j, IndexError,"The offset must be inside the time window") + check( + i <= offset < j, IndexError, "The offset must be inside the time window" + ) rel.append(offset) - self.json['Relations'][self.name] = rel + self.json["Relations"][self.name] = rel + class TimeConcatenate(Stream, ToStream): """ - Implement the concatenate function between two tensors along the time dimension (second dimension). + Implement the concatenate function between two tensors along the time dimension (second dimension). - See also: - Official PyTorch Cat documentation: - `torch.cat `_ + See also: + Official PyTorch Cat documentation: + `torch.cat `_ - :param input1: the first relation to concatenate - :type obj: Tensor - :param input2: the second relation to concatenate - :type obj: Tensor + :param input1: the first relation to concatenate + :type obj: Tensor + :param input2: the second relation to concatenate + :type obj: Tensor - Examples - -------- - .. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb - :alt: Open in Colab + Examples + -------- + .. image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/tonegas/nnodely/blob/main/examples/partitioning.ipynb + :alt: Open in Colab - Example: - >>> cat = TimeConcatenate(relation1, relation2) + Example: + >>> cat = TimeConcatenate(relation1, relation2) """ + @enforce_types - def __init__(self, obj1:Stream, obj2:Stream) -> Stream: - obj1,obj2 = toStream(obj1),toStream(obj2) - check(type(obj1) is Stream,TypeError, - f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.") - check(type(obj2) is Stream,TypeError, - f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.") - - #check('tw' in obj1.dim, KeyError, 'Input1 must have a time window') - #check('tw' in obj2.dim, KeyError, 'Input2 must have a time window') + def __init__(self, obj1: Stream, obj2: Stream) -> Stream: + obj1, obj2 = toStream(obj1), toStream(obj2) + check( + type(obj1) is Stream, + TypeError, + f"The type of {obj1} is {type(obj1)} and is not supported for the Concatenate operation.", + ) + check( + type(obj2) is Stream, + TypeError, + f"The type of {obj2} is {type(obj2)} and is not supported for the Concatenate operation.", + ) + + # check('tw' in obj1.dim, KeyError, 'Input1 must have a time window') + # check('tw' in obj2.dim, KeyError, 'Input2 must have a time window') dim = copy.deepcopy(obj1.dim) - if 'tw' in obj1.dim and 'tw' in obj2.dim: - dim['tw'] = obj1.dim['tw'] + obj2.dim['tw'] - elif 'sw' in obj1.dim and 'sw' in obj2.dim: - dim['sw'] = obj1.dim['sw'] + obj2.dim['sw'] - super().__init__(timeconcatenate_relation_name + str(Stream.count),merge(obj1.json,obj2.json),dim) - self.json['Relations'][self.name] = [timeconcatenate_relation_name,[obj1.name,obj2.name]] - + if "tw" in obj1.dim and "tw" in obj2.dim: + dim["tw"] = obj1.dim["tw"] + obj2.dim["tw"] + elif "sw" in obj1.dim and "sw" in obj2.dim: + dim["sw"] = obj1.dim["sw"] + obj2.dim["sw"] + super().__init__( + timeconcatenate_relation_name + str(Stream.count), + merge(obj1.json, obj2.json), + dim, + ) + self.json["Relations"][self.name] = [ + timeconcatenate_relation_name, + [obj1.name, obj2.name], + ] class Part_Layer(nn.Module): #: :noindex: - def __init__(self, dim:int, i:int, j:int): + def __init__(self, dim: int, i: int, j: int): super(Part_Layer, self).__init__() self.i, self.j = i, j @@ -404,12 +505,14 @@ def __init__(self, dim:int, i:int, j:int): def forward(self, x): ## assert x.ndim >= 3, 'The Part Relation Works only for 3D inputs' - return torch.einsum('bij,kj->bik', x, self.W) + return torch.einsum("bij,kj->bik", x, self.W) + ## Select elements on the third dimension in the range [i,j] def createPart(self, *inputs): return Part_Layer(dim=inputs[0], i=inputs[1][0], j=inputs[1][1]) + class Select_Layer(nn.Module): #: :noindex: def __init__(self, dim, idx): @@ -419,12 +522,14 @@ def __init__(self, dim, idx): def forward(self, x): ## assert x.ndim >= 3, 'The Part Relation Works only for 3D inputs' - return torch.einsum('ijk,k->ij', x, self.W).unsqueeze(2) + return torch.einsum("ijk,k->ij", x, self.W).unsqueeze(2) + ## Select an element i on the third dimension def createSelect(self, *inputs): return Select_Layer(dim=inputs[0], idx=inputs[1]) + class SamplePart_Layer(nn.Module): #: :noindex: def __init__(self, dim, part, offset): @@ -440,9 +545,10 @@ def __init__(self, dim, part, offset): def forward(self, x): if self.offset is not None: x = x - x[:, self.offset].unsqueeze(1) - result = torch.einsum('bij,ki->bkj', x, self.W) + result = torch.einsum("bij,ki->bkj", x, self.W) return result + class Concatenate_Layer(nn.Module): #: :noindex: def __init__(self): @@ -451,16 +557,19 @@ def __init__(self): def forward(self, *inputs): return torch.cat((inputs[0], inputs[1]), dim=2) + def createConcatenate(name, *inputs): #: :noindex: return Concatenate_Layer() + def createSamplePart(self, *inputs): if len(inputs) > 2: ## offset return SamplePart_Layer(dim=inputs[0], part=inputs[1], offset=inputs[2]) else: return SamplePart_Layer(dim=inputs[0], part=inputs[1], offset=None) + class SampleSelect_Layer(nn.Module): #: :noindex: def __init__(self, dim, idx): @@ -469,11 +578,13 @@ def __init__(self, dim, idx): self.W[idx] = 1 def forward(self, x): - return torch.einsum('ijk,j->ik', x, self.W).unsqueeze(1) + return torch.einsum("ijk,j->ik", x, self.W).unsqueeze(1) + def createSampleSelect(self, *inputs): return SampleSelect_Layer(dim=inputs[0], idx=inputs[1]) + class TimePart_Layer(nn.Module): #: :noindex: def __init__(self, dim, part, offset): @@ -489,15 +600,17 @@ def __init__(self, dim, part, offset): def forward(self, x): if self.offset is not None: x = x - x[:, self.offset].unsqueeze(1) - result = torch.einsum('bij,ki->bkj', x, self.W) + result = torch.einsum("bij,ki->bkj", x, self.W) return result + def createTimePart(self, *inputs): - if len(inputs) > 2: ## offset + if len(inputs) > 2: ## offset return TimePart_Layer(dim=inputs[0], part=inputs[1], offset=inputs[2]) else: return TimePart_Layer(dim=inputs[0], part=inputs[1], offset=None) + class TimeConcatenate_Layer(nn.Module): #: :noindex: def __init__(self): @@ -506,10 +619,12 @@ def __init__(self): def forward(self, *inputs): return torch.cat((inputs[0], inputs[1]), dim=1) + def createTimeConcatenate(name, *inputs): #: :noindex: return TimeConcatenate_Layer() + setattr(Model, part_relation_name, createPart) setattr(Model, select_relation_name, createSelect) setattr(Model, concatenate_relation_name, createConcatenate) diff --git a/nnodely/layers/rungekutta.py b/nnodely/layers/rungekutta.py index 35255cfc..ac89247b 100644 --- a/nnodely/layers/rungekutta.py +++ b/nnodely/layers/rungekutta.py @@ -11,9 +11,9 @@ import textwrap, inspect from collections.abc import Callable -fe_relation_name = 'ForwardEuler' -rk2_relation_name = 'RK2' -rk4_relation_name = 'RK4' +fe_relation_name = "ForwardEuler" +rk2_relation_name = "RK2" +rk4_relation_name = "RK4" # class ForwardEuler(NeuObj): # """ @@ -30,6 +30,7 @@ # 'name' : f.__name__, # } + # @enforce_types # def __call__(self, obj:Stream) -> Stream: # stream_name = fe_relation_name + str(Stream.count) @@ -42,55 +43,62 @@ class ForwardEuler(NeuObj): """ This operation perform Forward Euler Integration on a Stream """ + @enforce_types - def __init__(self, f:Callable|ParamFun) -> Stream: - super().__init__('F' + fe_relation_name + str(NeuObj.count)) + def __init__(self, f: Callable | ParamFun) -> Stream: + super().__init__("F" + fe_relation_name + str(NeuObj.count)) self.f = f if isinstance(f, ParamFun) else ParamFun(f) self.dt = SampleTime() + @enforce_types - def __call__(self, obj:Stream) -> Stream: + def __call__(self, obj: Stream) -> Stream: return obj + self.dt * self.f(obj) + class RK2(NeuObj): """ This operation perform RK2 Integration on a Stream """ + @enforce_types - def __init__(self, f:Callable|ParamFun) -> Stream: + def __init__(self, f: Callable | ParamFun) -> Stream: super().__init__(rk2_relation_name + str(NeuObj.count)) self.f = f if isinstance(f, ParamFun) else ParamFun(f) - #self.fe = ForwardEuler(self.f) + # self.fe = ForwardEuler(self.f) self.dt = SampleTime() @enforce_types - def __call__(self, obj:Stream) -> Stream: + def __call__(self, obj: Stream) -> Stream: f1 = self.f(obj) - f2 = self.f(obj + (self.dt/2) * f1) + f2 = self.f(obj + (self.dt / 2) * f1) return obj + self.dt * f2 + class RK4(NeuObj): """ This operation perform RK4 Integration on a Stream """ + @enforce_types - def __init__(self, f:Callable|ParamFun) -> Stream: + def __init__(self, f: Callable | ParamFun) -> Stream: super().__init__(rk4_relation_name + str(NeuObj.count)) self.f = f if isinstance(f, ParamFun) else ParamFun(f) self.dt = SampleTime() @enforce_types - def __call__(self, obj:Stream, t:Stream|None = None) -> Stream: - if t: ## Partial differential equation + def __call__(self, obj: Stream, t: Stream | None = None) -> Stream: + if t: ## Partial differential equation f1 = self.f(obj, t) - f2 = self.f(obj + (self.dt/2) * f1, t + (self.dt/2)) - f3 = self.f(obj + (self.dt/2) * f2, t + (self.dt/2)) + f2 = self.f(obj + (self.dt / 2) * f1, t + (self.dt / 2)) + f3 = self.f(obj + (self.dt / 2) * f2, t + (self.dt / 2)) f4 = self.f(obj + self.dt * f3, t + self.dt) - else: ## Ordinary differential equation + else: ## Ordinary differential equation f1 = self.f(obj) - f2 = self.f(obj + (self.dt/2) * f1) - f3 = self.f(obj + (self.dt/2) * f2) + f2 = self.f(obj + (self.dt / 2) * f1) + f3 = self.f(obj + (self.dt / 2) * f2) f4 = self.f(obj + self.dt * f3) - return obj + (self.dt/6) * (f1 + 2*f2 + 2*f3 + f4) + return obj + (self.dt / 6) * (f1 + 2 * f2 + 2 * f3 + f4) + # class ForwardEuler_Layer(nn.Module): # #: :noindex: @@ -108,7 +116,7 @@ def __call__(self, obj:Stream, t:Stream|None = None) -> Stream: # func = globals()[self.name] # print(f'ForwardEuler executing function name {self.name} which is {func}') # return x + self.dt * func(x) - + # def createForwardEuler(name, *inputs): # #: :noindex: # return ForwardEuler_Layer(inputs[0]) diff --git a/nnodely/layers/timeoperation.py b/nnodely/layers/timeoperation.py index d85172db..ac985f43 100644 --- a/nnodely/layers/timeoperation.py +++ b/nnodely/layers/timeoperation.py @@ -7,14 +7,12 @@ from nnodely.basic.model import Model from nnodely.support.fixstepsolver import Euler, Trapezoidal -SOLVERS = { - 'euler': Euler, - 'trapezoidal': Trapezoidal -} +SOLVERS = {"euler": Euler, "trapezoidal": Trapezoidal} # Binary operators -int_relation_name = 'Integrate' -der_relation_name = 'Differentiate' +int_relation_name = "Integrate" +der_relation_name = "Differentiate" + class Integrate(Stream, ToStream): """ @@ -24,18 +22,28 @@ class Integrate(Stream, ToStream): ---------- method : is the integration method """ + @enforce_types - def __init__(self, output:Stream, *, - int_name:str|None = None, der_name:str|None = None, method:str = 'euler') -> Stream: + def __init__( + self, + output: Stream, + *, + int_name: str | None = None, + der_name: str | None = None, + method: str = "euler", + ) -> Stream: if int_name is None: int_name = output.name + "_int" + str(NeuObj.count) if der_name is None: der_name = output.name + "_der" + str(NeuObj.count) - check(method in SOLVERS, ValueError, f"The method '{method}' is not supported yet") - solver = SOLVERS[method](int_name,der_name) + check( + method in SOLVERS, ValueError, f"The method '{method}' is not supported yet" + ) + solver = SOLVERS[method](int_name, der_name) output_int = solver.integrate(output) super().__init__(output_int.name, output_int.json, output_int.dim) + class Differentiate(Stream, ToStream): """ This operation Differentiate a Stream with respect to time or another Stream @@ -44,25 +52,44 @@ class Differentiate(Stream, ToStream): ---------- method : is the derivative method """ + @enforce_types - def __init__(self, output:Stream, input:Stream = None, *, - int_name:str|None = None, der_name:str|None = None, method:str = 'euler') -> Stream: + def __init__( + self, + output: Stream, + input: Stream = None, + *, + int_name: str | None = None, + der_name: str | None = None, + method: str = "euler", + ) -> Stream: if input is None: if int_name is None: int_name = output.name + "_int" + str(NeuObj.count) if der_name is None: der_name = output.name + "_der" + str(NeuObj.count) - check(method in SOLVERS, ValueError, f"The method '{method}' is not supported yet") - solver = SOLVERS[method](int_name,der_name) + check( + method in SOLVERS, + ValueError, + f"The method '{method}' is not supported yet", + ) + solver = SOLVERS[method](int_name, der_name) output_der = solver.derivate(output) super().__init__(output_der.name, output_der.json, output_der.dim) else: - super().__init__(der_relation_name + str(Stream.count), merge(output.json,input.json), input.dim) - self.json['Relations'][self.name] = [der_relation_name, [output.name, input.name]] + super().__init__( + der_relation_name + str(Stream.count), + merge(output.json, input.json), + input.dim, + ) + self.json["Relations"][self.name] = [ + der_relation_name, + [output.name, input.name], + ] subjson = subjson_from_relation(self.json, input.name) - grad_inputs = subjson['Inputs'].keys() + grad_inputs = subjson["Inputs"].keys() for i in grad_inputs: - self.json['Inputs'][i]['type'] = 'derivate' + self.json["Inputs"][i]["type"] = "derivate" class Differentiate_Layer(nn.Module): @@ -71,10 +98,19 @@ def __init__(self): super(Differentiate_Layer, self).__init__() def forward(self, *inputs): - return torch.autograd.grad(inputs[0], inputs[1], grad_outputs=torch.ones_like(inputs[0]), create_graph=True, retain_graph=True, allow_unused=False)[0] + return torch.autograd.grad( + inputs[0], + inputs[1], + grad_outputs=torch.ones_like(inputs[0]), + create_graph=True, + retain_graph=True, + allow_unused=False, + )[0] + def createAdd(name, *inputs): #: :noindex: return Differentiate_Layer() + setattr(Model, der_relation_name, createAdd) diff --git a/nnodely/layers/trigonometric.py b/nnodely/layers/trigonometric.py index 932cabad..9ebba516 100644 --- a/nnodely/layers/trigonometric.py +++ b/nnodely/layers/trigonometric.py @@ -6,19 +6,20 @@ from nnodely.support.utils import check, enforce_types from nnodely.layers.parameter import Parameter, Constant -sin_relation_name = 'Sin' -cos_relation_name = 'Cos' -tan_relation_name = 'Tan' -tanh_relation_name = 'Tanh' -cosh_relation_name = 'Cosh' -sech_relation_name = 'Sech' +sin_relation_name = "Sin" +cos_relation_name = "Cos" +tan_relation_name = "Tan" +tanh_relation_name = "Tanh" +cosh_relation_name = "Cosh" +sech_relation_name = "Sech" + class Sin(Stream, ToStream): """ Implement the sine function given an input relation. See also: - Official PyTorch Sin documentation: + Official PyTorch Sin documentation: `torch.sin `_ :param obj: the input relation stream @@ -28,20 +29,25 @@ class Sin(Stream, ToStream): -------- .. include:: /examples_basics/layer_module_ex/trig_module_ex/sin.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|int|float) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | int | float) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Sin operation.") - super().__init__(sin_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [sin_relation_name, [obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Sin operation.", + ) + super().__init__(sin_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [sin_relation_name, [obj.name]] + class Cos(Stream, ToStream): """ Implement the cosine function given an input relation. See also: - Official PyTorch Cos documentation: + Official PyTorch Cos documentation: `torch.cos `_ :param obj: the input relation stream @@ -51,20 +57,25 @@ class Cos(Stream, ToStream): -------- .. include:: /examples_basics/layer_module_ex/trig_module_ex/cos.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|int|float) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | int | float) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Cos operation.") - super().__init__(cos_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [cos_relation_name, [obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Cos operation.", + ) + super().__init__(cos_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [cos_relation_name, [obj.name]] + class Tan(Stream, ToStream): """ Implement the tangent function given an input relation. See also: - Official PyTorch Tan documentation: + Official PyTorch Tan documentation: `torch.tan `_ :param obj: the input relation stream @@ -74,20 +85,25 @@ class Tan(Stream, ToStream): -------- .. include:: /examples_basics/layer_module_ex/trig_module_ex/tan.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|int|float) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | int | float) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Tan operation.") - super().__init__(tan_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [tan_relation_name, [obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Tan operation.", + ) + super().__init__(tan_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [tan_relation_name, [obj.name]] + class Cosh(Stream, ToStream): """ Returns a new tensor with the hyperbolic cosine of the elements of input. See also: - Official PyTorch Cosh documentation: + Official PyTorch Cosh documentation: `torch.cosh `_ :param obj: the input relation stream @@ -97,12 +113,17 @@ class Cosh(Stream, ToStream): -------- .. include:: /examples_basics/layer_module_ex/trig_module_ex/cosh.rst """ - def __init__(self, obj:Stream) -> Stream: + + def __init__(self, obj: Stream) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Cosh operation.") - super().__init__(cosh_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [cosh_relation_name, [obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Cosh operation.", + ) + super().__init__(cosh_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [cosh_relation_name, [obj.name]] + class Sech(Stream, ToStream): """ @@ -115,99 +136,140 @@ class Sech(Stream, ToStream): -------- .. include:: /examples_basics/layer_module_ex/trig_module_ex/sech.rst """ - def __init__(self, obj:Stream) -> Stream: + + def __init__(self, obj: Stream) -> Stream: obj = toStream(obj) - check(type(obj) is Stream, TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Sech operation.") - super().__init__(sech_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [sech_relation_name, [obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Sech operation.", + ) + super().__init__(sech_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [sech_relation_name, [obj.name]] + class Tanh(Stream, ToStream): """ - Implement the Hyperbolic Tangent (Tanh) relation function. + Implement the Hyperbolic Tangent (Tanh) relation function. - See also: - Official PyTorch tanh documentation: - `torch.nn.Tanh `_ + See also: + Official PyTorch tanh documentation: + `torch.nn.Tanh `_ - :param obj: The relation stream. - :type obj: Stream + :param obj: The relation stream. + :type obj: Stream - Example: - -------- - .. include:: /examples_basics/layer_module_ex/trig_module_ex/tanh.rst + Example: + -------- + .. include:: /examples_basics/layer_module_ex/trig_module_ex/tanh.rst """ + @enforce_types - def __init__(self, obj:Stream|Parameter|Constant|float|int) -> Stream: + def __init__(self, obj: Stream | Parameter | Constant | float | int) -> Stream: obj = toStream(obj) - check(type(obj) is Stream,TypeError, - f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.") - super().__init__(tanh_relation_name + str(Stream.count),obj.json,obj.dim) - self.json['Relations'][self.name] = [tanh_relation_name,[obj.name]] + check( + type(obj) is Stream, + TypeError, + f"The type of {obj} is {type(obj)} and is not supported for Tanh operation.", + ) + super().__init__(tanh_relation_name + str(Stream.count), obj.json, obj.dim) + self.json["Relations"][self.name] = [tanh_relation_name, [obj.name]] + class Sin_Layer(nn.Module): - def __init__(self,): + def __init__( + self, + ): super(Sin_Layer, self).__init__() + def forward(self, x): return torch.sin(x) + def createSin(self, *inputs): return Sin_Layer() + class Cos_Layer(nn.Module): - def __init__(self,): + def __init__( + self, + ): super(Cos_Layer, self).__init__() + def forward(self, x): return torch.cos(x) + def createCos(self, *inputs): return Cos_Layer() + class Tan_Layer(nn.Module): - def __init__(self,): + def __init__( + self, + ): super(Tan_Layer, self).__init__() + def forward(self, x): return torch.tan(x) + def createTan(self, *inputs): return Tan_Layer() + class Cosh_Layer(nn.Module): - def __init__(self,): + def __init__( + self, + ): super(Cosh_Layer, self).__init__() + def forward(self, x): return torch.cosh(x) + def createCosh(self, *inputs): return Cosh_Layer() + class Tanh_Layer(nn.Module): """ - :noindex: + :noindex: """ - def __init__(self,): + + def __init__( + self, + ): super(Tanh_Layer, self).__init__() + def forward(self, x): return torch.tanh(x) + def createTanh(self, *input): """ - :noindex: + :noindex: """ return Tanh_Layer() + class Sech_Layer(nn.Module): - def __init__(self,): + def __init__( + self, + ): super(Sech_Layer, self).__init__() + def forward(self, x): - return 1/torch.cosh(x) + return 1 / torch.cosh(x) + def createSech(self, *inputs): return Sech_Layer() + setattr(Model, sin_relation_name, createSin) setattr(Model, cos_relation_name, createCos) setattr(Model, tan_relation_name, createTan) setattr(Model, cosh_relation_name, createCosh) setattr(Model, tanh_relation_name, createTanh) -setattr(Model, sech_relation_name, createSech) \ No newline at end of file +setattr(Model, sech_relation_name, createSech) diff --git a/nnodely/nnodely.py b/nnodely/nnodely.py index 083f2e4c..8b6359ce 100644 --- a/nnodely/nnodely.py +++ b/nnodely/nnodely.py @@ -16,13 +16,15 @@ from nnodely.support.utils import ReadOnlyDict, ParamDict, enforce_types, check from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.INFO) @enforce_types -def clearNames(names:str|list|None = None): +def clearNames(names: str | list | None = None): NeuObj.clearNames(names) + class Modely(Composer, Trainer, Loader, Validator, Exporter): """ Create the main object, the nnodely object, that will be used to create the network, train and export it. @@ -46,21 +48,25 @@ class Modely(Composer, Trainer, Loader, Validator, Exporter): ------- >>> model = Modely() """ + @enforce_types - def __init__(self, *, - visualizer:str|EmptyVisualizer|None = 'Standard', - exporter:str|EmptyExporter|None = 'Standard', - seed:int|None = None, - workspace:str|None = None, - log_internal:bool = False, - save_history:bool = False): + def __init__( + self, + *, + visualizer: str | EmptyVisualizer | None = "Standard", + exporter: str | EmptyExporter | None = "Standard", + seed: int | None = None, + workspace: str | None = None, + log_internal: bool = False, + save_history: bool = False, + ): ## Set the random seed for reproducibility if seed is not None: self.resetSeed(seed) # Visualizer - if visualizer == 'Standard': + if visualizer == "Standard": self.visualizer = TextVisualizer(1) elif visualizer != None: self.visualizer = visualizer @@ -87,7 +93,9 @@ def neuralized(self): @neuralized.setter def neuralized(self, value): - raise AttributeError("Cannot modify read-only property 'neuralized' use neuralizeModel() instead.") + raise AttributeError( + "Cannot modify read-only property 'neuralized' use neuralizeModel() instead." + ) @property def traced(self): @@ -100,24 +108,31 @@ def traced(self, value): @property def parameters(self): if self._neuralized: - return ParamDict(self._model_def['Parameters'], self._model.all_parameters) + return ParamDict(self._model_def["Parameters"], self._model.all_parameters) else: - return ParamDict(self._model_def['Parameters']) + return ParamDict(self._model_def["Parameters"]) @property def constants(self): - return ReadOnlyDict({key:value.detach().numpy().tolist() for key,value in self._model.all_constants}) + return ReadOnlyDict( + { + key: value.detach().numpy().tolist() + for key, value in self._model.all_constants + } + ) @property def states(self): - return {key:value.detach().numpy().tolist() for key,value in self._states.items()} + return { + key: value.detach().numpy().tolist() for key, value in self._states.items() + } @property def json(self): return copy.deepcopy(self._model_def._ModelDef__json) @enforce_types - def resetSeed(self, seed:int) -> None: + def resetSeed(self, seed: int) -> None: """ Resets the random seed for reproducibility. @@ -135,7 +150,13 @@ def resetSeed(self, seed:int) -> None: random.seed(seed) ## set the random module seed np.random.seed(seed) ## set the numpy seed - def trainAndAnalyze(self, *, test_dataset: str | list | dict | None = None, test_batch_size: int = 128, **kwargs): + def trainAndAnalyze( + self, + *, + test_dataset: str | list | dict | None = None, + test_batch_size: int = 128, + **kwargs, + ): """ Trains the model using the provided datasets and parameters. After training, it analyzes the results on the training, validation, and test datasets. @@ -212,38 +233,83 @@ def trainAndAnalyze(self, *, test_dataset: str | list | dict | None = None, test self.trainModel(**kwargs) params = self.running_parameters - minimize_gain = params['minimize_gain'] - closed_loop, connect, prediction_samples = params['closed_loop'], params['connect'], params['prediction_samples'] - - if kwargs.get('train_dataset', None) is None: - check(test_dataset is None, ValueError, 'If train_dataset is None, test_dataset must also be None.') + minimize_gain = params["minimize_gain"] + closed_loop, connect, prediction_samples = ( + params["closed_loop"], + params["connect"], + params["prediction_samples"], + ) + + if kwargs.get("train_dataset", None) is None: + check( + test_dataset is None, + ValueError, + "If train_dataset is None, test_dataset must also be None.", + ) else: - params['test_tag'] = self._get_tag(test_dataset) - params['XY_test'] = self._get_data(test_dataset) - params['n_samples_test'] = next(iter(params['XY_test'].values())).size(0) if params['XY_test'] else 0 - params['test_indexes'] = self._get_batch_indexes(test_dataset, params['n_samples_test'], prediction_samples) + params["test_tag"] = self._get_tag(test_dataset) + params["XY_test"] = self._get_data(test_dataset) + params["n_samples_test"] = ( + next(iter(params["XY_test"].values())).size(0) + if params["XY_test"] + else 0 + ) + params["test_indexes"] = self._get_batch_indexes( + test_dataset, params["n_samples_test"], prediction_samples + ) ## Training set Results - self._analyze(params['XY_train'], dataset_tag=params['train_tag'], indexes=params['train_indexes'], minimize_gain=minimize_gain, - closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples, - step=params['train_step'], batch_size=params['train_batch_size']) - + self._analyze( + params["XY_train"], + dataset_tag=params["train_tag"], + indexes=params["train_indexes"], + minimize_gain=minimize_gain, + closed_loop=closed_loop, + connect=connect, + prediction_samples=prediction_samples, + step=params["train_step"], + batch_size=params["train_batch_size"], + ) + ## Validation set Results - if params['n_samples_val'] > 0: - self._analyze(params['XY_val'], dataset_tag=params['val_tag'], indexes=params['val_indexes'], minimize_gain=minimize_gain, - closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples, - step=params['val_step'], batch_size=params['val_batch_size']) + if params["n_samples_val"] > 0: + self._analyze( + params["XY_val"], + dataset_tag=params["val_tag"], + indexes=params["val_indexes"], + minimize_gain=minimize_gain, + closed_loop=closed_loop, + connect=connect, + prediction_samples=prediction_samples, + step=params["val_step"], + batch_size=params["val_batch_size"], + ) else: - log.warning("Validation dataset is empty. Skipping validation results analysis.") + log.warning( + "Validation dataset is empty. Skipping validation results analysis." + ) ## Test set Results - if params['n_samples_test'] > 0: - params['test_batch_size'] = self._clip_batch_size(len(params['test_indexes']), test_batch_size) - params['test_step'] = self._clip_step(params['step'], params['test_indexes'], params['test_batch_size']) - self._analyze(params['XY_test'], dataset_tag=params['test_tag'], indexes=params['test_indexes'], minimize_gain=minimize_gain, - closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples, - step=params['test_step'], batch_size=test_batch_size) + if params["n_samples_test"] > 0: + params["test_batch_size"] = self._clip_batch_size( + len(params["test_indexes"]), test_batch_size + ) + params["test_step"] = self._clip_step( + params["step"], params["test_indexes"], params["test_batch_size"] + ) + self._analyze( + params["XY_test"], + dataset_tag=params["test_tag"], + indexes=params["test_indexes"], + minimize_gain=minimize_gain, + closed_loop=closed_loop, + connect=connect, + prediction_samples=prediction_samples, + step=params["test_step"], + batch_size=test_batch_size, + ) else: log.warning("Test dataset is empty. Skipping test results analysis.") -nnodely = Modely \ No newline at end of file + +nnodely = Modely diff --git a/nnodely/operators/composer.py b/nnodely/operators/composer.py index 563777f6..1f9948d5 100644 --- a/nnodely/operators/composer.py +++ b/nnodely/operators/composer.py @@ -6,29 +6,44 @@ from nnodely.basic.modeldef import ModelDef from nnodely.basic.model import Model -from nnodely.support.utils import check, TORCH_DTYPE, NP_DTYPE, enforce_types, tensor_to_list +from nnodely.support.utils import ( + check, + TORCH_DTYPE, + NP_DTYPE, + enforce_types, + tensor_to_list, +) from nnodely.support.mathutils import argmax_dict, argmin_dict from nnodely.basic.relation import Stream from nnodely.layers.input import Input from nnodely.layers.output import Output from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) + class Composer(Network): @enforce_types def __init__(self): - check(type(self) is not Composer, TypeError, "Composer class cannot be instantiated directly") + check( + type(self) is not Composer, + TypeError, + "Composer class cannot be instantiated directly", + ) super().__init__() def __addInfo(self) -> None: - total_params = sum(p.numel() for p in self._model.parameters() if p.requires_grad) - self._model_def['Info']['num_parameters'] = total_params + total_params = sum( + p.numel() for p in self._model.parameters() if p.requires_grad + ) + self._model_def["Info"]["num_parameters"] = total_params from nnodely import __version__ - self._model_def['Info']['nnodely_version'] = __version__ + + self._model_def["Info"]["nnodely_version"] = __version__ @enforce_types - def addModel(self, name:str, stream_list:list|Output) -> None: + def addModel(self, name: str, stream_list: list | Output) -> None: """ Adds a new model with the given name along with a list of Outputs. @@ -47,7 +62,7 @@ def addModel(self, name:str, stream_list:list|Output) -> None: self._neuralized = False @enforce_types - def removeModel(self, name_list:list|str) -> None: + def removeModel(self, name_list: list | str) -> None: """ Removes models with the given list of names. @@ -64,7 +79,13 @@ def removeModel(self, name_list:list|str) -> None: self._neuralized = False @enforce_types - def addConnect(self, stream_out:str|Output|Stream, input_in:str|Input, *, local:bool=False) -> None: + def addConnect( + self, + stream_out: str | Output | Stream, + input_in: str | Input, + *, + local: bool = False, + ) -> None: """ Adds a connection from a relation stream to an input. @@ -80,11 +101,17 @@ def addConnect(self, stream_out:str|Output|Stream, input_in:str|Input, *, local: .. include:: /examples_basics/compser_module_ex/addConnect.rst """ - self._model_def.addConnection(stream_out, input_in,'connect', local) + self._model_def.addConnection(stream_out, input_in, "connect", local) self._neuralized = False @enforce_types - def addClosedLoop(self, stream_out:str|Output|Stream, input_in:str|Input, *, local:bool=False) -> None: + def addClosedLoop( + self, + stream_out: str | Output | Stream, + input_in: str | Input, + *, + local: bool = False, + ) -> None: """ Adds a closed loop connection from a relation stream to an input. @@ -100,11 +127,11 @@ def addClosedLoop(self, stream_out:str|Output|Stream, input_in:str|Input, *, loc .. include:: /examples_basics/compser_module_ex/addClosedLoop.rst """ - self._model_def.addConnection(stream_out, input_in,'closedLoop', local) + self._model_def.addConnection(stream_out, input_in, "closedLoop", local) self._neuralized = False @enforce_types - def removeConnection(self, input_in:str|Input) -> None: + def removeConnection(self, input_in: str | Input) -> None: """ Remove a closed loop or connect connection from an input. @@ -126,7 +153,13 @@ def removeConnection(self, input_in:str|Input) -> None: self._neuralized = False @enforce_types - def neuralizeModel(self, sample_time:float|int|None = None, *, clear_model:bool = False, model_def:dict|None = None) -> None: + def neuralizeModel( + self, + sample_time: float | int | None = None, + *, + clear_model: bool = False, + model_def: dict | None = None, + ) -> None: """ Neuralizes the model, preparing it for inference and training. This method creates a neural network model starting from the model definition. It will also create all the time windows and correct slicing for all the inputs defined. @@ -151,29 +184,45 @@ def neuralizeModel(self, sample_time:float|int|None = None, *, clear_model:bool .. include:: /examples_basics/compser_module_ex/neuralizeModel.rst """ if model_def is not None: - check(sample_time == None, ValueError, 'The sample_time must be None if a model_def is provided') - check(clear_model == False, ValueError, 'The clear_model must be False if a model_def is provided') + check( + sample_time == None, + ValueError, + "The sample_time must be None if a model_def is provided", + ) + check( + clear_model == False, + ValueError, + "The clear_model must be False if a model_def is provided", + ) self._model_def = ModelDef(model_def) else: - self._model_def.updateParameters(model = None, clear_model = clear_model) + self._model_def.updateParameters(model=None, clear_model=clear_model) self._model_def.setBuildWindow(sample_time) self._model = Model(self._model_def.getJson()) self.__addInfo() - self._input_ns_backward = {key:value['ns'][0] for key, value in self._model_def['Inputs'].items()} - self._input_ns_forward = {key:value['ns'][1] for key, value in self._model_def['Inputs'].items()} + self._input_ns_backward = { + key: value["ns"][0] for key, value in self._model_def["Inputs"].items() + } + self._input_ns_forward = { + key: value["ns"][1] for key, value in self._model_def["Inputs"].items() + } self._max_samples_backward = max(self._input_ns_backward.values()) self._max_samples_forward = max(self._input_ns_forward.values()) self._input_n_samples = {} - for key, value in self._model_def['Inputs'].items(): + for key, value in self._model_def["Inputs"].items(): if self._input_ns_forward[key] >= 0: - if 'closedLoop' in value: + if "closedLoop" in value: log.warning(f"Closed loop on {key} with sample in the future.") - if 'connect' in value: + if "connect" in value: log.warning(f"Connect on {key} with sample in the future.") - self._input_n_samples[key] = self._input_ns_backward[key] + self._input_ns_forward[key] - self._max_n_samples = max(self._input_ns_backward.values()) + max(self._input_ns_forward.values()) + self._input_n_samples[key] = ( + self._input_ns_backward[key] + self._input_ns_forward[key] + ) + self._max_n_samples = max(self._input_ns_backward.values()) + max( + self._input_ns_forward.values() + ) ## Initialize States self.resetStates() @@ -186,7 +235,17 @@ def neuralizeModel(self, sample_time:float|int|None = None, *, clear_model:bool self.visualizer.showBuiltModel() @enforce_types - def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, connect:dict={}, prediction_samples:str|int='auto', num_of_samples:int|None=None, log_internal:bool=False) -> dict: + def __call__( + self, + inputs: dict = {}, + *, + sampled: bool = False, + closed_loop: dict = {}, + connect: dict = {}, + prediction_samples: str | int = "auto", + num_of_samples: int | None = None, + log_internal: bool = False, + ) -> dict: """ Performs inference on the model. @@ -219,99 +278,149 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c Examples -------- - + .. include:: /examples_basics/inference_module_ex/inference.rst """ ## Copy dict for avoid python bug inputs = copy.deepcopy(inputs) - all_closed_loop = copy.deepcopy(closed_loop) #| self._model_def._input_closed_loop - all_connect = copy.deepcopy(connect) #| self._model_def._input_connect + all_closed_loop = copy.deepcopy( + closed_loop + ) # | self._model_def._input_closed_loop + all_connect = copy.deepcopy(connect) # | self._model_def._input_connect ## Check neuralize check(self.neuralized, RuntimeError, "The network is not neuralized.") ## Check closed loop integrity - prediction_samples = self._setup_recurrent_variables(prediction_samples, all_closed_loop, all_connect) + prediction_samples = self._setup_recurrent_variables( + prediction_samples, all_closed_loop, all_connect + ) ## List of keys - model_inputs = list(self._model_def['Inputs'].keys()) - json_inputs = self._model_def['Inputs'] + model_inputs = list(self._model_def["Inputs"].keys()) + json_inputs = self._model_def["Inputs"] extra_inputs = list(set(list(inputs.keys())) - set(model_inputs)) - non_mandatory_inputs = list(all_closed_loop.keys()) + list(all_connect.keys()) + list(self._model_def.recurrentInputs().keys()) + non_mandatory_inputs = ( + list(all_closed_loop.keys()) + + list(all_connect.keys()) + + list(self._model_def.recurrentInputs().keys()) + ) mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs)) ## Remove extra inputs for key in extra_inputs: log.warning( - f'The provided input {key} is not used inside the network. the inference will continue without using it') + f"The provided input {key} is not used inside the network. the inference will continue without using it" + ) del inputs[key] ## Get the number of data windows for each input - num_of_windows = {key: len(value) for key, value in inputs.items()} if sampled else { - key: len(value) - self._input_n_samples[key] + 1 for key, value in inputs.items()} + num_of_windows = ( + {key: len(value) for key, value in inputs.items()} + if sampled + else { + key: len(value) - self._input_n_samples[key] + 1 + for key, value in inputs.items() + } + ) if num_of_samples is not None and sampled == True: - log.warning(f'num_of_samples is ignored if sampled is equal to True') + log.warning(f"num_of_samples is ignored if sampled is equal to True") ## Get the maximum inference window if num_of_samples and not sampled: window_dim = num_of_samples for key in inputs.keys(): - input_dim = self._model_def['Inputs'][key]['dim'] - new_samples = num_of_samples - (len(inputs[key]) - self._input_n_samples[key] + 1) + input_dim = self._model_def["Inputs"][key]["dim"] + new_samples = num_of_samples - ( + len(inputs[key]) - self._input_n_samples[key] + 1 + ) if input_dim > 1: - log.warning(f'The variable {key} is filled with {new_samples} samples equal to zeros.') - inputs[key] += [[0 for _ in range(input_dim)] for _ in range(new_samples)] + log.warning( + f"The variable {key} is filled with {new_samples} samples equal to zeros." + ) + inputs[key] += [ + [0 for _ in range(input_dim)] for _ in range(new_samples) + ] else: - log.warning(f'The variable {key} is filled with {new_samples} samples equal to zeros.') + log.warning( + f"The variable {key} is filled with {new_samples} samples equal to zeros." + ) inputs[key] += [0 for _ in range(new_samples)] elif inputs: windows = [] for key in inputs.keys(): if key in mandatory_inputs: - n_samples = len(inputs[key]) if sampled else len(inputs[key]) - self._model_def['Inputs'][key]['ntot'] + 1 + n_samples = ( + len(inputs[key]) + if sampled + else len(inputs[key]) + - self._model_def["Inputs"][key]["ntot"] + + 1 + ) windows.append(n_samples) if not windows: for key in inputs.keys(): if key in non_mandatory_inputs: if key in model_inputs: - n_samples = len(inputs[key]) if sampled else len(inputs[key]) - self._model_def['Inputs'][key]['ntot'] + 1 + n_samples = ( + len(inputs[key]) + if sampled + else len(inputs[key]) + - self._model_def["Inputs"][key]["ntot"] + + 1 + ) windows.append(n_samples) window_dim = min(windows) if windows else 0 else: ## No inputs window_dim = 1 if non_mandatory_inputs else 0 - check(window_dim > 0, StopIteration, f'Missing samples in the input window') + check(window_dim > 0, StopIteration, f"Missing samples in the input window") if len(set(num_of_windows.values())) > 1: max_ind_key, max_dim = argmax_dict(num_of_windows) min_ind_key, min_dim = argmin_dict(num_of_windows) log.warning( - f'Different number of samples between inputs [MAX {num_of_windows[max_ind_key]} = {max_dim}; MIN {num_of_windows[min_ind_key]} = {min_dim}]') + f"Different number of samples between inputs [MAX {num_of_windows[max_ind_key]} = {max_dim}; MIN {num_of_windows[min_ind_key]} = {min_dim}]" + ) ## Autofill the missing inputs provided_inputs = list(inputs.keys()) missing_inputs = list(set(mandatory_inputs) - set(provided_inputs)) if missing_inputs: - log.warning(f'Inputs not provided: {missing_inputs}. Autofilling with zeros..') + log.warning( + f"Inputs not provided: {missing_inputs}. Autofilling with zeros.." + ) for key in missing_inputs: inputs[key] = np.zeros( - shape=(self._input_n_samples[key] + window_dim - 1, self._model_def['Inputs'][key]['dim']), - dtype=NP_DTYPE).tolist() + shape=( + self._input_n_samples[key] + window_dim - 1, + self._model_def["Inputs"][key]["dim"], + ), + dtype=NP_DTYPE, + ).tolist() ## Transform inputs in 3D Tensors for key in inputs.keys(): - input_dim = json_inputs[key]['dim'] + input_dim = json_inputs[key]["dim"] inputs[key] = torch.from_numpy(np.array(inputs[key])).to(TORCH_DTYPE) if input_dim > 1: correct_dim = 3 if sampled else 2 - check(len(inputs[key].shape) == correct_dim, ValueError, - f'The input {key} must have {correct_dim} dimensions') - check(inputs[key].shape[correct_dim - 1] == input_dim, ValueError, - f'The second dimension of the input "{key}" must be equal to {input_dim}') - - if input_dim == 1 and inputs[key].shape[-1] != 1: ## add the input dimension + check( + len(inputs[key].shape) == correct_dim, + ValueError, + f"The input {key} must have {correct_dim} dimensions", + ) + check( + inputs[key].shape[correct_dim - 1] == input_dim, + ValueError, + f'The second dimension of the input "{key}" must be equal to {input_dim}', + ) + + if ( + input_dim == 1 and inputs[key].shape[-1] != 1 + ): ## add the input dimension inputs[key] = inputs[key].unsqueeze(-1) if inputs[key].ndim <= 1: ## add the batch dimension inputs[key] = inputs[key].unsqueeze(0) @@ -320,15 +429,24 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c ## initialize the resulting dictionary result_dict = {} - for key in self._model_def['Outputs'].keys(): + for key in self._model_def["Outputs"].keys(): result_dict[key] = [] if log_internal: - internals_dict = {'ingress': [], 'state': [], 'closedLoop': [], 'connect': []} + internals_dict = { + "ingress": [], + "state": [], + "closedLoop": [], + "connect": [], + } ## Inference - with (torch.enable_grad() if self._get_gradient_on_inference() else torch.inference_mode()): + with ( + torch.enable_grad() + if self._get_gradient_on_inference() + else torch.inference_mode() + ): ## Update with virtual states - if prediction_samples == 'auto' or prediction_samples >= 0: + if prediction_samples == "auto" or prediction_samples >= 0: self._model.update(closed_loop=all_closed_loop, connect=all_connect) else: self._model.update(disconnect=True) @@ -339,21 +457,31 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c for idx in range(window_dim): ## Get mandatory data inputs for key in mandatory_inputs: - X[key] = inputs[key][idx:idx + 1] if sampled else inputs[key][:,idx:idx + self._input_n_samples[key]] - if 'type' in json_inputs[key].keys(): + X[key] = ( + inputs[key][idx : idx + 1] + if sampled + else inputs[key][:, idx : idx + self._input_n_samples[key]] + ) + if "type" in json_inputs[key].keys(): X[key] = X[key].requires_grad_(True) ## reset states - if count == 0 or prediction_samples == 'auto': + if count == 0 or prediction_samples == "auto": count = prediction_samples for key in non_mandatory_inputs: ## Get non mandatory data (from inputs, from states, or with zeros) ## If it is given as input AND ## if prediction_samples is 'auto' and there are enough samples OR ## if prediction_samples is NOT 'auto' if key in inputs.keys() and ( - (prediction_samples == 'auto' and idx < num_of_windows[key]) or \ - (prediction_samples != 'auto') + (prediction_samples == "auto" and idx < num_of_windows[key]) + or (prediction_samples != "auto") ): - X[key] = inputs[key][idx:idx + 1] if sampled else inputs[key][:,idx:idx + self._input_n_samples[key]] + X[key] = ( + inputs[key][idx : idx + 1] + if sampled + else inputs[key][ + :, idx : idx + self._input_n_samples[key] + ] + ) # if 0 in X[key].shape: # window_size = self._input_n_samples[key] # dim = json_inputs[key]['dim'] @@ -362,47 +490,51 @@ def __call__(self, inputs:dict={}, *, sampled:bool=False, closed_loop:dict={}, c ## if prediction_samples = 'auto' and there are not enough samples OR ## it is the first iteration with prediction_samples = None elif key in self._states.keys() and ( - prediction_samples == 'auto' or - (first and prediction_samples == None) + prediction_samples == "auto" + or (first and prediction_samples == None) ): X[key] = self._states[key] else: - ## if there are no samples + ## if there are no samples window_size = self._input_n_samples[key] - dim = json_inputs[key]['dim'] - X[key] = torch.zeros(size=(1, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False) - - if 'type' in json_inputs[key].keys(): + dim = json_inputs[key]["dim"] + X[key] = torch.zeros( + size=(1, window_size, dim), + dtype=TORCH_DTYPE, + requires_grad=False, + ) + + if "type" in json_inputs[key].keys(): X[key] = X[key].requires_grad_(True) first = False else: # Remove the gradient of the previous forward for key in X.keys(): - if 'type' in json_inputs[key].keys(): + if "type" in json_inputs[key].keys(): X[key] = X[key].detach().requires_grad_(True) count -= 1 ## Forward pass result, _, out_closed_loop, out_connect = self._model(X) if log_internal: - internals_dict['ingress'].append(tensor_to_list(X)) - internals_dict['closedLoop'].append(out_closed_loop) - internals_dict['connect'].append(out_connect) + internals_dict["ingress"].append(tensor_to_list(X)) + internals_dict["closedLoop"].append(out_closed_loop) + internals_dict["connect"].append(out_connect) ## Append the prediction of the current sample to the result dictionary - for key in self._model_def['Outputs'].keys(): + for key in self._model_def["Outputs"].keys(): if result[key].shape[-1] == 1: result[key] = result[key].squeeze(-1) if result[key].shape[-1] == 1: result[key] = result[key].squeeze(-1) - result_dict[key].append(result[key].detach().squeeze(dim=0).tolist()) + result_dict[key].append( + result[key].detach().squeeze(dim=0).tolist() + ) ## Update closed_loop and connect if prediction_samples: self._update_state(X, out_closed_loop, out_connect) - + ## Remove virtual states self._remove_virtual_states(connect, closed_loop) return result_dict if not log_internal else (result_dict, internals_dict) - - diff --git a/nnodely/operators/exporter.py b/nnodely/operators/exporter.py index 79cbed3e..7baaebb5 100644 --- a/nnodely/operators/exporter.py +++ b/nnodely/operators/exporter.py @@ -9,13 +9,25 @@ class Exporter(Network): @enforce_types - def __init__(self, exporter:EmptyExporter|str|None=None, workspace:str|None=None, *, save_history:bool=False): - check(type(self) is not Exporter, TypeError, "Exporter class cannot be instantiated directly") + def __init__( + self, + exporter: EmptyExporter | str | None = None, + workspace: str | None = None, + *, + save_history: bool = False, + ): + check( + type(self) is not Exporter, + TypeError, + "Exporter class cannot be instantiated directly", + ) super().__init__() # Exporter - if exporter == 'Standard': - self.__exporter = StandardExporter(workspace, self.visualizer, save_history=save_history) + if exporter == "Standard": + self.__exporter = StandardExporter( + workspace, self.visualizer, save_history=save_history + ) elif exporter != None: self.__exporter = exporter else: @@ -26,7 +38,13 @@ def getWorkspace(self) -> str: return self.__exporter.getWorkspace() @enforce_types - def saveTorchModel(self, name:str='net', model_folder:str|None=None, *, models:str|None=None) -> None: + def saveTorchModel( + self, + name: str = "net", + model_folder: str | None = None, + *, + models: str | None = None, + ) -> None: """ Saves the neural network model in PyTorch format. @@ -49,15 +67,21 @@ def saveTorchModel(self, name:str='net', model_folder:str|None=None, *, models:s .. include:: /examples_basics/export_module_ex/saveTorchModel.rst """ - check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.") - check(self._neuralized == True, RuntimeError, 'The model is not neuralized yet!') + check( + self._model_def.isDefined(), + RuntimeError, + "The network has not been defined.", + ) + check( + self._neuralized == True, RuntimeError, "The model is not neuralized yet!" + ) if models is not None: if type(models) is str: models = [models] - if name == 'net': - name += '_' + '_'.join(models) + if name == "net": + name += "_" + "_".join(models) model_def = ModelDef(self._model_def.getJson(models)) - model_def.setBuildWindow(self._model_def['Info']['SampleTime']) + model_def.setBuildWindow(self._model_def["Info"]["SampleTime"]) model_def.updateParameters(self._model) model = Model(model_def.getJson()) else: @@ -65,7 +89,9 @@ def saveTorchModel(self, name:str='net', model_folder:str|None=None, *, models:s self.__exporter.saveTorchModel(model, name, model_folder) @enforce_types - def loadTorchModel(self, name:str='net', model_folder:str|None=None) -> None: + def loadTorchModel( + self, name: str = "net", model_folder: str | None = None + ) -> None: """ Loads a neural network model from a PyTorch format file. @@ -86,11 +112,17 @@ def loadTorchModel(self, name:str='net', model_folder:str|None=None) -> None: .. include:: /examples_basics/export_module_ex/loadTorchModel.rst """ - check(self.neuralized == True, RuntimeError, 'The model is not neuralized yet.') + check(self.neuralized == True, RuntimeError, "The model is not neuralized yet.") self.__exporter.loadTorchModel(self._model, name, model_folder) @enforce_types - def saveModel(self, name:str='net', model_folder:str|None=None, *, models:str|list|None=None) -> None: + def saveModel( + self, + name: str = "net", + model_folder: str | None = None, + *, + models: str | list | None = None, + ) -> None: """ Saves the neural network model definition in a json file. @@ -113,21 +145,25 @@ def saveModel(self, name:str='net', model_folder:str|None=None, *, models:str|li .. include:: /examples_basics/export_module_ex/saveModel.rst """ - check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.") + check( + self._model_def.isDefined(), + RuntimeError, + "The network has not been defined.", + ) if models is not None: if type(models) is str: models = [models] - if name == 'net': - name += '_' + '_'.join(models) + if name == "net": + name += "_" + "_".join(models) model_def = ModelDef(self._model_def.getJson(models)) - model_def.setBuildWindow(self._model_def['Info']['SampleTime']) + model_def.setBuildWindow(self._model_def["Info"]["SampleTime"]) model_def.updateParameters(self._model) else: model_def = self._model_def self.__exporter.saveModel(model_def.getJson(), name, model_folder) @enforce_types - def loadModel(self, name:str='net', model_folder:str|None=None) -> None: + def loadModel(self, name: str = "net", model_folder: str | None = None) -> None: """ Loads a neural network model from a json file containing the model definition. @@ -150,17 +186,19 @@ def loadModel(self, name:str='net', model_folder:str|None=None) -> None: """ model_def = self.__exporter.loadModel(name, model_folder) check(model_def, RuntimeError, "Error to load the network.") - new_tags = (list(model_def['Inputs'].keys()) + - list(model_def['Functions'].keys()) + - list(model_def['Relations'].keys()) + - list(model_def['Parameters'].keys())+ - list(model_def['Constants'].keys())) + new_tags = ( + list(model_def["Inputs"].keys()) + + list(model_def["Functions"].keys()) + + list(model_def["Relations"].keys()) + + list(model_def["Parameters"].keys()) + + list(model_def["Constants"].keys()) + ) ## TODO: setting the Stream.count is not enough, we need a global tag manager # old_tags = list(self._model_def['Functions'].keys()) + list(self._model_def['Relations'].keys()) + list(self._model_def['Parameters'].keys()) if self._model_def is not None else [] # check that there are no common tags # common_tags = set(new_tags).intersection(set(old_tags)) # check(len(common_tags) == 0, RuntimeError, f"The model contains some tags that are already present in the current model: {common_tags}.\n Please rename them before loading the model.") - #check(Stream.count == 0, RuntimeError, "There are some defined Stream, loadModel can be called only at the beginning, when the neural graph is empty.") + # check(Stream.count == 0, RuntimeError, "There are some defined Stream, loadModel can be called only at the beginning, when the neural graph is empty.") Stream.count = Stream.count + len(new_tags) + 1 NeuObj.count = NeuObj.count + len(new_tags) + 1 self._model_def = ModelDef(model_def) @@ -169,7 +207,13 @@ def loadModel(self, name:str='net', model_folder:str|None=None) -> None: self._traced = False @enforce_types - def exportPythonModel(self, name:str='net', model_folder:str|None=None, *, models:str|None=None) -> None: + def exportPythonModel( + self, + name: str = "net", + model_folder: str | None = None, + *, + models: str | None = None, + ) -> None: """ Exports the neural network model as a standalone PyTorch Module class. @@ -194,20 +238,31 @@ def exportPythonModel(self, name:str='net', model_folder:str|None=None, *, model .. include:: /examples_basics/export_module_ex/exportPythonModel.rst """ - check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.") - check(self._traced == False, RuntimeError, - 'The model is traced and cannot be exported to Python.\n Run neuralizeModel() to recreate a standard model.') + check( + self._model_def.isDefined(), + RuntimeError, + "The network has not been defined.", + ) + check( + self._traced == False, + RuntimeError, + "The model is traced and cannot be exported to Python.\n Run neuralizeModel() to recreate a standard model.", + ) if models is not None: if type(models) is str: models = [models] - if name == 'net': - name += '_' + '_'.join(models) + if name == "net": + name += "_" + "_".join(models) model_def = ModelDef(self._model_def.getJson(models)) - model_def.setBuildWindow(self._model_def['Info']['SampleTime']) + model_def.setBuildWindow(self._model_def["Info"]["SampleTime"]) model_def.updateParameters(self._model) model = Model(model_def.getJson()) else: - check(self._neuralized == True, RuntimeError, 'The model is not neuralized yet.') + check( + self._neuralized == True, + RuntimeError, + "The model is not neuralized yet.", + ) model_def = self._model_def model = self._model model.update() @@ -215,7 +270,9 @@ def exportPythonModel(self, name:str='net', model_folder:str|None=None, *, model self.__exporter.exportPythonModel(model_def, model, name, model_folder) @enforce_types - def importPythonModel(self, name:str='net', model_folder:str|None=None) -> None: + def importPythonModel( + self, name: str = "net", model_folder: str | None = None + ) -> None: """ Imports a neural network model from a standalone PyTorch Module class. @@ -244,7 +301,15 @@ def importPythonModel(self, name:str='net', model_folder:str|None=None) -> None: self._model_def.updateParameters(self._model) @enforce_types - def exportONNX(self, inputs_order:list|None=None, outputs_order:list|None=None, name:str='net', model_folder:str|None=None, *, models:str|list|None=None) -> None: + def exportONNX( + self, + inputs_order: list | None = None, + outputs_order: list | None = None, + name: str = "net", + model_folder: str | None = None, + *, + models: str | list | None = None, + ) -> None: """ Exports the neural network model to an ONNX file. @@ -277,32 +342,52 @@ def exportONNX(self, inputs_order:list|None=None, outputs_order:list|None=None, .. include:: /examples_basics/export_module_ex/exportONNX.rst """ - check(self._model_def.isDefined(), RuntimeError, "The network has not been defined.") - check(self._traced == False, RuntimeError, - 'The model is traced and cannot be exported to ONNX.\n Run neuralizeModel() to recreate a standard model.') - check(self._neuralized == True, RuntimeError, 'The model is not neuralized yet.') + check( + self._model_def.isDefined(), + RuntimeError, + "The network has not been defined.", + ) + check( + self._traced == False, + RuntimeError, + "The model is traced and cannot be exported to ONNX.\n Run neuralizeModel() to recreate a standard model.", + ) + check( + self._neuralized == True, RuntimeError, "The model is not neuralized yet." + ) # From here -------------- if models is not None: if type(models) is str: models = [models] - if name == 'net': - name += '_' + '_'.join(models) + if name == "net": + name += "_" + "_".join(models) model_def = ModelDef(self._model_def.getJson(models)) - check(len(model_def.recurrentInputs().keys()) < len(model_def['Inputs'].keys()), TypeError, - "The network has only recurrent inputs.") - model_def.setBuildWindow(self._model_def['Info']['SampleTime']) + check( + len(model_def.recurrentInputs().keys()) + < len(model_def["Inputs"].keys()), + TypeError, + "The network has only recurrent inputs.", + ) + model_def.setBuildWindow(self._model_def["Info"]["SampleTime"]) model_def.updateParameters(self._model) model = Model(model_def.getJson()) else: model_def = self._model_def model = self._model model.update() - check(len(model_def.recurrentInputs().keys()) < len(model_def['Inputs'].keys()), TypeError, - "The network is autonomous because only recurrent variables are present.") - self.__exporter.exportONNX(model_def, model, inputs_order, outputs_order, name, model_folder) + check( + len(model_def.recurrentInputs().keys()) < len(model_def["Inputs"].keys()), + TypeError, + "The network is autonomous because only recurrent variables are present.", + ) + self.__exporter.exportONNX( + model_def, model, inputs_order, outputs_order, name, model_folder + ) @enforce_types - def onnxInference(self, inputs:dict, name:str='net', model_folder:str|None=None) -> dict: + def onnxInference( + self, inputs: dict, name: str = "net", model_folder: str | None = None + ) -> dict: """ Run an inference session using an onnx model previously exported using the nnodely framework. @@ -328,13 +413,13 @@ def onnxInference(self, inputs:dict, name:str='net', model_folder:str|None=None) Examples -------- - + .. include:: /examples_basics/export_module_ex/onnxInference.rst """ return self.__exporter.onnxInference(inputs, name, model_folder) @enforce_types - def exportReport(self, name:str='net', model_folder:str|None=None) -> None: + def exportReport(self, name: str = "net", model_folder: str | None = None) -> None: """ Generates a PDF report with plots containing the results of the training and validation of the neural network. diff --git a/nnodely/operators/loader.py b/nnodely/operators/loader.py index 8b2d0960..0cb80bc5 100644 --- a/nnodely/operators/loader.py +++ b/nnodely/operators/loader.py @@ -10,12 +10,18 @@ from nnodely.support.utils import check, enforce_types from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) + class Loader(Network): @enforce_types def __init__(self): - check(type(self) is not Loader, TypeError, "Loader class cannot be instantiated directly") + check( + type(self) is not Loader, + TypeError, + "Loader class cannot be instantiated directly", + ) super().__init__() # Dataaset Parameters @@ -23,7 +29,9 @@ def __init__(self): self.__datasets_loaded = set() @enforce_types - def getSamples(self, dataset:str, *, index:int|None = None, window:int=1) -> dict: + def getSamples( + self, dataset: str, *, index: int | None = None, window: int = 1 + ) -> dict: """ Retrieves a window of samples from a given dataset. @@ -53,19 +61,25 @@ def getSamples(self, dataset:str, *, index:int|None = None, window:int=1) -> dic """ if index is None: index = random.randint(0, self._num_of_samples[dataset] - window) - check(self._data_loaded, ValueError, 'The Dataset must first be loaded using function!') + check( + self._data_loaded, + ValueError, + "The Dataset must first be loaded using function!", + ) if self._data_loaded: result_dict = {} - for key in self._model_def['Inputs'].keys(): + for key in self._model_def["Inputs"].keys(): result_dict[key] = [] for idx in range(window): - for key ,samples in self._data[dataset].items(): - if key in self._model_def['Inputs'].keys(): - result_dict[key].append(samples[index+idx]) + for key, samples in self._data[dataset].items(): + if key in self._model_def["Inputs"].keys(): + result_dict[key].append(samples[index + idx]) return result_dict @enforce_types - def filterData(self, filter_function:Callable, dataset_name:str|None = None) -> None: + def filterData( + self, filter_function: Callable, dataset_name: str | None = None + ) -> None: """ Filters the data in the dataset using the provided filter function. @@ -97,7 +111,9 @@ def filterData(self, filter_function:Callable, dataset_name:str|None = None) -> idx_to_remove.append(idx) for key in self._data[name].keys(): - self._data[name][key] = np.delete(self._data[name][key], idx_to_remove, axis=0) + self._data[name][key] = np.delete( + self._data[name][key], idx_to_remove, axis=0 + ) self._num_of_samples[name] = self._data[name][key].shape[0] self.visualizer.showDataset(name=name) @@ -115,12 +131,16 @@ def filterData(self, filter_function:Callable, dataset_name:str|None = None) -> idx_to_remove.append(idx) for key in self._data[dataset_name].keys(): - self._data[dataset_name][key] = np.delete(self._data[dataset_name][key], idx_to_remove, axis=0) - self._num_of_samples[dataset_name] = self._data[dataset_name][key].shape[0] + self._data[dataset_name][key] = np.delete( + self._data[dataset_name][key], idx_to_remove, axis=0 + ) + self._num_of_samples[dataset_name] = self._data[dataset_name][ + key + ].shape[0] self.visualizer.showDataset(name=dataset_name) @enforce_types - def resamplingData(self, df:pd.DataFrame, *, scale:float = 1e9) -> None: + def resamplingData(self, df: pd.DataFrame, *, scale: float = 1e9) -> None: """ Resamples the DataFrame to a specified sample time. @@ -149,7 +169,7 @@ def resamplingData(self, df:pd.DataFrame, *, scale:float = 1e9) -> None: sample_time_ns = int(self._model_def.getSampleTime() * scale) if sample_time_ns <= 0: raise ValueError(f"Invalid resampling step: {sample_time_ns}ns") - method = 'linear' + method = "linear" if isinstance(df.index, pd.DatetimeIndex): # FORZA risoluzione ns (evita unit='s' interno) if df.index.dtype != "datetime64[ns]": @@ -177,12 +197,14 @@ def resamplingData(self, df:pd.DataFrame, *, scale:float = 1e9) -> None: df = df.resample(f"{sample_time_ns}ns").interpolate(method=method) else: - raise TypeError("No time column found in the DataFrame. Please provide a time column for resampling.") + raise TypeError( + "No time column found in the DataFrame. Please provide a time column for resampling." + ) return df - + @enforce_types def __get_format_idxs(self, format: list | None = None) -> dict: - model_inputs = self._model_def['Inputs'] + model_inputs = self._model_def["Inputs"] format_idx = {} idx = 0 for item in format: @@ -190,11 +212,17 @@ def __get_format_idxs(self, format: list | None = None) -> dict: n_cols = None for key in item: if key in model_inputs.keys(): - if n_cols is None or n_cols == model_inputs[key]['dim']: - n_cols = model_inputs[key]['dim'] + if n_cols is None or n_cols == model_inputs[key]["dim"]: + n_cols = model_inputs[key]["dim"] else: - raise ValueError(f'The variables {item} have different dimensionality.') - check(key not in format_idx, ValueError, f"The format '{format}' in not correct some variables appears more than once.") + raise ValueError( + f"The variables {item} have different dimensionality." + ) + check( + key not in format_idx, + ValueError, + f"The format '{format}' in not correct some variables appears more than once.", + ) format_idx[key] = (idx, idx + n_cols) if n_cols is not None: idx += n_cols @@ -204,15 +232,18 @@ def __get_format_idxs(self, format: list | None = None) -> dict: if item not in model_inputs.keys(): idx += 1 continue - n_cols = model_inputs[item]['dim'] - check(item not in format_idx, ValueError, - f"The format '{format}' in not correct some variables appears more than once.") + n_cols = model_inputs[item]["dim"] + check( + item not in format_idx, + ValueError, + f"The format '{format}' in not correct some variables appears more than once.", + ) format_idx[item] = (idx, idx + n_cols) idx += n_cols return format_idx - + @enforce_types - def __get_files(self, folder:str) -> list: + def __get_files(self, folder: str) -> list: try: _, _, files = next(os.walk(folder)) files.sort() @@ -220,14 +251,14 @@ def __get_files(self, folder:str) -> list: check(False, StopIteration, f'ERROR: The path "{folder}" does not exist!') return [] return files - + @enforce_types def __stack_arrays(self, data: dict) -> tuple: ## Convert lists to numpy arrays num_of_samples = {} for key in data: data[key] = np.stack(data[key]) - if self._model_def['Inputs'][key]['dim'] > 1: + if self._model_def["Inputs"][key]["dim"] > 1: data[key] = np.array(data[key].tolist(), dtype=np.float64) if data[key].ndim == 2: ## Add the sample dimension data[key] = np.expand_dims(data[key], axis=-1) @@ -237,14 +268,17 @@ def __stack_arrays(self, data: dict) -> tuple: return num_of_samples @enforce_types - def loadData(self, name:str, - source: str | dict | pd.DataFrame, *, - format: list | None = None, - skiplines: int = 0, - delimiter: str = ',', - header: int | str | Sequence | None = None, - resampling: bool = False - ) -> None: + def loadData( + self, + name: str, + source: str | dict | pd.DataFrame, + *, + format: list | None = None, + skiplines: int = 0, + delimiter: str = ",", + header: int | str | Sequence | None = None, + resampling: bool = False, + ) -> None: """ Loads data into the model. The data can be loaded from a directory path containing the csv files or from a crafted dataset. @@ -271,13 +305,15 @@ def loadData(self, name:str, Examples -------- - + .. include:: /examples_basics/data_loader_module_ex/loadData.rst """ check(self.neuralized, ValueError, "The network is not neuralized.") - check(delimiter in ['\t', '\n', ';', ',', ' '], ValueError, 'delimiter not valid!') + check( + delimiter in ["\t", "\n", ";", ",", " "], ValueError, "delimiter not valid!" + ) - json_inputs = self._model_def['Inputs'] + json_inputs = self._model_def["Inputs"] ## Initialize the dictionary containing the data check_names(name, self._data.keys(), f"Dataset") @@ -299,23 +335,46 @@ def loadData(self, name:str, for file in files: try: ## read the csv - df = pd.read_csv(os.path.join(source, file), skiprows=skiplines, delimiter=delimiter, header=header) + df = pd.read_csv( + os.path.join(source, file), + skiprows=skiplines, + delimiter=delimiter, + header=header, + ) if not all(df.iloc[0].apply(lambda x: isinstance(x, (int, float)))): - log.warning(f"The file {file} does not contain a numerical column.") + log.warning( + f"The file {file} does not contain a numerical column." + ) ## Resampling if the time column is provided (must be a Datetime object) if resampling: self.resamplingData(df) except: - log.warning(f'Cannot read file {os.path.join(source, file)}') + log.warning(f"Cannot read file {os.path.join(source, file)}") continue if self._file_count > 1: - self._multifile[name].append((self._multifile[name][-1] + (len(df) - self._max_n_samples + 1)) if self._multifile[name] else len(df) - self._max_n_samples + 1) + self._multifile[name].append( + ( + self._multifile[name][-1] + + (len(df) - self._max_n_samples + 1) + ) + if self._multifile[name] + else len(df) - self._max_n_samples + 1 + ) ## Cycle through all the windows for key, idxs in format_idx.items(): - back, forw = self._input_ns_backward[key], self._input_ns_forward[key] + back, forw = ( + self._input_ns_backward[key], + self._input_ns_forward[key], + ) ## Save as numpy array the data - data = df.iloc[:, idxs[0]:idxs[1]].to_numpy() - self._data[name][key] += [data[i - back:i + forw] for i in range(self._max_samples_backward, len(df) - self._max_samples_forward + 1)] + data = df.iloc[:, idxs[0] : idxs[1]].to_numpy() + self._data[name][key] += [ + data[i - back : i + forw] + for i in range( + self._max_samples_backward, + len(df) - self._max_samples_forward + 1, + ) + ] else: ## we have a crafted dataset ## add the dataset self._data[name] = {} @@ -326,9 +385,17 @@ def loadData(self, name:str, if key not in source.keys(): continue self._data[name][key] = [] ## Initialize the dataset - back, forw = self._input_ns_backward[key], self._input_ns_forward[key] + back, forw = ( + self._input_ns_backward[key], + self._input_ns_forward[key], + ) for idx in range(len(source[key]) - self._max_n_samples + 1): - self._data[name][key].append(source[key][idx + (self._max_samples_backward - back):idx + (self._max_samples_backward + forw)]) + self._data[name][key].append( + source[key][ + idx + (self._max_samples_backward - back) : idx + + (self._max_samples_backward + forw) + ] + ) else: if resampling: source = self.resamplingData(source) @@ -336,15 +403,25 @@ def loadData(self, name:str, if key not in source.columns: continue self._data[name][key] = [] ## Initialize the dataset - back, forw = self._input_ns_backward[key], self._input_ns_forward[key] + back, forw = ( + self._input_ns_backward[key], + self._input_ns_forward[key], + ) for idx in range(len(source) - self._max_n_samples + 1): - window = source[key].iloc[idx + (self._max_samples_backward - back):idx + (self._max_samples_backward + forw)] + window = source[key].iloc[ + idx + (self._max_samples_backward - back) : idx + + (self._max_samples_backward + forw) + ] self._data[name][key].append(window.to_numpy()) ## Convert lists to numpy arrays num_of_samples = self.__stack_arrays(self._data[name]) # Check dim of the samples - check(len(set(num_of_samples.values())) == 1, ValueError, f"The number of the sample of the dataset {name} are not the same for all input in the dataset: {num_of_samples}") + check( + len(set(num_of_samples.values())) == 1, + ValueError, + f"The number of the sample of the dataset {name} are not the same for all input in the dataset: {num_of_samples}", + ) self._num_of_samples[name] = num_of_samples[list(num_of_samples.keys())[0]] ## Set the Loaded flag to True self._data_loaded = True @@ -352,4 +429,4 @@ def loadData(self, name:str, self.__n_datasets = len(self._data.keys()) self.__datasets_loaded.add(name) ## Show the dataset - self.visualizer.showDataset(name=name) \ No newline at end of file + self.visualizer.showDataset(name=name) diff --git a/nnodely/operators/network.py b/nnodely/operators/network.py index 2d60497c..5c0acc52 100644 --- a/nnodely/operators/network.py +++ b/nnodely/operators/network.py @@ -2,18 +2,30 @@ from collections import defaultdict import numpy as np -import torch, random - -from nnodely.support.utils import TORCH_DTYPE, NP_DTYPE, check, enforce_types, tensor_to_list +import torch, random + +from nnodely.support.utils import ( + TORCH_DTYPE, + NP_DTYPE, + check, + enforce_types, + tensor_to_list, +) from nnodely.basic.modeldef import ModelDef from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) + class Network: @enforce_types def __init__(self): - check(type(self) is not Network, TypeError, "Loader class cannot be instantiated directly") + check( + type(self) is not Network, + TypeError, + "Loader class cannot be instantiated directly", + ) # Models definition self._model_def = ModelDef() @@ -39,20 +51,31 @@ def __init__(self): # Training information self._standard_train_parameters = { - 'models': None, - 'train_dataset': None, 'validation_dataset': None, - 'dataset': None, 'splits': [100, 0, 0], - 'closed_loop': {}, 'connect': {}, 'step': 0, 'prediction_samples': 0, - 'shuffle_data': True, - 'early_stopping': None, 'early_stopping_params': {}, - 'select_model': 'last', 'select_model_params': {}, - 'minimize_gain': {}, - 'num_of_epochs': 100, - 'train_batch_size': 128, 'val_batch_size': 128, - 'optimizer': 'Adam', - 'lr': 0.001, 'lr_param': {}, - 'optimizer_params': [], 'add_optimizer_params': [], - 'optimizer_defaults': {}, 'add_optimizer_defaults': {} + "models": None, + "train_dataset": None, + "validation_dataset": None, + "dataset": None, + "splits": [100, 0, 0], + "closed_loop": {}, + "connect": {}, + "step": 0, + "prediction_samples": 0, + "shuffle_data": True, + "early_stopping": None, + "early_stopping_params": {}, + "select_model": "last", + "select_model_params": {}, + "minimize_gain": {}, + "num_of_epochs": 100, + "train_batch_size": 128, + "val_batch_size": 128, + "optimizer": "Adam", + "lr": 0.001, + "lr_param": {}, + "optimizer_params": [], + "add_optimizer_params": [], + "optimizer_defaults": {}, + "add_optimizer_defaults": {}, } self._training = {} @@ -63,7 +86,7 @@ def __init__(self): def _save_internal(self, key, value): self._internals[key] = tensor_to_list(value) - def _set_log_internal(self, log_internal:bool): + def _set_log_internal(self, log_internal: bool): self._log_internal = log_internal def _clean_log_internal(self): @@ -71,34 +94,47 @@ def _clean_log_internal(self): def _remove_virtual_states(self, connect, closed_loop): if connect or closed_loop: - for key in (connect.keys() | closed_loop.keys()): + for key in connect.keys() | closed_loop.keys(): if key in self._states.keys(): del self._states[key] def _update_state(self, X, out_closed_loop, out_connect): for key, value in out_connect.items(): X[key] = torch.roll(value, shifts=-1, dims=1) ## Roll the time window - X[key][:, -1, :] = float('inf') ## inf value to make clear that the last state value + X[key][:, -1, :] = float( + "inf" + ) ## inf value to make clear that the last state value self._states[key] = X[key].clone().detach() for key, val in out_closed_loop.items(): - shift = val.shape[1] #+ self._input_ns_forward[key] ## take the output time dimension + forward samples + shift = val.shape[ + 1 + ] # + self._input_ns_forward[key] ## take the output time dimension + forward samples X[key] = torch.roll(X[key], shifts=-1, dims=1) ## Roll the time window X[key][:, -shift:, :] = val ## substitute with the predicted value self._states[key] = X[key].clone().detach() def _get_gradient_on_inference(self): - for key, value in self._model_def['Inputs'].items(): - if 'type' in value.keys(): + for key, value in self._model_def["Inputs"].items(): + if "type" in value.keys(): return True return False def _get_mandatory_inputs(self, connect, closed_loop): - model_inputs = list(self._model_def['Inputs'].keys()) - non_mandatory_inputs = list(closed_loop.keys()) + list(connect.keys()) + list(self._model_def.recurrentInputs().keys()) + model_inputs = list(self._model_def["Inputs"].keys()) + non_mandatory_inputs = ( + list(closed_loop.keys()) + + list(connect.keys()) + + list(self._model_def.recurrentInputs().keys()) + ) mandatory_inputs = list(set(model_inputs) - set(non_mandatory_inputs)) return mandatory_inputs, non_mandatory_inputs - - def _get_batch_indexes(self, datasets:str|list|dict|None, n_samples:int=0, prediction_samples:int=0): + + def _get_batch_indexes( + self, + datasets: str | list | dict | None, + n_samples: int = 0, + prediction_samples: int = 0, + ): if datasets is None: return [] batch_indexes = list(range(n_samples)) @@ -107,16 +143,22 @@ def _get_batch_indexes(self, datasets:str|list|dict|None, n_samples:int=0, predi forbidden_idxs = [] n_samples_count = 0 for dataset in datasets: - if dataset in self._multifile.keys(): ## i have some forbidden indexes + if dataset in self._multifile.keys(): ## i have some forbidden indexes for i in self._multifile[dataset]: - if i+n_samples_count < batch_indexes[-1]: - forbidden_idxs.extend(range((i+n_samples_count) - prediction_samples, (i+n_samples_count), 1)) + if i + n_samples_count < batch_indexes[-1]: + forbidden_idxs.extend( + range( + (i + n_samples_count) - prediction_samples, + (i + n_samples_count), + 1, + ) + ) n_samples_count += self._num_of_samples[dataset] batch_indexes = [idx for idx in batch_indexes if idx not in forbidden_idxs] batch_indexes = batch_indexes[:-prediction_samples] return batch_indexes - - def _get_data(self, dataset:str|list|dict|None): + + def _get_data(self, dataset: str | list | dict | None): if dataset is None: return {} if isinstance(dataset, dict): @@ -124,78 +166,162 @@ def _get_data(self, dataset:str|list|dict|None): return dataset dataset = [dataset] if type(dataset) is str else dataset loaded_datasets = list(self._data.keys()) - check(len([data for data in dataset if data in loaded_datasets]) > 0, KeyError, f'the datasets: {dataset} are not loaded!') + check( + len([data for data in dataset if data in loaded_datasets]) > 0, + KeyError, + f"the datasets: {dataset} are not loaded!", + ) total_data = defaultdict(list) for data in dataset: if data not in loaded_datasets: - log.warning(f'{data} is not loaded. Ignoring this dataset...') + log.warning(f"{data} is not loaded. Ignoring this dataset...") dataset.remove(data) continue for k, v in self._data[data].items(): total_data[k].append(v) total_data = {key: np.concatenate(arrays) for key, arrays in total_data.items()} - total_data = {key: torch.from_numpy(val).to(TORCH_DTYPE) for key, val in total_data.items()} + total_data = { + key: torch.from_numpy(val).to(TORCH_DTYPE) + for key, val in total_data.items() + } return total_data def _clip_step(self, step, batch_indexes, batch_size): clipped_step = copy.deepcopy(step) if clipped_step < 0: ## clip the step to zero - log.warning(f"The step is negative ({clipped_step}). The step is set to zero.", stacklevel=5) + log.warning( + f"The step is negative ({clipped_step}). The step is set to zero.", + stacklevel=5, + ) clipped_step = 0 - if clipped_step > (len(batch_indexes) - batch_size): ## Clip the step to the maximum number of samples - log.warning(f"The step ({clipped_step}) is greater than the number of available samples ({len(batch_indexes) - batch_size}). The step is set to the maximum number.", stacklevel=5) + if clipped_step > ( + len(batch_indexes) - batch_size + ): ## Clip the step to the maximum number of samples + log.warning( + f"The step ({clipped_step}) is greater than the number of available samples ({len(batch_indexes) - batch_size}). The step is set to the maximum number.", + stacklevel=5, + ) clipped_step = len(batch_indexes) - batch_size - check((batch_size + clipped_step) > 0, ValueError, f"The sum of batch_size={batch_size} and the step={clipped_step} must be greater than 0.") + check( + (batch_size + clipped_step) > 0, + ValueError, + f"The sum of batch_size={batch_size} and the step={clipped_step} must be greater than 0.", + ) return clipped_step def _clip_batch_size(self, n_samples, batch_size=None): batch_size = batch_size if batch_size <= n_samples else max(0, n_samples) - check((n_samples - batch_size + 1) > 0, ValueError, f"The number of available sample are {n_samples - batch_size + 1}") - check(batch_size > 0, ValueError, f'The batch_size must be greater than 0.') + check( + (n_samples - batch_size + 1) > 0, + ValueError, + f"The number of available sample are {n_samples - batch_size + 1}", + ) + check(batch_size > 0, ValueError, f"The batch_size must be greater than 0.") return batch_size - - def __split_dataset(self, dataset:str|list|dict, splits:list): - check(len(splits) == 3, ValueError, '3 elements must be inserted for the dataset split in training, validation and test') - check(sum(splits) == 100, ValueError, 'Training, Validation and Test splits must sum up to 100.') - check(splits[0] > 0, ValueError, 'The training split cannot be zero.') - train_size, val_size, test_size = splits[0] / 100, splits[1] / 100, splits[2] / 100 + + def __split_dataset(self, dataset: str | list | dict, splits: list): + check( + len(splits) == 3, + ValueError, + "3 elements must be inserted for the dataset split in training, validation and test", + ) + check( + sum(splits) == 100, + ValueError, + "Training, Validation and Test splits must sum up to 100.", + ) + check(splits[0] > 0, ValueError, "The training split cannot be zero.") + train_size, val_size, test_size = ( + splits[0] / 100, + splits[1] / 100, + splits[2] / 100, + ) XY_train, XY_val, XY_test = {}, {}, {} if isinstance(dataset, dict): self.__check_data_integrity(dataset) num_of_samples = next(iter(dataset.values())).size(0) - XY_train = {key: value[:round(num_of_samples*train_size), :, :] for key, value in dataset.items()} - XY_val = {key: value[round(num_of_samples*train_size):round(num_of_samples*(train_size + val_size)), :, :] for key, value in dataset.items()} - XY_test = {key: value[round(num_of_samples*(train_size + val_size)):, :, :] for key, value in dataset.items()} + XY_train = { + key: value[: round(num_of_samples * train_size), :, :] + for key, value in dataset.items() + } + XY_val = { + key: value[ + round(num_of_samples * train_size) : round( + num_of_samples * (train_size + val_size) + ), + :, + :, + ] + for key, value in dataset.items() + } + XY_test = { + key: value[round(num_of_samples * (train_size + val_size)) :, :, :] + for key, value in dataset.items() + } else: dataset = [dataset] if type(dataset) is str else dataset - check(len([data for data in dataset if data in self._data.keys()]) > 0, KeyError, f'the datasets: {dataset} are not loaded!') + check( + len([data for data in dataset if data in self._data.keys()]) > 0, + KeyError, + f"the datasets: {dataset} are not loaded!", + ) for data in dataset: if data not in self._data.keys(): - log.warning(f'{data} is not loaded. The training will continue without this dataset.') + log.warning( + f"{data} is not loaded. The training will continue without this dataset." + ) dataset.remove(data) num_of_samples = sum([self._num_of_samples[data] for data in dataset]) - n_samples_train, n_samples_val = round(num_of_samples * train_size), round(num_of_samples * val_size) + n_samples_train, n_samples_val = ( + round(num_of_samples * train_size), + round(num_of_samples * val_size), + ) n_samples_test = num_of_samples - n_samples_train - n_samples_val - check(n_samples_train > 0, ValueError, f'The number of train samples {n_samples_train} must be greater than 0.') + check( + n_samples_train > 0, + ValueError, + f"The number of train samples {n_samples_train} must be greater than 0.", + ) total_data = defaultdict(list) for data in dataset: for k, v in self._data[data].items(): total_data[k].append(v) - total_data = {key: np.concatenate(arrays, dtype=NP_DTYPE) for key, arrays in total_data.items()} + total_data = { + key: np.concatenate(arrays, dtype=NP_DTYPE) + for key, arrays in total_data.items() + } for key, samples in total_data.items(): if val_size == 0.0 and test_size == 0.0: ## we have only training set XY_train[key] = torch.from_numpy(samples).to(TORCH_DTYPE) - elif val_size == 0.0 and test_size != 0.0: ## we have only training and test set - XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE) - XY_test[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE) - elif val_size != 0.0 and test_size == 0.0: ## we have only training and validation set - XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE) - XY_val[key] = torch.from_numpy(samples[n_samples_train:]).to(TORCH_DTYPE) + elif ( + val_size == 0.0 and test_size != 0.0 + ): ## we have only training and test set + XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to( + TORCH_DTYPE + ) + XY_test[key] = torch.from_numpy(samples[n_samples_train:]).to( + TORCH_DTYPE + ) + elif ( + val_size != 0.0 and test_size == 0.0 + ): ## we have only training and validation set + XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to( + TORCH_DTYPE + ) + XY_val[key] = torch.from_numpy(samples[n_samples_train:]).to( + TORCH_DTYPE + ) else: ## we have training, validation and test set - XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to(TORCH_DTYPE) - XY_val[key] = torch.from_numpy(samples[n_samples_train:-n_samples_test]).to(TORCH_DTYPE) - XY_test[key] = torch.from_numpy(samples[n_samples_train + n_samples_val:]).to(TORCH_DTYPE) + XY_train[key] = torch.from_numpy(samples[:n_samples_train]).to( + TORCH_DTYPE + ) + XY_val[key] = torch.from_numpy( + samples[n_samples_train:-n_samples_test] + ).to(TORCH_DTYPE) + XY_test[key] = torch.from_numpy( + samples[n_samples_train + n_samples_val :] + ).to(TORCH_DTYPE) return XY_train, XY_val, XY_test def _get_tag(self, dataset: str | list | dict | None) -> str: @@ -205,79 +331,153 @@ def _get_tag(self, dataset: str | list | dict | None) -> str: if isinstance(dataset, str): return dataset elif isinstance(dataset, list): - return f"{dataset[0]}_{len(dataset)}" if len(dataset) > 1 else f"{dataset[0]}" + return ( + f"{dataset[0]}_{len(dataset)}" if len(dataset) > 1 else f"{dataset[0]}" + ) elif isinstance(dataset, dict): return "custom_dataset" return dataset - def _setup_dataset(self, train_dataset:str|list|dict, validation_dataset:str|list|dict, test_dataset:str|list|dict, dataset:str|list|dict, splits:list): - if train_dataset is None: ## use the splits + def _setup_dataset( + self, + train_dataset: str | list | dict, + validation_dataset: str | list | dict, + test_dataset: str | list | dict, + dataset: str | list | dict, + splits: list, + ): + if train_dataset is None: ## use the splits train_dataset = list(self._data.keys()) if dataset is None else dataset return self.__split_dataset(train_dataset, splits) - else: ## use each dataset - return self._get_data(train_dataset), self._get_data(validation_dataset), self._get_data(test_dataset) - - def __check_data_integrity(self, dataset:dict): + else: ## use each dataset + return ( + self._get_data(train_dataset), + self._get_data(validation_dataset), + self._get_data(test_dataset), + ) + + def __check_data_integrity(self, dataset: dict): if bool(dataset): - check(len(set([t.size(0) for t in dataset.values()])) == 1, ValueError, "All the tensors in the dataset must have the same number of samples.") - #TODO check why is wrong - #check(len([t for t in self._model_def['Inputs'].keys() if t in dataset.keys()]) == len(list(self._model_def['Inputs'].keys())), ValueError, "Some inputs are missing.") + check( + len(set([t.size(0) for t in dataset.values()])) == 1, + ValueError, + "All the tensors in the dataset must have the same number of samples.", + ) + # TODO check why is wrong + # check(len([t for t in self._model_def['Inputs'].keys() if t in dataset.keys()]) == len(list(self._model_def['Inputs'].keys())), ValueError, "Some inputs are missing.") for key, value in dataset.items(): - if key not in self._model_def['Inputs']: - log.warning(f"The key '{key}' is not an input of the network. It will be ignored.") + if key not in self._model_def["Inputs"]: + log.warning( + f"The key '{key}' is not an input of the network. It will be ignored." + ) else: - check(isinstance(value, torch.Tensor), TypeError, f"The value of the input '{key}' must be a torch.Tensor.") - check(value.size(1) == self._model_def['Inputs'][key]['ntot'], ValueError, f"The time size of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['ntot']}, got {value.size(1)}.") - check(value.size(2) == self._model_def['Inputs'][key]['dim'], ValueError, f"The dimension of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['dim']}, got {value.size(2)}.") - - def _get_not_mandatory_inputs(self, data, X, non_mandatory_inputs, remaning_indexes, batch_size, step, shuffle = False): - related_indexes = random.sample(remaning_indexes, batch_size) if shuffle else remaning_indexes[:batch_size] + check( + isinstance(value, torch.Tensor), + TypeError, + f"The value of the input '{key}' must be a torch.Tensor.", + ) + check( + value.size(1) == self._model_def["Inputs"][key]["ntot"], + ValueError, + f"The time size of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['ntot']}, got {value.size(1)}.", + ) + check( + value.size(2) == self._model_def["Inputs"][key]["dim"], + ValueError, + f"The dimension of the input '{key}' is not correct. Expected {self._model_def['Inputs'][key]['dim']}, got {value.size(2)}.", + ) + + def _get_not_mandatory_inputs( + self, + data, + X, + non_mandatory_inputs, + remaning_indexes, + batch_size, + step, + shuffle=False, + ): + related_indexes = ( + random.sample(remaning_indexes, batch_size) + if shuffle + else remaning_indexes[:batch_size] + ) for num in related_indexes: remaning_indexes.remove(num) if step > 0: if len(remaning_indexes) >= step: - step_idxs = random.sample(remaning_indexes, step) if shuffle else remaning_indexes[:step] + step_idxs = ( + random.sample(remaning_indexes, step) + if shuffle + else remaning_indexes[:step] + ) for num in step_idxs: remaning_indexes.remove(num) else: remaning_indexes.clear() for key in non_mandatory_inputs: - if key in data.keys(): ## with data + if key in data.keys(): ## with data X[key] = data[key][related_indexes] else: ## with zeros window_size = self._input_n_samples[key] - dim = self._model_def['Inputs'][key]['dim'] - if 'type' in self._model_def['Inputs'][key]: - X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=True) + dim = self._model_def["Inputs"][key]["dim"] + if "type" in self._model_def["Inputs"][key]: + X[key] = torch.zeros( + size=(batch_size, window_size, dim), + dtype=TORCH_DTYPE, + requires_grad=True, + ) else: - X[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False) + X[key] = torch.zeros( + size=(batch_size, window_size, dim), + dtype=TORCH_DTYPE, + requires_grad=False, + ) self._states[key] = X[key] return related_indexes - def _inference(self, data, n_samples, batch_size, loss_gains, loss_functions, - shuffle = False, optimizer = None, - total_losses = None, A = None, B = None): + def _inference( + self, + data, + n_samples, + batch_size, + loss_gains, + loss_functions, + shuffle=False, + optimizer=None, + total_losses=None, + A=None, + B=None, + ): if shuffle: randomize = torch.randperm(n_samples) data = {key: val[randomize] for key, val in data.items()} ## Initialize the train losses vector - aux_losses = torch.zeros([len(self._model_def['Minimizers']), n_samples // batch_size]) + aux_losses = torch.zeros( + [len(self._model_def["Minimizers"]), n_samples // batch_size] + ) for idx in range(0, (n_samples - batch_size + 1), batch_size): ## Build the input tensor XY = {} for key, val in data.items(): - if self._model_def['Inputs'].get(key, None): - if self._model_def['Inputs'][key].get('type', None) == 'derivate': - XY[key] = val[idx:idx + batch_size].detach().requires_grad_(True) + if self._model_def["Inputs"].get(key, None): + if self._model_def["Inputs"][key].get("type", None) == "derivate": + XY[key] = ( + val[idx : idx + batch_size].detach().requires_grad_(True) + ) else: - XY[key] = val[idx:idx + batch_size] - #XY = {key: val[idx:idx + batch_size].detach().requires_grad_(True) for key, val in data.items()} + XY[key] = val[idx : idx + batch_size] + # XY = {key: val[idx:idx + batch_size].detach().requires_grad_(True) for key, val in data.items()} for key in self._model_def.recurrentInputs().keys(): if key not in XY.keys(): window_size = self._input_n_samples[key] - dim = self._model_def['Inputs'][key]['dim'] - XY[key] = torch.zeros(size=(batch_size, window_size, dim), dtype=TORCH_DTYPE, requires_grad=True) + dim = self._model_def["Inputs"][key]["dim"] + XY[key] = torch.zeros( + size=(batch_size, window_size, dim), + dtype=TORCH_DTYPE, + requires_grad=True, + ) ## Reset gradient if optimizer: optimizer.zero_grad() @@ -285,16 +485,22 @@ def _inference(self, data, n_samples, batch_size, loss_gains, loss_functions, out, minimize_out, _, _ = self._model(XY) ## Forward pass if self._log_internal: - internals_dict = {'XY': tensor_to_list(XY), 'out': out, 'param': self._model.all_parameters} + internals_dict = { + "XY": tensor_to_list(XY), + "out": out, + "param": self._model.all_parameters, + } ## Loss Calculation total_loss = 0 - for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()): + for ind, (key, value) in enumerate(self._model_def["Minimizers"].items()): if A is not None: - A[key].append(minimize_out[value['A']].detach().numpy()) + A[key].append(minimize_out[value["A"]].detach().numpy()) if B is not None: - B[key].append(minimize_out[value['B']].detach().numpy()) - loss = loss_functions[key](minimize_out[value['A']], minimize_out[value['B']]) + B[key].append(minimize_out[value["B"]].detach().numpy()) + loss = loss_functions[key]( + minimize_out[value["A"]], minimize_out[value["B"]] + ) loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss if total_losses is not None: total_losses[key].append(loss.detach().numpy()) @@ -302,7 +508,7 @@ def _inference(self, data, n_samples, batch_size, loss_gains, loss_functions, total_loss += loss if self._log_internal: - self._save_internal('inout_' + str(idx), internals_dict) + self._save_internal("inout_" + str(idx), internals_dict) ## Gradient step if optimizer: @@ -313,24 +519,49 @@ def _inference(self, data, n_samples, batch_size, loss_gains, loss_functions, ## return the losses return aux_losses - def _recurrent_inference(self, data, batch_indexes, batch_size, loss_gains, prediction_samples, - step, non_mandatory_inputs, mandatory_inputs, loss_functions, - shuffle = False, optimizer = None, - total_losses = None, A = None, B = None, idxs = None): + def _recurrent_inference( + self, + data, + batch_indexes, + batch_size, + loss_gains, + prediction_samples, + step, + non_mandatory_inputs, + mandatory_inputs, + loss_functions, + shuffle=False, + optimizer=None, + total_losses=None, + A=None, + B=None, + idxs=None, + ): indexes = copy.deepcopy(batch_indexes) - aux_losses = torch.zeros([len(self._model_def['Minimizers']), round((len(indexes) + step) / (batch_size + step))]) + aux_losses = torch.zeros( + [ + len(self._model_def["Minimizers"]), + round((len(indexes) + step) / (batch_size + step)), + ] + ) X = {} batch_idx = 0 while len(indexes) >= batch_size: - selected_indexes = self._get_not_mandatory_inputs(data, X, non_mandatory_inputs, indexes, batch_size, step, shuffle) - horizon_losses = {ind: [] for ind in range(len(self._model_def['Minimizers']))} + selected_indexes = self._get_not_mandatory_inputs( + data, X, non_mandatory_inputs, indexes, batch_size, step, shuffle + ) + horizon_losses = { + ind: [] for ind in range(len(self._model_def["Minimizers"])) + } if optimizer: optimizer.zero_grad() ## Reset the gradient for horizon_idx in range(prediction_samples + 1): # Save the indexes if idxs is not None: - idxs[horizon_idx].append([idx + horizon_idx for idx in selected_indexes]) + idxs[horizon_idx].append( + [idx + horizon_idx for idx in selected_indexes] + ) ## Get data for key in mandatory_inputs: X[key] = data[key][[idx + horizon_idx for idx in selected_indexes]] @@ -338,29 +569,47 @@ def _recurrent_inference(self, data, batch_indexes, batch_size, loss_gains, pred out, minimize_out, out_closed_loop, out_connect = self._model(X) if self._log_internal: - internals_dict = {'XY': tensor_to_list(X), 'out': out, 'param': self._model.all_parameters, - 'closedLoop': self._model.closed_loop_update, 'connect': self._model.connect_update} + internals_dict = { + "XY": tensor_to_list(X), + "out": out, + "param": self._model.all_parameters, + "closedLoop": self._model.closed_loop_update, + "connect": self._model.connect_update, + } ## Loss Calculation - for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()): + for ind, (key, value) in enumerate( + self._model_def["Minimizers"].items() + ): if A is not None: - A[key][horizon_idx].append(minimize_out[value['A']].detach().numpy()) + A[key][horizon_idx].append( + minimize_out[value["A"]].detach().numpy() + ) if B is not None: - B[key][horizon_idx].append(minimize_out[value['B']].detach().numpy()) - loss = loss_functions[key](minimize_out[value['A']], minimize_out[value['B']]) - loss = (loss * loss_gains[key]) if key in loss_gains.keys() else loss + B[key][horizon_idx].append( + minimize_out[value["B"]].detach().numpy() + ) + loss = loss_functions[key]( + minimize_out[value["A"]], minimize_out[value["B"]] + ) + loss = ( + (loss * loss_gains[key]) if key in loss_gains.keys() else loss + ) horizon_losses[ind].append(loss) ## Update self._update_state(X, out_closed_loop, out_connect) if self._log_internal: - internals_dict['state'] = self._states - self._save_internal('inout_' + str(batch_idx) + '_' + str(horizon_idx), internals_dict) + internals_dict["state"] = self._states + self._save_internal( + "inout_" + str(batch_idx) + "_" + str(horizon_idx), + internals_dict, + ) ## Calculate the total loss total_loss = 0 - for ind, key in enumerate(self._model_def['Minimizers'].keys()): + for ind, key in enumerate(self._model_def["Minimizers"].keys()): loss = sum(horizon_losses[ind]) / (prediction_samples + 1) aux_losses[ind][batch_idx] = loss.item() if total_losses is not None: @@ -379,30 +628,58 @@ def _recurrent_inference(self, data, batch_indexes, batch_size, loss_gains, pred def _setup_recurrent_variables(self, prediction_samples, closed_loop, connect): ## Prediction samples - check(prediction_samples == 'auto' or prediction_samples >= -1, KeyError, "The sample horizon must be positive, -1, 'auto', for disconnect connection!") + check( + prediction_samples == "auto" or prediction_samples >= -1, + KeyError, + "The sample horizon must be positive, -1, 'auto', for disconnect connection!", + ) ## Close loop information for input, output in closed_loop.items(): - check(input in self._model_def['Inputs'], ValueError, f'the tag {input} is not an input variable.') - check(output in self._model_def['Outputs'], ValueError, f'the tag {output} is not an output of the network') - log.info(f'Recurrent train: closing the loop between the the input ports {input} and the output ports {output} for {prediction_samples} samples') + check( + input in self._model_def["Inputs"], + ValueError, + f"the tag {input} is not an input variable.", + ) + check( + output in self._model_def["Outputs"], + ValueError, + f"the tag {output} is not an output of the network", + ) + log.info( + f"Recurrent train: closing the loop between the the input ports {input} and the output ports {output} for {prediction_samples} samples" + ) if self._input_ns_forward[input] > 0: - log.warning(f"Closed loop on variable '{input}' with sample in the future.") + log.warning( + f"Closed loop on variable '{input}' with sample in the future." + ) ## Connect information for input, output in connect.items(): - check(input in self._model_def['Inputs'], ValueError, f'the tag {input} is not an input variable.') - check(output in self._model_def['Outputs'], ValueError, f'the tag {output} is not an output of the network') - log.info(f'Recurrent train: connecting the input ports {input} with output ports {output} for {prediction_samples} samples') + check( + input in self._model_def["Inputs"], + ValueError, + f"the tag {input} is not an input variable.", + ) + check( + output in self._model_def["Outputs"], + ValueError, + f"the tag {output} is not an output of the network", + ) + log.info( + f"Recurrent train: connecting the input ports {input} with output ports {output} for {prediction_samples} samples" + ) if self._input_ns_forward[input] > 0: - log.warning(f"Connect on variable '{input}' with sample in the future.") + log.warning(f"Connect on variable '{input}' with sample in the future.") ## Disable recurrent training if there are no recurrent variables - if len(connect|closed_loop|self._model_def.recurrentInputs()) == 0: + if len(connect | closed_loop | self._model_def.recurrentInputs()) == 0: if type(prediction_samples) is not str and prediction_samples >= 0: - log.warning(f"The value of the prediction_samples={prediction_samples} but the network has no recurrent variables.") + log.warning( + f"The value of the prediction_samples={prediction_samples} but the network has no recurrent variables." + ) prediction_samples = -1 return prediction_samples @enforce_types - def resetStates(self, states:set={}, *, batch:int=1) -> None: + def resetStates(self, states: set = {}, *, batch: int = 1) -> None: """ Resets the state of all the recurrent inputs of the network to zero. Parameters @@ -412,15 +689,22 @@ def resetStates(self, states:set={}, *, batch:int=1) -> None: batch : int, optional The batch size for the reset states. Default is 1. """ - if states: ## reset only specific states + if states: ## reset only specific states for key in states: window_size = self._input_n_samples[key] - dim = self._model_def['Inputs'][key]['dim'] - self._states[key] = torch.zeros(size=(batch, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False) - else: ## reset all states + dim = self._model_def["Inputs"][key]["dim"] + self._states[key] = torch.zeros( + size=(batch, window_size, dim), + dtype=TORCH_DTYPE, + requires_grad=False, + ) + else: ## reset all states self._states = {} for key, state in self._model_def.recurrentInputs().items(): window_size = self._input_n_samples[key] - dim = state['dim'] - self._states[key] = torch.zeros(size=(batch, window_size, dim), dtype=TORCH_DTYPE, requires_grad=False) - + dim = state["dim"] + self._states[key] = torch.zeros( + size=(batch, window_size, dim), + dtype=TORCH_DTYPE, + requires_grad=False, + ) diff --git a/nnodely/operators/trainer.py b/nnodely/operators/trainer.py index 2fec15e2..deecc930 100644 --- a/nnodely/operators/trainer.py +++ b/nnodely/operators/trainer.py @@ -13,12 +13,17 @@ from nnodely.layers.output import Output from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.INFO) class Trainer(Network): def __init__(self): - check(type(self) is not Trainer, TypeError, "Trainer class cannot be instantiated directly") + check( + type(self) is not Trainer, + TypeError, + "Trainer class cannot be instantiated directly", + ) super().__init__() ## User Parameters @@ -31,7 +36,13 @@ def __init__(self): self.__optimizer = None @enforce_types - def addMinimize(self, name:str, streamA:str|Stream|Output, streamB:str|Stream|Output, loss_function:str='mse') -> None: + def addMinimize( + self, + name: str, + streamA: str | Stream | Output, + streamB: str | Stream | Output, + loss_function: str = "mse", + ) -> None: """ Adds a minimize loss function to the model. @@ -55,7 +66,7 @@ def addMinimize(self, name:str, streamA:str|Stream|Output, streamB:str|Stream|Ou self._neuralized = False @enforce_types - def removeMinimize(self, name_list:list|str) -> None: + def removeMinimize(self, name_list: list | str) -> None: """ Removes minimize loss functions using the given list of names. @@ -72,13 +83,33 @@ def removeMinimize(self, name_list:list|str) -> None: self._neuralized = False def __preliminary_checks(self, **kwargs): - check(self._data_loaded, RuntimeError, 'There is no data loaded! The Training will stop.') - check('Models' in self._model_def.getJson(), RuntimeError, 'There are no models to train. Load a model using the addModel function.') - check(list(self._model.parameters()), RuntimeError, 'There are no modules with learnable parameters! The Training will stop.') - if kwargs.get('train_dataset', None) is None: - check(kwargs.get('validation_dataset', None) is None, ValueError, 'If train_dataset is None, validation_dataset must also be None.') - for model in kwargs['models']: - check(model in kwargs['all_models'], ValueError, f'The model {model} is not in the model definition') + check( + self._data_loaded, + RuntimeError, + "There is no data loaded! The Training will stop.", + ) + check( + "Models" in self._model_def.getJson(), + RuntimeError, + "There are no models to train. Load a model using the addModel function.", + ) + check( + list(self._model.parameters()), + RuntimeError, + "There are no modules with learnable parameters! The Training will stop.", + ) + if kwargs.get("train_dataset", None) is None: + check( + kwargs.get("validation_dataset", None) is None, + ValueError, + "If train_dataset is None, validation_dataset must also be None.", + ) + for model in kwargs["models"]: + check( + model in kwargs["all_models"], + ValueError, + f"The model {model} is not in the model definition", + ) def __fill_parameters(func): @wraps(func) @@ -89,10 +120,14 @@ def wrapper(self, *args, **kwargs): # Get standard parameters standard = self._standard_train_parameters # Get user_parameters - users = bound.arguments.get('training_params', None) + users = bound.arguments.get("training_params", None) # Fill missing (None) arguments for param in sig.parameters.values(): - if param.name == 'self' or param.name == 'lr' or param.name == 'lr_param': + if ( + param.name == "self" + or param.name == "lr" + or param.name == "lr_param" + ): continue if bound.arguments.get(param.name, None) is None: if param.name in users.keys(): @@ -100,35 +135,51 @@ def wrapper(self, *args, **kwargs): else: bound.arguments[param.name] = standard.get(param.name, None) return func(**bound.arguments) + return wrapper - def __initialize_optimizer(self, models, optimizer, training_params, optimizer_params, optimizer_defaults, add_optimizer_defaults, add_optimizer_params, lr, lr_param): + def __initialize_optimizer( + self, + models, + optimizer, + training_params, + optimizer_params, + optimizer_defaults, + add_optimizer_defaults, + add_optimizer_params, + lr, + lr_param, + ): ## Get models params_to_train = set() for model in models: - if type(self._model_def['Models']) is dict: - params_to_train |= set(self._model_def['Models'][model]['Parameters']) + if type(self._model_def["Models"]) is dict: + params_to_train |= set(self._model_def["Models"][model]["Parameters"]) else: - params_to_train |= set(self._model_def['Parameters'].keys()) + params_to_train |= set(self._model_def["Parameters"].keys()) # Get the optimizer if type(optimizer) is str: - if optimizer == 'SGD': + if optimizer == "SGD": optimizer = SGD({}, []) - elif optimizer == 'Adam': + elif optimizer == "Adam": optimizer = Adam({}, []) else: optimizer = copy.deepcopy(optimizer) - check(issubclass(type(optimizer), Optimizer), TypeError, "The optimizer must be an Optimizer or str") + check( + issubclass(type(optimizer), Optimizer), + TypeError, + "The optimizer must be an Optimizer or str", + ) optimizer.set_params_to_train(self._model.all_parameters, params_to_train) - optimizer.add_defaults('lr', self._standard_train_parameters['lr']) + optimizer.add_defaults("lr", self._standard_train_parameters["lr"]) - if training_params and 'lr' in training_params: - optimizer.add_defaults('lr', training_params['lr']) - if training_params and 'lr_param' in training_params: - optimizer.add_option_to_params('lr', training_params['lr_param']) + if training_params and "lr" in training_params: + optimizer.add_defaults("lr", training_params["lr"]) + if training_params and "lr_param" in training_params: + optimizer.add_option_to_params("lr", training_params["lr_param"]) if optimizer_defaults != {}: optimizer.set_defaults(optimizer_defaults) @@ -140,21 +191,21 @@ def __initialize_optimizer(self, models, optimizer, training_params, optimizer_p add_optimizer_params = optimizer.unfold(add_optimizer_params) for param in add_optimizer_params: - par = param['params'] - del param['params'] + par = param["params"] + del param["params"] for key, value in param.items(): optimizer.add_option_to_params(key, {par: value}) # Modify the parameter - optimizer.add_defaults('lr', lr) + optimizer.add_defaults("lr", lr) if lr_param: - optimizer.add_option_to_params('lr', lr_param) + optimizer.add_option_to_params("lr", lr_param) self.__optimizer = optimizer def __initialize_loss(self): - for name, values in self._model_def['Minimizers'].items(): - self.__loss_functions[name] = CustomLoss(values['loss']) + for name, values in self._model_def["Minimizers"].items(): + self.__loss_functions[name] = CustomLoss(values["loss"]) def getTrainingInfo(self): """ @@ -168,72 +219,109 @@ def getTrainingInfo(self): dict A dictionary containing the training parameters and information. """ - to_remove = ['XY_train','XY_val','XY_test','train_indexes','val_indexes','test_indexes'] - tp = copy.deepcopy({key:value for key, value in self.running_parameters.items() if key not in to_remove}) + to_remove = [ + "XY_train", + "XY_val", + "XY_test", + "train_indexes", + "val_indexes", + "test_indexes", + ] + tp = copy.deepcopy( + { + key: value + for key, value in self.running_parameters.items() + if key not in to_remove + } + ) ## training - tp['update_per_epochs'] = len(self.running_parameters['train_indexes']) // (tp['train_batch_size'] + tp['step']) - if tp['prediction_samples'] >= 0: # TODO - tp['n_first_samples_train'] = len(self.running_parameters['train_indexes']) - if tp['n_samples_val'] > 0: - tp['n_first_samples_val'] = len(self.running_parameters['val_indexes']) - if tp['n_samples_test'] > 0: - tp['n_first_samples_test'] = len(self.running_parameters['test_indexes']) - + tp["update_per_epochs"] = len(self.running_parameters["train_indexes"]) // ( + tp["train_batch_size"] + tp["step"] + ) + if tp["prediction_samples"] >= 0: # TODO + tp["n_first_samples_train"] = len(self.running_parameters["train_indexes"]) + if tp["n_samples_val"] > 0: + tp["n_first_samples_val"] = len(self.running_parameters["val_indexes"]) + if tp["n_samples_test"] > 0: + tp["n_first_samples_test"] = len( + self.running_parameters["test_indexes"] + ) ## optimizer - tp['optimizer'] = self.__optimizer.name - tp['optimizer_defaults'] = self.__optimizer.optimizer_defaults - tp['optimizer_params'] = self.__optimizer.optimizer_params + tp["optimizer"] = self.__optimizer.name + tp["optimizer_defaults"] = self.__optimizer.optimizer_defaults + tp["optimizer_params"] = self.__optimizer.optimizer_params ## early stopping - early_stopping = tp['early_stopping'] + early_stopping = tp["early_stopping"] if early_stopping: - tp['early_stopping'] = early_stopping.__name__ + tp["early_stopping"] = early_stopping.__name__ ## Loss functions - tp['minimizers'] = {} - for name, values in self._model_def['Minimizers'].items(): - tp['minimizers'][name] = {} - tp['minimizers'][name]['A'] = values['A'] - tp['minimizers'][name]['B'] = values['B'] - tp['minimizers'][name]['loss'] = values['loss'] - if name in tp['minimize_gain']: - tp['minimizers'][name]['gain'] = tp['minimize_gain'][name] + tp["minimizers"] = {} + for name, values in self._model_def["Minimizers"].items(): + tp["minimizers"][name] = {} + tp["minimizers"][name]["A"] = values["A"] + tp["minimizers"][name]["B"] = values["B"] + tp["minimizers"][name]["loss"] = values["loss"] + if name in tp["minimize_gain"]: + tp["minimizers"][name]["gain"] = tp["minimize_gain"][name] return tp def __check_needed_keys(self, train_data, connect, closed_loop): # Needed keys - keys = set(self._model_def['Inputs'].keys()) - keys |= ({value['A'] for value in self._model_def['Minimizers'].values()} | {value['B'] for value in self._model_def['Minimizers'].values()}) + keys = set(self._model_def["Inputs"].keys()) + keys |= {value["A"] for value in self._model_def["Minimizers"].values()} | { + value["B"] for value in self._model_def["Minimizers"].values() + } # Available keys - keys -= set(self._model_def['Outputs'].keys()|self._model_def['Relations'].keys()) + keys -= set( + self._model_def["Outputs"].keys() | self._model_def["Relations"].keys() + ) keys -= set(self._model_def.recurrentInputs().keys()) - keys -= (set(connect.keys()|closed_loop.keys())) + keys -= set(connect.keys() | closed_loop.keys()) # Check if the keys are in the dataset - check(set(keys).issubset(set(train_data.keys())), KeyError, f"Not all the mandatory keys {keys} are present in the training dataset {set(train_data.keys())}.") + check( + set(keys).issubset(set(train_data.keys())), + KeyError, + f"Not all the mandatory keys {keys} are present in the training dataset {set(train_data.keys())}.", + ) @enforce_types @__fill_parameters - def trainModel(self, *, - name: str | None = None, - models: str | list | None = None, - train_dataset: str | list | dict | None = None, validation_dataset: str | list | dict | None = None, - dataset: str | list | None = None, splits: list | None = None, - closed_loop: dict | None = None, connect: dict | None = None, step: int | None = None, prediction_samples: int | None = None, - shuffle_data: bool | None = None, - early_stopping: Callable | None = None, early_stopping_params: dict | None = None, - select_model: Callable | None = None, select_model_params: dict | None = None, - minimize_gain: dict | None = None, - num_of_epochs: int = None, - train_batch_size: int = None, val_batch_size: int = None, - optimizer: str | Optimizer | None = None, - lr: int | float | None = None, lr_param: dict | None = None, - optimizer_params: list | None = None, optimizer_defaults: dict | None = None, - add_optimizer_params: list | None = None, add_optimizer_defaults: dict | None = None, - training_params: dict | None = {} - ) -> None: + def trainModel( + self, + *, + name: str | None = None, + models: str | list | None = None, + train_dataset: str | list | dict | None = None, + validation_dataset: str | list | dict | None = None, + dataset: str | list | None = None, + splits: list | None = None, + closed_loop: dict | None = None, + connect: dict | None = None, + step: int | None = None, + prediction_samples: int | None = None, + shuffle_data: bool | None = None, + early_stopping: Callable | None = None, + early_stopping_params: dict | None = None, + select_model: Callable | None = None, + select_model_params: dict | None = None, + minimize_gain: dict | None = None, + num_of_epochs: int = None, + train_batch_size: int = None, + val_batch_size: int = None, + optimizer: str | Optimizer | None = None, + lr: int | float | None = None, + lr_param: dict | None = None, + optimizer_params: list | None = None, + optimizer_defaults: dict | None = None, + add_optimizer_params: list | None = None, + add_optimizer_defaults: dict | None = None, + training_params: dict | None = {}, + ) -> None: """ Trains the model using the provided datasets and parameters. @@ -307,21 +395,36 @@ def trainModel(self, *, .. include:: /examples_basics/trainer_module_ex/trainModel.rst """ ## Get model for train - all_models = list(self._model_def['Models'].keys()) if type(self._model_def['Models']) is dict else [self._model_def['Models']] + all_models = ( + list(self._model_def["Models"].keys()) + if type(self._model_def["Models"]) is dict + else [self._model_def["Models"]] + ) if models is None: models = all_models if isinstance(models, str): models = [models] ## Preliminary Checks - self.__preliminary_checks(models = models, all_models = all_models, train_dataset = train_dataset, validation_dataset = validation_dataset) + self.__preliminary_checks( + models=models, + all_models=all_models, + train_dataset=train_dataset, + validation_dataset=validation_dataset, + ) ## Recurret variables - prediction_samples = self._setup_recurrent_variables(prediction_samples, closed_loop, connect) + prediction_samples = self._setup_recurrent_variables( + prediction_samples, closed_loop, connect + ) ## Get the dataset - XY_train, XY_val, XY_test = self._setup_dataset(train_dataset, validation_dataset, None, dataset, splits) - self.__check_needed_keys(train_data=XY_train, connect=connect, closed_loop=closed_loop) + XY_train, XY_val, XY_test = self._setup_dataset( + train_dataset, validation_dataset, None, dataset, splits + ) + self.__check_needed_keys( + train_data=XY_train, connect=connect, closed_loop=closed_loop + ) n_samples_train = next(iter(XY_train.values())).size(0) n_samples_val = next(iter(XY_val.values())).size(0) if XY_val else 0 @@ -330,7 +433,7 @@ def trainModel(self, *, if train_dataset is not None: train_tag = self._get_tag(train_dataset) val_tag = self._get_tag(validation_dataset) - else: ## splits is used + else: ## splits is used if dataset is None: dataset = list(self._data.keys()) tag = self._get_tag(dataset) @@ -340,22 +443,50 @@ def trainModel(self, *, train_indexes, val_indexes = [], [] if train_dataset is not None: - train_indexes, val_indexes = self._get_batch_indexes(train_dataset, n_samples_train, prediction_samples), self._get_batch_indexes(validation_dataset, n_samples_val, prediction_samples) + train_indexes, val_indexes = ( + self._get_batch_indexes( + train_dataset, n_samples_train, prediction_samples + ), + self._get_batch_indexes( + validation_dataset, n_samples_val, prediction_samples + ), + ) else: dataset = list(self._data.keys()) if dataset is None else dataset - train_indexes = self._get_batch_indexes(dataset, n_samples_train, prediction_samples) - check(len(train_indexes) > 0, ValueError, - 'The number of valid train samples is less than the number of prediction samples.') + train_indexes = self._get_batch_indexes( + dataset, n_samples_train, prediction_samples + ) + check( + len(train_indexes) > 0, + ValueError, + "The number of valid train samples is less than the number of prediction samples.", + ) if n_samples_val > 0: - val_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val, prediction_samples) - val_indexes = [i - n_samples_train for i in val_indexes if i >= n_samples_train] + val_indexes = self._get_batch_indexes( + dataset, n_samples_train + n_samples_val, prediction_samples + ) + val_indexes = [ + i - n_samples_train for i in val_indexes if i >= n_samples_train + ] if len(val_indexes) < 0: - log.warning('The number of valid validation samples is less than the number of prediction samples.') + log.warning( + "The number of valid validation samples is less than the number of prediction samples." + ) if n_samples_test > 0: - test_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val + n_samples_test, prediction_samples) - test_indexes = [i - (n_samples_train+n_samples_val)for i in test_indexes if i >= (n_samples_train+n_samples_val)] + test_indexes = self._get_batch_indexes( + dataset, + n_samples_train + n_samples_val + n_samples_test, + prediction_samples, + ) + test_indexes = [ + i - (n_samples_train + n_samples_val) + for i in test_indexes + if i >= (n_samples_train + n_samples_val) + ] if len(test_indexes) < 0: - log.warning('The number of valid test samples is less than the number of prediction samples.') + log.warning( + "The number of valid test samples is less than the number of prediction samples." + ) ## clip batch size and step train_batch_size = self._clip_batch_size(len(train_indexes), train_batch_size) @@ -365,32 +496,48 @@ def trainModel(self, *, val_step = self._clip_step(step, val_indexes, val_batch_size) ## Save the training parameters - self.running_parameters = {key:value for key,value in locals().items() if key not in ['self', 'kwargs', 'training_params', 'lr', 'lr_param']} + self.running_parameters = { + key: value + for key, value in locals().items() + if key not in ["self", "kwargs", "training_params", "lr", "lr_param"] + } ## Define the optimizer - self.__initialize_optimizer(models, optimizer, training_params, optimizer_params, optimizer_defaults, add_optimizer_defaults, add_optimizer_params, lr, lr_param) + self.__initialize_optimizer( + models, + optimizer, + training_params, + optimizer_params, + optimizer_defaults, + add_optimizer_defaults, + add_optimizer_params, + lr, + lr_param, + ) torch_optimizer = self.__optimizer.get_torch_optimizer() ## Define the loss functions self.__initialize_loss() ## Define mandatory inputs - mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs(connect, closed_loop) + mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs( + connect, closed_loop + ) ## Check close loop and connect self._clean_log_internal() ## Create the train, validation and test loss dictionaries train_losses, val_losses = {}, {} - for key in self._model_def['Minimizers'].keys(): + for key in self._model_def["Minimizers"].keys(): train_losses[key] = [] if n_samples_val > 0: val_losses[key] = [] ## Set the gradient to true if necessary - model_inputs = self._model_def['Inputs'] + model_inputs = self._model_def["Inputs"] for key in model_inputs.keys(): - if 'type' in model_inputs[key]: + if "type" in model_inputs[key]: if key in XY_train: XY_train[key].requires_grad_(True) if key in XY_val: @@ -415,27 +562,64 @@ def trainModel(self, *, ## TRAIN self._model.train() if prediction_samples >= 0: - losses = self._recurrent_inference(XY_train, train_indexes, train_batch_size, minimize_gain, prediction_samples, train_step, non_mandatory_inputs, mandatory_inputs, self.__loss_functions, shuffle=shuffle_data, optimizer=torch_optimizer) + losses = self._recurrent_inference( + XY_train, + train_indexes, + train_batch_size, + minimize_gain, + prediction_samples, + train_step, + non_mandatory_inputs, + mandatory_inputs, + self.__loss_functions, + shuffle=shuffle_data, + optimizer=torch_optimizer, + ) else: - losses = self._inference(XY_train, n_samples_train, train_batch_size, minimize_gain, self.__loss_functions, shuffle=shuffle_data, optimizer=torch_optimizer) + losses = self._inference( + XY_train, + n_samples_train, + train_batch_size, + minimize_gain, + self.__loss_functions, + shuffle=shuffle_data, + optimizer=torch_optimizer, + ) ## save the losses - for ind, key in enumerate(self._model_def['Minimizers'].keys()): + for ind, key in enumerate(self._model_def["Minimizers"].keys()): train_losses[key].append(torch.mean(losses[ind]).tolist()) if n_samples_val > 0: ## VALIDATION self._model.eval() setted_log_internal = self._log_internal - self._set_log_internal(False) # TODO To remove when the function is moved outside the train + self._set_log_internal( + False + ) # TODO To remove when the function is moved outside the train if prediction_samples >= 0: - losses = self._recurrent_inference(XY_val, val_indexes, val_batch_size, minimize_gain, prediction_samples, val_step, - non_mandatory_inputs, mandatory_inputs, self.__loss_functions) + losses = self._recurrent_inference( + XY_val, + val_indexes, + val_batch_size, + minimize_gain, + prediction_samples, + val_step, + non_mandatory_inputs, + mandatory_inputs, + self.__loss_functions, + ) else: - losses = self._inference(XY_val, n_samples_val, val_batch_size, minimize_gain, self.__loss_functions) + losses = self._inference( + XY_val, + n_samples_val, + val_batch_size, + minimize_gain, + self.__loss_functions, + ) self._set_log_internal(setted_log_internal) ## save the losses - for ind, key in enumerate(self._model_def['Minimizers'].keys()): + for ind, key in enumerate(self._model_def["Minimizers"].keys()): val_losses[key].append(torch.mean(losses[ind]).tolist()) if callable(select_model): @@ -446,7 +630,9 @@ def trainModel(self, *, ## Early-stopping if callable(early_stopping): if early_stopping(train_losses, val_losses, early_stopping_params): - log.info(f'Stopping the training at epoch {epoch} due to early stopping.') + log.info( + f"Stopping the training at epoch {epoch} due to early stopping." + ) break ## Visualize the training... @@ -457,10 +643,10 @@ def trainModel(self, *, end = time.time() self.visualizer.showTrainingTime(end - start) - for key in self._model_def['Minimizers'].keys(): - self._training[key] = {'train': train_losses[key]} + for key in self._model_def["Minimizers"].keys(): + self._training[key] = {"train": train_losses[key]} if n_samples_val > 0: - self._training[key]['val'] = val_losses[key] + self._training[key]["val"] = val_losses[key] self.visualizer.showEndTraining(num_of_epochs - 1, train_losses, val_losses) ## Select the model @@ -469,11 +655,13 @@ def trainModel(self, *, # The model selected is updated for the last time by the final batch; # so the minimum loss (selected model) is referred to the model before the last update. # If the batch is small compared to the dataset dimension the differences in the model are small. - log.warning('If not validation set is provided the selected model can differ from the optimal.') - log.info(f'Selected the model at the epoch {best_model_epoch + 1}.') + log.warning( + "If not validation set is provided the selected model can differ from the optimal." + ) + log.info(f"Selected the model at the epoch {best_model_epoch + 1}.") self._model = Model(selected_model_def) else: - log.info('The selected model is the LAST model of the training.') + log.info("The selected model is the LAST model of the training.") ## Remove virtual states self._remove_virtual_states(connect, closed_loop) @@ -481,5 +669,6 @@ def trainModel(self, *, ## Get trained model from torch and set the model_def self._model_def.updateParameters(self._model) -#from 685 -#from 840 \ No newline at end of file + +# from 685 +# from 840 diff --git a/nnodely/operators/validator.py b/nnodely/operators/validator.py index a2bdc583..0915ca79 100644 --- a/nnodely/operators/validator.py +++ b/nnodely/operators/validator.py @@ -5,12 +5,17 @@ from nnodely.basic.loss import CustomLoss from nnodely.operators.network import Network -from nnodely.support.utils import check, TORCH_DTYPE, enforce_types +from nnodely.support.utils import check, TORCH_DTYPE, enforce_types + class Validator(Network): @enforce_types def __init__(self): - check(type(self) is not Validator, TypeError, "Validator class cannot be instantiated directly") + check( + type(self) is not Validator, + TypeError, + "Validator class cannot be instantiated directly", + ) super().__init__() # Validation Parameters @@ -26,18 +31,23 @@ def prediction(self): return ReadOnlyDict(self.__prediction) @enforce_types - def _analyze(self, - dataset: dict, - dataset_tag: str, - indexes: list = None, - minimize_gain: dict = {}, - closed_loop: dict = {}, - connect: dict = {}, - prediction_samples: int | str = 0, - step: int = 0, - batch_size: int | None = None - ) -> None: - with torch.enable_grad() if self._get_gradient_on_inference() else torch.inference_mode(): + def _analyze( + self, + dataset: dict, + dataset_tag: str, + indexes: list = None, + minimize_gain: dict = {}, + closed_loop: dict = {}, + connect: dict = {}, + prediction_samples: int | str = 0, + step: int = 0, + batch_size: int | None = None, + ) -> None: + with ( + torch.enable_grad() + if self._get_gradient_on_inference() + else torch.inference_mode() + ): self._model.eval() self.__performance[dataset_tag] = {} self.__prediction[dataset_tag] = {} @@ -48,36 +58,52 @@ def _analyze(self, # Create the losses losses = {} - for name, values in self._model_def['Minimizers'].items(): - losses[name] = CustomLoss(values['loss']) + for name, values in self._model_def["Minimizers"].items(): + losses[name] = CustomLoss(values["loss"]) - #data = self._get_data(dataset) + # data = self._get_data(dataset) n_samples = len(dataset[list(dataset.keys())[0]]) batch_size = get_batch_size(n_samples, batch_size, prediction_samples) - prediction_samples = self._setup_recurrent_variables(prediction_samples, closed_loop, connect) + prediction_samples = self._setup_recurrent_variables( + prediction_samples, closed_loop, connect + ) if prediction_samples >= 0: - mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs(connect,closed_loop) + mandatory_inputs, non_mandatory_inputs = self._get_mandatory_inputs( + connect, closed_loop + ) idxs = [] for horizon_idx in range(prediction_samples + 1): idxs.append([]) - for key, value in self._model_def['Minimizers'].items(): + for key, value in self._model_def["Minimizers"].items(): total_losses[key], A[key], B[key] = [], [], [] for horizon_idx in range(prediction_samples + 1): A[key].append([]) B[key].append([]) ## Update with virtual states - self._model.update(closed_loop = closed_loop, connect = connect) - self._recurrent_inference(dataset, indexes, batch_size, minimize_gain, prediction_samples, - step, non_mandatory_inputs, mandatory_inputs, losses, - total_losses = total_losses, A = A, B = B, idxs = idxs) + self._model.update(closed_loop=closed_loop, connect=connect) + self._recurrent_inference( + dataset, + indexes, + batch_size, + minimize_gain, + prediction_samples, + step, + non_mandatory_inputs, + mandatory_inputs, + losses, + total_losses=total_losses, + A=A, + B=B, + idxs=idxs, + ) for horizon_idx in range(prediction_samples + 1): idxs[horizon_idx] = np.concatenate(idxs[horizon_idx]) - for key, value in self._model_def['Minimizers'].items(): + for key, value in self._model_def["Minimizers"].items(): for horizon_idx in range(prediction_samples + 1): if A is not None: A[key][horizon_idx] = np.concatenate(A[key][horizon_idx]) @@ -86,65 +112,102 @@ def _analyze(self, if total_losses is not None: total_losses[key] = np.mean(total_losses[key]) else: - for key, value in self._model_def['Minimizers'].items(): + for key, value in self._model_def["Minimizers"].items(): total_losses[key], A[key], B[key] = [], [], [] self._model.update(disconnect=True) - self._inference(dataset, n_samples, batch_size, minimize_gain, losses, - total_losses = total_losses, A = A, B = B) - - for key, value in self._model_def['Minimizers'].items(): + self._inference( + dataset, + n_samples, + batch_size, + minimize_gain, + losses, + total_losses=total_losses, + A=A, + B=B, + ) + + for key, value in self._model_def["Minimizers"].items(): A[key] = np.concatenate(A[key]) B[key] = np.concatenate(B[key]) total_losses[key] = np.mean(total_losses[key]) - for ind, (key, value) in enumerate(self._model_def['Minimizers'].items()): + for ind, (key, value) in enumerate(self._model_def["Minimizers"].items()): A_np = np.array(A[key]) B_np = np.array(B[key]) self.__performance[dataset_tag][key] = {} - self.__performance[dataset_tag][key][value['loss']] = np.mean(total_losses[key]).item() - self.__performance[dataset_tag][key]['fvu'] = {} + self.__performance[dataset_tag][key][value["loss"]] = np.mean( + total_losses[key] + ).item() + self.__performance[dataset_tag][key]["fvu"] = {} # Compute FVU residual = A_np - B_np error_var = np.var(residual) error_mean = np.mean(residual) - #error_var_manual = np.sum((residual-error_mean) ** 2) / (len(self.__prediction['B'][ind]) - 0) - #print(f"{key} var np:{new_error_var} and var manual:{error_var_manual}") + # error_var_manual = np.sum((residual-error_mean) ** 2) / (len(self.__prediction['B'][ind]) - 0) + # print(f"{key} var np:{new_error_var} and var manual:{error_var_manual}") with warnings.catch_warnings(record=True) as w: - self.__performance[dataset_tag][key]['fvu']['A'] = (error_var / np.var(A_np)).item() - self.__performance[dataset_tag][key]['fvu']['B'] = (error_var / np.var(B_np)).item() - if w and np.var(A_np) == 0.0 and np.var(B_np) == 0.0: - self.__performance[dataset_tag][key]['fvu']['A'] = np.nan - self.__performance[dataset_tag][key]['fvu']['B'] = np.nan - self.__performance[dataset_tag][key]['fvu']['total'] = np.mean([self.__performance[dataset_tag][key]['fvu']['A'],self.__performance[dataset_tag][key]['fvu']['B']]).item() + self.__performance[dataset_tag][key]["fvu"]["A"] = ( + error_var / np.var(A_np) + ).item() + self.__performance[dataset_tag][key]["fvu"]["B"] = ( + error_var / np.var(B_np) + ).item() + if w and np.var(A_np) == 0.0 and np.var(B_np) == 0.0: + self.__performance[dataset_tag][key]["fvu"]["A"] = np.nan + self.__performance[dataset_tag][key]["fvu"]["B"] = np.nan + self.__performance[dataset_tag][key]["fvu"]["total"] = np.mean( + [ + self.__performance[dataset_tag][key]["fvu"]["A"], + self.__performance[dataset_tag][key]["fvu"]["B"], + ] + ).item() # Compute AIC - #normal_dist = norm(0, error_var ** 0.5) - #probability_of_residual = normal_dist.pdf(residual) - #log_likelihood_first = sum(np.log(probability_of_residual)) - p1 = -len(residual)/2.0*np.log(2*np.pi) + # normal_dist = norm(0, error_var ** 0.5) + # probability_of_residual = normal_dist.pdf(residual) + # log_likelihood_first = sum(np.log(probability_of_residual)) + p1 = -len(residual) / 2.0 * np.log(2 * np.pi) with warnings.catch_warnings(record=True) as w: - p2 = -len(residual)/2.0*np.log(error_var) - p3 = -1 / (2.0 * error_var) * np.sum(residual ** 2) + p2 = -len(residual) / 2.0 * np.log(error_var) + p3 = -1 / (2.0 * error_var) * np.sum(residual**2) if w and p2 == np.float32(np.inf) and p3 == np.float32(-np.inf): p2 = p3 = 0.0 - log_likelihood = p1+p2+p3 - #print(f"{key} log likelihood second mode:{log_likelihood} = {p1}+{p2}+{p3} first mode: {log_likelihood_first}") - total_params = sum(p.numel() for p in self._model.parameters() if p.requires_grad) - #print(f"{key} total_params:{total_params}") - aic = - 2 * log_likelihood + 2 * total_params - #print(f"{key} aic:{aic}") - self.__performance[dataset_tag][key]['aic'] = {'value':aic,'total_params':total_params,'log_likelihood':log_likelihood} + log_likelihood = p1 + p2 + p3 + # print(f"{key} log likelihood second mode:{log_likelihood} = {p1}+{p2}+{p3} first mode: {log_likelihood_first}") + total_params = sum( + p.numel() for p in self._model.parameters() if p.requires_grad + ) + # print(f"{key} total_params:{total_params}") + aic = -2 * log_likelihood + 2 * total_params + # print(f"{key} aic:{aic}") + self.__performance[dataset_tag][key]["aic"] = { + "value": aic, + "total_params": total_params, + "log_likelihood": log_likelihood, + } # Prediction and target self.__prediction[dataset_tag][key] = {} - self.__prediction[dataset_tag][key]['A'] = A_np.tolist() - self.__prediction[dataset_tag][key]['B'] = B_np.tolist() + self.__prediction[dataset_tag][key]["A"] = A_np.tolist() + self.__prediction[dataset_tag][key]["B"] = B_np.tolist() if idxs is not None: - self.__prediction[dataset_tag]['idxs'] = np.array(idxs).tolist() - self.__performance[dataset_tag]['total'] = {} - self.__performance[dataset_tag]['total']['mean_error'] = np.mean([value for key,value in total_losses.items()]) - self.__performance[dataset_tag]['total']['fvu'] = np.mean([self.__performance[dataset_tag][key]['fvu']['total'] for key in self._model_def['Minimizers'].keys()]) - self.__performance[dataset_tag]['total']['aic'] = np.mean([self.__performance[dataset_tag][key]['aic']['value']for key in self._model_def['Minimizers'].keys()]) + self.__prediction[dataset_tag]["idxs"] = np.array(idxs).tolist() + self.__performance[dataset_tag]["total"] = {} + self.__performance[dataset_tag]["total"]["mean_error"] = np.mean( + [value for key, value in total_losses.items()] + ) + self.__performance[dataset_tag]["total"]["fvu"] = np.mean( + [ + self.__performance[dataset_tag][key]["fvu"]["total"] + for key in self._model_def["Minimizers"].keys() + ] + ) + self.__performance[dataset_tag]["total"]["aic"] = np.mean( + [ + self.__performance[dataset_tag][key]["aic"]["value"] + for key in self._model_def["Minimizers"].keys() + ] + ) self.visualizer.showResult(dataset_tag) @@ -185,13 +248,12 @@ def _analyze(self, # batch_size : # The batch size use for analyse the performance of the model on the provided dataset. - # """ # # Get the dataset if is None take all datasets # if dataset is None: # dataset = list(self._data.keys()) - # data = self._get_data(dataset) + # data = self._get_data(dataset) # n_samples = len(data[list(data.keys())[0]]) # data_tag = self._get_tag(dataset) if name is None else name # indexes = list(range(n_samples)) @@ -219,19 +281,20 @@ def _analyze(self, # if n_samples_test > 0: # self._analyze(data_test, f"{data_tag}_test", indexes, minimize_gain, closed_loop, connect, prediction_samples, step, batch_size) - @enforce_types - def analyzeModel(self, - dataset: str | list | dict | None = None, *, - tag: str | None = None, - splits: list | None = None, - minimize_gain: dict = {}, - closed_loop: dict = {}, - connect: dict = {}, - prediction_samples: int | str = 0, - step: int = 0, - batch_size: int | None = None - ) -> None: + def analyzeModel( + self, + dataset: str | list | dict | None = None, + *, + tag: str | None = None, + splits: list | None = None, + minimize_gain: dict = {}, + closed_loop: dict = {}, + connect: dict = {}, + prediction_samples: int | str = 0, + step: int = 0, + batch_size: int | None = None, + ) -> None: """ The function is used to analyze the performance of the model on the provided dataset. @@ -258,13 +321,15 @@ def analyzeModel(self, """ # Get the dataset if is None take all datasets if tag is None: - tag = dataset if isinstance(dataset, str) else 'default' + tag = dataset if isinstance(dataset, str) else "default" if dataset is None: dataset = list(self._data.keys()) - if splits: ## splits is used + if splits: ## splits is used ## Get the dataset - XY_train, XY_val, XY_test = self._setup_dataset(None, None, None, dataset, splits) + XY_train, XY_val, XY_test = self._setup_dataset( + None, None, None, dataset, splits + ) n_samples_train = next(iter(XY_train.values())).size(0) n_samples_val = next(iter(XY_val.values())).size(0) if XY_val else 0 n_samples_test = next(iter(XY_test.values())).size(0) if XY_test else 0 @@ -274,31 +339,82 @@ def analyzeModel(self, test_tag = f"{tag}_test" if n_samples_test > 0 else None train_indexes, val_indexes = [], [] - train_indexes = self._get_batch_indexes(dataset, n_samples_train, prediction_samples) - check(len(train_indexes) > 0, ValueError, - 'The number of valid train samples is less than the number of prediction samples.') + train_indexes = self._get_batch_indexes( + dataset, n_samples_train, prediction_samples + ) + check( + len(train_indexes) > 0, + ValueError, + "The number of valid train samples is less than the number of prediction samples.", + ) if n_samples_val > 0: - val_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val, prediction_samples) - val_indexes = [i - n_samples_train for i in val_indexes if i >= n_samples_train] + val_indexes = self._get_batch_indexes( + dataset, n_samples_train + n_samples_val, prediction_samples + ) + val_indexes = [ + i - n_samples_train for i in val_indexes if i >= n_samples_train + ] if n_samples_test > 0: - test_indexes = self._get_batch_indexes(dataset, n_samples_train + n_samples_val + n_samples_test, prediction_samples) - test_indexes = [i - (n_samples_train+n_samples_val)for i in test_indexes if i >= (n_samples_train+n_samples_val)] + test_indexes = self._get_batch_indexes( + dataset, + n_samples_train + n_samples_val + n_samples_test, + prediction_samples, + ) + test_indexes = [ + i - (n_samples_train + n_samples_val) + for i in test_indexes + if i >= (n_samples_train + n_samples_val) + ] ## Training set Results - self._analyze(XY_train, dataset_tag=train_tag, indexes=train_indexes, minimize_gain=minimize_gain, - closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples, - step=step, batch_size=batch_size) + self._analyze( + XY_train, + dataset_tag=train_tag, + indexes=train_indexes, + minimize_gain=minimize_gain, + closed_loop=closed_loop, + connect=connect, + prediction_samples=prediction_samples, + step=step, + batch_size=batch_size, + ) ## Validation set Results if n_samples_val > 0: - self._analyze(XY_val, dataset_tag=val_tag, indexes=val_indexes, minimize_gain=minimize_gain, - closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples, - step=step, batch_size=batch_size) + self._analyze( + XY_val, + dataset_tag=val_tag, + indexes=val_indexes, + minimize_gain=minimize_gain, + closed_loop=closed_loop, + connect=connect, + prediction_samples=prediction_samples, + step=step, + batch_size=batch_size, + ) ## Test set Results if n_samples_test > 0: - self._analyze(XY_test, dataset_tag=test_tag, indexes=test_indexes, minimize_gain=minimize_gain, - closed_loop=closed_loop, connect=connect, prediction_samples=prediction_samples, - step=step, batch_size=batch_size) + self._analyze( + XY_test, + dataset_tag=test_tag, + indexes=test_indexes, + minimize_gain=minimize_gain, + closed_loop=closed_loop, + connect=connect, + prediction_samples=prediction_samples, + step=step, + batch_size=batch_size, + ) else: - data = self._get_data(dataset) + data = self._get_data(dataset) n_samples = next(iter(data.values())).size(0) indexes = self._get_batch_indexes(dataset, n_samples, prediction_samples) - self._analyze(data, tag, indexes, minimize_gain, closed_loop, connect, prediction_samples, step, batch_size) \ No newline at end of file + self._analyze( + data, + tag, + indexes, + minimize_gain, + closed_loop, + connect, + prediction_samples, + step, + batch_size, + ) diff --git a/nnodely/support/earlystopping.py b/nnodely/support/earlystopping.py index dae6cd2e..b65c6a12 100644 --- a/nnodely/support/earlystopping.py +++ b/nnodely/support/earlystopping.py @@ -22,20 +22,24 @@ def early_stop_patience(train_losses, val_losses, params): bool True if training should be stopped early, False otherwise. """ - patience = params['patience'] if 'patience' in params.keys() else 50 + patience = params["patience"] if "patience" in params.keys() else 50 if val_losses: losses = val_losses else: # if there is no validation set, use the training losses losses = train_losses - if 'error' in params.keys(): + if "error" in params.keys(): # if the type of loss to be used is provided by the user - losses_use = losses[params['error']] + losses_use = losses[params["error"]] else: # take the mean of all the losses for all the keys of the dictionary import numpy as np - losses_use = [np.mean([losses[key][index] for key in losses.keys()]) for index in range(len(losses[list(losses.keys())[0]]))] + + losses_use = [ + np.mean([losses[key][index] for key in losses.keys()]) + for index in range(len(losses[list(losses.keys())[0]])) + ] if len(losses_use) > patience: # index of the minimum validation loss min_val_loss_index = losses_use.index(min(losses_use)) @@ -69,8 +73,12 @@ def select_best_model(train_losses, val_losses, params): # if there is no validation set, use the training losses losses = train_losses import numpy as np - losses_use = [np.mean([losses[key][index] for key in losses.keys()]) for index in range(len(losses[list(losses.keys())[0]]))] - if len(losses_use)-1 == losses_use.index(min(losses_use)): + + losses_use = [ + np.mean([losses[key][index] for key in losses.keys()]) + for index in range(len(losses[list(losses.keys())[0]])) + ] + if len(losses_use) - 1 == losses_use.index(min(losses_use)): return True else: return False @@ -94,9 +102,11 @@ def mean_stopping(train_losses, val_losses, params): bool True if training should be stopped early, False otherwise. """ - tol = params['tol'] if 'tol' in params.keys() else 0.001 + tol = params["tol"] if "tol" in params.keys() else 0.001 if val_losses: - for (train_loss_name, train_loss_value), (val_loss_name, val_loss_value) in zip(train_losses.items(), val_losses.items()): + for (train_loss_name, train_loss_value), (val_loss_name, val_loss_value) in zip( + train_losses.items(), val_losses.items() + ): if abs(train_loss_value[-1] - val_loss_value[-1]) < tol: return True else: @@ -105,6 +115,7 @@ def mean_stopping(train_losses, val_losses, params): return True return False + def standard_early_stopping(train_losses, val_losses, params): """ Determines whether to stop training early based on training and validation losses. @@ -123,12 +134,14 @@ def standard_early_stopping(train_losses, val_losses, params): bool True if training should be stopped early, False otherwise. """ - n = params['tol'] if 'tol' in params.keys() else 10 + n = params["tol"] if "tol" in params.keys() else 10 if val_losses: - for (_, train_loss_value), (_, val_loss_value) in zip(train_losses.items(), val_losses.items()): + for (_, train_loss_value), (_, val_loss_value) in zip( + train_losses.items(), val_losses.items() + ): if (len(train_loss_value) <= n) and (len(val_loss_value) <= n): return False - + tol = 0.0 for train_loss, val_loss in zip(train_loss_value[-n:], val_loss_value[-n:]): if abs(train_loss - val_loss) > tol: @@ -137,13 +150,12 @@ def standard_early_stopping(train_losses, val_losses, params): return False else: for _, loss_value in train_losses.items(): - if (len(loss_value) <= n): + if len(loss_value) <= n: return False - + tol = loss_value[-n] - for loss in loss_value[-n+1:]: + for loss in loss_value[-n + 1 :]: if loss < tol: return False - + return True - \ No newline at end of file diff --git a/nnodely/support/fixstepsolver.py b/nnodely/support/fixstepsolver.py index 01158271..7f45a1b4 100644 --- a/nnodely/support/fixstepsolver.py +++ b/nnodely/support/fixstepsolver.py @@ -1,35 +1,48 @@ from nnodely.layers.parameter import SampleTime -class FixedStepSolver(): - def __init__(self, int_name:str|None = None, der_name:str|None = None): + +class FixedStepSolver: + def __init__(self, int_name: str | None = None, der_name: str | None = None): self.dt = SampleTime() self.int_name = int_name self.der_name = der_name + class Euler(FixedStepSolver): - def __init__(self, int_name:str|None = None, der_name:str|None = None): + def __init__(self, int_name: str | None = None, der_name: str | None = None): super().__init__(int_name, der_name) + def integrate(self, obj): from nnodely.layers.input import Input - integral = Input(self.int_name, dimensions=obj.dim['dim']) - return (integral.last() + obj * self.dt).closedLoop(integral) + + integral = Input(self.int_name, dimensions=obj.dim["dim"]) + return (integral.last() + obj * self.dt).closedLoop(integral) def derivate(self, obj): from nnodely.layers.input import Input - obj = Input(self.int_name, dimensions=obj.dim['dim']).connect(obj) + + obj = Input(self.int_name, dimensions=obj.dim["dim"]).connect(obj) return (obj.last() - obj.sw([-2, -1])) / self.dt + class Trapezoidal(FixedStepSolver): - def __init__(self, int_name:str|None = None, der_name:str|None = None): + def __init__(self, int_name: str | None = None, der_name: str | None = None): super().__init__(int_name, der_name) + def integrate(self, obj): from nnodely.layers.input import Input - integral = Input(self.int_name, dimensions=obj.dim['dim']) - obj = Input(self.der_name, dimensions=obj.dim['dim']).connect(obj) - return (integral.last() + (obj.last() + obj.sw([-2,-1])) * 0.5 * self.dt).closedLoop(integral) + + integral = Input(self.int_name, dimensions=obj.dim["dim"]) + obj = Input(self.der_name, dimensions=obj.dim["dim"]).connect(obj) + return ( + integral.last() + (obj.last() + obj.sw([-2, -1])) * 0.5 * self.dt + ).closedLoop(integral) def derivate(self, obj): from nnodely.layers.input import Input - obj = Input(self.int_name, dimensions=obj.dim['dim']).connect(obj) - derivative = Input(self.der_name, dimensions=obj.dim['dim']) - return (((obj.last() - obj.sw([-2, -1])) * 2.0) / self.dt - derivative.last()).closedLoop(derivative) \ No newline at end of file + + obj = Input(self.int_name, dimensions=obj.dim["dim"]).connect(obj) + derivative = Input(self.der_name, dimensions=obj.dim["dim"]) + return ( + ((obj.last() - obj.sw([-2, -1])) * 2.0) / self.dt - derivative.last() + ).closedLoop(derivative) diff --git a/nnodely/support/initializer.py b/nnodely/support/initializer.py index 133d495e..10704e0b 100644 --- a/nnodely/support/initializer.py +++ b/nnodely/support/initializer.py @@ -1,5 +1,4 @@ - -def init_constant(indexes, params_size, dict_param = {'value':1}): +def init_constant(indexes, params_size, dict_param={"value": 1}): """ Initializes parameters to a constant value. @@ -10,9 +9,12 @@ def init_constant(indexes, params_size, dict_param = {'value':1}): value : int or float The constant value to initialize the parameters with. """ - return dict_param['value'] + return dict_param["value"] + -def init_negexp(indexes, params_size, dict_param = {'size_index':0, 'first_value':1, 'lambda':3}): +def init_negexp( + indexes, params_size, dict_param={"size_index": 0, "first_value": 1, "lambda": 3} +): """ Initializes parameters using a negative decay exponential function. @@ -32,12 +34,27 @@ def init_negexp(indexes, params_size, dict_param = {'size_index':0, 'first_value The decay rate parameter of the exponential function. """ import numpy as np - size_index = dict_param['size_index'] + + size_index = dict_param["size_index"] # check if the size of the list of parameters is 1, to avoid a division by zero - x = 1 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1) - return dict_param['first_value']*np.exp(-dict_param['lambda']*(1-x)) + x = ( + 1 + if params_size[size_index] - 1 == 0 + else indexes[size_index] / (params_size[size_index] - 1) + ) + return dict_param["first_value"] * np.exp(-dict_param["lambda"] * (1 - x)) -def init_exp(indexes, params_size, dict_param = {'size_index':0, 'max_value':1, 'lambda':3, 'monotonicity':'decreasing'}): + +def init_exp( + indexes, + params_size, + dict_param={ + "size_index": 0, + "max_value": 1, + "lambda": 3, + "monotonicity": "decreasing", + }, +): """ Initializes parameters using an increasing or decreasing exponential function. @@ -64,21 +81,37 @@ def init_exp(indexes, params_size, dict_param = {'size_index':0, 'max_value':1, If the monotonicity is not 'increasing' or 'decreasing'. """ import numpy as np - size_index = dict_param['size_index'] - monotonicity = dict_param['monotonicity'] - if monotonicity == 'increasing': + + size_index = dict_param["size_index"] + monotonicity = dict_param["monotonicity"] + if monotonicity == "increasing": # increasing exponential, the 'max_value' is the value at x=1, i.e, at the end of the range - x = 1 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1) - out = dict_param['max_value']*np.exp(dict_param['lambda']*(x-1)) - elif monotonicity == 'decreasing': + x = ( + 1 + if params_size[size_index] - 1 == 0 + else indexes[size_index] / (params_size[size_index] - 1) + ) + out = dict_param["max_value"] * np.exp(dict_param["lambda"] * (x - 1)) + elif monotonicity == "decreasing": # decreasing exponential, the 'max_value' is the value at x=0, i.e, at the beginning of the range - x = 0 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1) - out = dict_param['max_value']*np.exp(-dict_param['lambda']*x) + x = ( + 0 + if params_size[size_index] - 1 == 0 + else indexes[size_index] / (params_size[size_index] - 1) + ) + out = dict_param["max_value"] * np.exp(-dict_param["lambda"] * x) else: - raise ValueError('The parameter monotonicity must be either increasing or decreasing.') + raise ValueError( + "The parameter monotonicity must be either increasing or decreasing." + ) return out -def init_lin(indexes, params_size, dict_param = {'size_index':0, 'first_value':1, 'last_value':0}): + +def init_lin( + indexes, + params_size, + dict_param={"size_index": 0, "first_value": 1, "last_value": 0}, +): """ Initializes parameters using a linear function. @@ -97,6 +130,12 @@ def init_lin(indexes, params_size, dict_param = {'size_index':0, 'first_value':1 last_value : int or float The value at the end of the range. """ - size_index = dict_param['size_index'] - x = 0 if params_size[size_index]-1 == 0 else indexes[size_index]/(params_size[size_index]-1) - return (dict_param['last_value'] - dict_param['first_value']) * x + dict_param['first_value'] + size_index = dict_param["size_index"] + x = ( + 0 + if params_size[size_index] - 1 == 0 + else indexes[size_index] / (params_size[size_index] - 1) + ) + return (dict_param["last_value"] - dict_param["first_value"]) * x + dict_param[ + "first_value" + ] diff --git a/nnodely/support/jsonutils.py b/nnodely/support/jsonutils.py index ec294fae..f331a939 100644 --- a/nnodely/support/jsonutils.py +++ b/nnodely/support/jsonutils.py @@ -5,47 +5,70 @@ from nnodely.support.utils import check from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.WARNING) + def get_window(obj): - return 'tw' if 'tw' in obj.dim else ('sw' if 'sw' in obj.dim else None) + return "tw" if "tw" in obj.dim else ("sw" if "sw" in obj.dim else None) + # Codice per comprimere le relazioni - #print(self.json['Relations']) - # used_rel = {string for values in self.json['Relations'].values() for string in values[1]} - # if obj1.name not in used_rel and obj1.name in self.json['Relations'].keys() and self.json['Relations'][obj1.name][0] == add_relation_name: - # self.json['Relations'][self.name] = [add_relation_name, self.json['Relations'][obj1.name][1]+[obj2.name]] - # del self.json['Relations'][obj1.name] - # else: - # Devo aggiungere un operazione che rimuove un operazione di Add,Sub,Mul,Div se può essere unita ad un'altra operazione dello stesso tipo - # -def merge(source, destination, main = True): +# print(self.json['Relations']) +# used_rel = {string for values in self.json['Relations'].values() for string in values[1]} +# if obj1.name not in used_rel and obj1.name in self.json['Relations'].keys() and self.json['Relations'][obj1.name][0] == add_relation_name: +# self.json['Relations'][self.name] = [add_relation_name, self.json['Relations'][obj1.name][1]+[obj2.name]] +# del self.json['Relations'][obj1.name] +# else: +# Devo aggiungere un operazione che rimuove un operazione di Add,Sub,Mul,Div se può essere unita ad un'altra operazione dello stesso tipo +# +def merge(source, destination, main=True): if main: for key, value in destination["Functions"].items(): - if key in source["Functions"].keys() and 'n_input' in value.keys() and 'n_input' in source["Functions"][key].keys(): - check(value == {} or source["Functions"][key] == {} or value['n_input'] == source["Functions"][key]['n_input'], - TypeError, - f"The ParamFun {key} is present multiple times, with different number of inputs. " - f"The ParamFun {key} is called with {value['n_input']} parameters and with {source['Functions'][key]['n_input']} parameters.") + if ( + key in source["Functions"].keys() + and "n_input" in value.keys() + and "n_input" in source["Functions"][key].keys() + ): + check( + value == {} + or source["Functions"][key] == {} + or value["n_input"] == source["Functions"][key]["n_input"], + TypeError, + f"The ParamFun {key} is present multiple times, with different number of inputs. " + f"The ParamFun {key} is called with {value['n_input']} parameters and with {source['Functions'][key]['n_input']} parameters.", + ) for key, value in destination["Parameters"].items(): if key in source["Parameters"].keys(): - if 'dim' in value.keys() and 'dim' in source["Parameters"][key].keys(): - check(value['dim'] == source["Parameters"][key]['dim'], - TypeError, - f"The Parameter {key} is present multiple times, with different dimensions. " - f"The Parameter {key} is called with {value['dim']} dimension and with {source['Parameters'][key]['dim']} dimension.") - window_dest = 'tw' if 'tw' in value else ('sw' if 'sw' in value else None) - window_source = 'tw' if 'tw' in source["Parameters"][key] else ('sw' if 'sw' in source["Parameters"][key] else None) + if "dim" in value.keys() and "dim" in source["Parameters"][key].keys(): + check( + value["dim"] == source["Parameters"][key]["dim"], + TypeError, + f"The Parameter {key} is present multiple times, with different dimensions. " + f"The Parameter {key} is called with {value['dim']} dimension and with {source['Parameters'][key]['dim']} dimension.", + ) + window_dest = ( + "tw" if "tw" in value else ("sw" if "sw" in value else None) + ) + window_source = ( + "tw" + if "tw" in source["Parameters"][key] + else ("sw" if "sw" in source["Parameters"][key] else None) + ) if window_dest is not None: - check(window_dest == window_source and value[window_dest] == source["Parameters"][key][window_source] , - TypeError, - f"The Parameter {key} is present multiple times, with different window. " - f"The Parameter {key} is called with {window_dest}={value[window_dest]} dimension and with {window_source}={source['Parameters'][key][window_source]} dimension.") + check( + window_dest == window_source + and value[window_dest] + == source["Parameters"][key][window_source], + TypeError, + f"The Parameter {key} is present multiple times, with different window. " + f"The Parameter {key} is called with {window_dest}={value[window_dest]} dimension and with {window_source}={source['Parameters'][key][window_source]} dimension.", + ) log.debug("Merge Source") - log.debug("\n"+pformat(source)) + log.debug("\n" + pformat(source)) log.debug("Merge Destination") - log.debug("\n"+pformat(destination)) + log.debug("\n" + pformat(destination)) result = copy.deepcopy(destination) else: result = destination @@ -56,7 +79,7 @@ def merge(source, destination, main = True): merge(value, node, False) else: if key in result and type(result[key]) is list: - if key == 'tw' or key == 'sw': + if key == "tw" or key == "sw": if result[key][0] > value[0]: result[key][0] = value[0] if result[key][1] < value[1]: @@ -68,51 +91,77 @@ def merge(source, destination, main = True): log.debug("\n" + pformat(result)) return result + def get_models_json(json): model_json = {} - model_json['Parameters'] = list(json['Parameters'].keys()) - model_json['Constants'] = list(json['Constants'].keys()) - model_json['Inputs'] = list(json['Inputs'].keys()) - model_json['Outputs'] = list(json['Outputs'].keys()) - model_json['Functions'] = list(json['Functions'].keys()) - model_json['Relations'] = list(json['Relations'].keys()) + model_json["Parameters"] = list(json["Parameters"].keys()) + model_json["Constants"] = list(json["Constants"].keys()) + model_json["Inputs"] = list(json["Inputs"].keys()) + model_json["Outputs"] = list(json["Outputs"].keys()) + model_json["Functions"] = list(json["Functions"].keys()) + model_json["Relations"] = list(json["Relations"].keys()) return model_json + def check_model(json): - all_inputs = json['Inputs'].keys() - all_outputs = json['Outputs'].keys() + all_inputs = json["Inputs"].keys() + all_outputs = json["Outputs"].keys() from nnodely.basic.relation import MAIN_JSON + subjson = MAIN_JSON for name in all_outputs: subjson = merge(subjson, subjson_from_output(json, name)) - needed_inputs = subjson['Inputs'].keys() + needed_inputs = subjson["Inputs"].keys() extenal_inputs = set(all_inputs) - set(needed_inputs) - check(all_inputs == needed_inputs, RuntimeError, - f'Connect or close loop operation on the inputs {list(extenal_inputs)}, that are not used in the model.') + check( + all_inputs == needed_inputs, + RuntimeError, + f"Connect or close loop operation on the inputs {list(extenal_inputs)}, that are not used in the model.", + ) return json + def binary_cheks(self, obj1, obj2, name): from nnodely.basic.relation import Stream, toStream - obj1,obj2 = toStream(obj1),toStream(obj2) - check(type(obj1) is Stream,TypeError, - f"The type of {obj1} is {type(obj1)} and is not supported for add operation.") - check(type(obj2) is Stream,TypeError, - f"The type of {obj2} is {type(obj2)} and is not supported for add operation.") + + obj1, obj2 = toStream(obj1), toStream(obj2) + check( + type(obj1) is Stream, + TypeError, + f"The type of {obj1} is {type(obj1)} and is not supported for add operation.", + ) + check( + type(obj2) is Stream, + TypeError, + f"The type of {obj2} is {type(obj2)} and is not supported for add operation.", + ) window_obj1 = get_window(obj1) window_obj2 = get_window(obj2) if window_obj1 is not None and window_obj2 is not None: - check(window_obj1==window_obj2, TypeError, - f"For {name} the time window type must match or None but they were {window_obj1} and {window_obj2}.") - check(obj1.dim[window_obj1] == obj2.dim[window_obj2], ValueError, - f"For {name} the time window must match or None but they were {window_obj1}={obj1.dim[window_obj1]} and {window_obj2}={obj2.dim[window_obj2]}.") - check(obj1.dim['dim'] == obj2.dim['dim'] or obj1.dim == {'dim':1} or obj2.dim == {'dim':1}, ValueError, - f"For {name} the dimension of {obj1.name} = {obj1.dim} must be the same of {obj2.name} = {obj2.dim}.") + check( + window_obj1 == window_obj2, + TypeError, + f"For {name} the time window type must match or None but they were {window_obj1} and {window_obj2}.", + ) + check( + obj1.dim[window_obj1] == obj2.dim[window_obj2], + ValueError, + f"For {name} the time window must match or None but they were {window_obj1}={obj1.dim[window_obj1]} and {window_obj2}={obj2.dim[window_obj2]}.", + ) + check( + obj1.dim["dim"] == obj2.dim["dim"] + or obj1.dim == {"dim": 1} + or obj2.dim == {"dim": 1}, + ValueError, + f"For {name} the dimension of {obj1.name} = {obj1.dim} must be the same of {obj2.name} = {obj2.dim}.", + ) dim = obj1.dim | obj2.dim - dim['dim'] = max(obj1.dim['dim'], obj2.dim['dim']) + dim["dim"] = max(obj1.dim["dim"], obj2.dim["dim"]) return obj1, obj2, dim + def subjson_from_relation(json, relation): json = copy.deepcopy(json) # Get all the inputs needed to compute a specific relation from the json graph @@ -123,159 +172,206 @@ def subjson_from_relation(json, relation): functions = set() def search(rel): - if rel in json['Inputs']: # Found an input + if rel in json["Inputs"]: # Found an input inputs.add(rel) - if rel in json['Inputs']: - if 'connect' in json['Inputs'][rel] and json['Inputs'][rel]['local'] == 1: - search(json['Inputs'][rel]['connect']) - if 'closed_loop' in json['Inputs'][rel] and json['Inputs'][rel]['local'] == 1: - search(json['Inputs'][rel]['closed_loop']) + if rel in json["Inputs"]: + if ( + "connect" in json["Inputs"][rel] + and json["Inputs"][rel]["local"] == 1 + ): + search(json["Inputs"][rel]["connect"]) + if ( + "closed_loop" in json["Inputs"][rel] + and json["Inputs"][rel]["local"] == 1 + ): + search(json["Inputs"][rel]["closed_loop"]) # if 'init' in json['Inputs'][rel]: # search(json['Inputs'][rel]['init']) - elif rel in json['Constants']: # Found a constant or parameter + elif rel in json["Constants"]: # Found a constant or parameter constants.add(rel) - elif rel in json['Parameters']: + elif rel in json["Parameters"]: parameters.add(rel) - elif rel in json['Functions']: + elif rel in json["Functions"]: functions.add(rel) - if 'params_and_consts' in json['Functions'][rel]: - for sub_rel in json['Functions'][rel]['params_and_consts']: + if "params_and_consts" in json["Functions"][rel]: + for sub_rel in json["Functions"][rel]["params_and_consts"]: search(sub_rel) - elif rel in json['Relations']: # Another relation + elif rel in json["Relations"]: # Another relation relations.add(rel) - for sub_rel in json['Relations'][rel][1]: + for sub_rel in json["Relations"][rel][1]: search(sub_rel) - for sub_rel in json['Relations'][rel][2:]: - if json['Relations'][rel][0] in ('Fir', 'Linear'): + for sub_rel in json["Relations"][rel][2:]: + if json["Relations"][rel][0] in ("Fir", "Linear"): search(sub_rel) - if json['Relations'][rel][0] in ('Fuzzify'): + if json["Relations"][rel][0] in ("Fuzzify"): search(sub_rel) - if json['Relations'][rel][0] in ('ParamFun'): + if json["Relations"][rel][0] in ("ParamFun"): search(sub_rel) search(relation) from nnodely.basic.relation import MAIN_JSON + sub_json = copy.deepcopy(MAIN_JSON) - sub_json['Relations'] = {key: value for key, value in json['Relations'].items() if key in relations} - sub_json['Inputs'] = {key: value for key, value in json['Inputs'].items() if key in inputs} - sub_json['Constants'] = {key: value for key, value in json['Constants'].items() if key in constants} - sub_json['Parameters'] = {key: value for key, value in json['Parameters'].items() if key in parameters} - sub_json['Functions'] = {key: value for key, value in json['Functions'].items() if key in functions} - sub_json['Outputs'] = {} - sub_json['Info'] = {} + sub_json["Relations"] = { + key: value for key, value in json["Relations"].items() if key in relations + } + sub_json["Inputs"] = { + key: value for key, value in json["Inputs"].items() if key in inputs + } + sub_json["Constants"] = { + key: value for key, value in json["Constants"].items() if key in constants + } + sub_json["Parameters"] = { + key: value for key, value in json["Parameters"].items() if key in parameters + } + sub_json["Functions"] = { + key: value for key, value in json["Functions"].items() if key in functions + } + sub_json["Outputs"] = {} + sub_json["Info"] = {} return sub_json -def subjson_from_output(json, outputs:str|list): +def subjson_from_output(json, outputs: str | list): json = copy.deepcopy(json) from nnodely.basic.relation import MAIN_JSON + sub_json = copy.deepcopy(MAIN_JSON) if type(outputs) is str: outputs = [outputs] for output in outputs: - sub_json = merge(sub_json, subjson_from_relation(json,json['Outputs'][output])) - sub_json['Outputs'][output] = json['Outputs'][output] + sub_json = merge(sub_json, subjson_from_relation(json, json["Outputs"][output])) + sub_json["Outputs"][output] = json["Outputs"][output] return sub_json -def subjson_from_model(json, models:str|list): + +def subjson_from_model(json, models: str | list): from nnodely.basic.relation import MAIN_JSON + json = copy.deepcopy(json) sub_json = copy.deepcopy(MAIN_JSON) - models_names = set([json['Models']]) if type(json['Models']) is str else set(json['Models'].keys()) + models_names = ( + set([json["Models"]]) + if type(json["Models"]) is str + else set(json["Models"].keys()) + ) if type(models) is str or len(models) == 1: if len(models) == 1: models = models[0] check(models in models_names, AttributeError, f"Model [{models}] not found!") - if type(json['Models']) is str: - outputs = set(json['Outputs'].keys()) + if type(json["Models"]) is str: + outputs = set(json["Outputs"].keys()) else: - outputs = set(json['Models'][models]['Outputs']) - sub_json['Models'] = models + outputs = set(json["Models"][models]["Outputs"]) + sub_json["Models"] = models else: outputs = set() - sub_json['Models'] = {} + sub_json["Models"] = {} for model in models: check(model in models_names, AttributeError, f"Model [{model}] not found!") - outputs |= set(json['Models'][model]['Outputs']) - sub_json['Models'][model] = {key: value for key, value in json['Models'][model].items()} + outputs |= set(json["Models"][model]["Outputs"]) + sub_json["Models"][model] = { + key: value for key, value in json["Models"][model].items() + } # Remove the extern connections not keys in the graph final_json = merge(sub_json, subjson_from_output(json, outputs)) - for key, value in final_json['Inputs'].items(): - if 'connect' in value and (value['local'] == 0 and value['connect'] not in final_json['Relations'].keys()): - del final_json['Inputs'][key]['connect'] - del final_json['Inputs'][key]['local'] - log.warning(f'The input {key} is "connect" outside the model connection removed for subjson') - if 'closedLoop' in value and (value['local'] == 0 and value['closedLoop'] not in final_json['Relations'].keys()): - del final_json['Inputs'][key]['closedLoop'] - del final_json['Inputs'][key]['local'] - log.warning(f'The input {key} is "closedLoop" outside the model connection removed for subjson') + for key, value in final_json["Inputs"].items(): + if "connect" in value and ( + value["local"] == 0 + and value["connect"] not in final_json["Relations"].keys() + ): + del final_json["Inputs"][key]["connect"] + del final_json["Inputs"][key]["local"] + log.warning( + f'The input {key} is "connect" outside the model connection removed for subjson' + ) + if "closedLoop" in value and ( + value["local"] == 0 + and value["closedLoop"] not in final_json["Relations"].keys() + ): + del final_json["Inputs"][key]["closedLoop"] + del final_json["Inputs"][key]["local"] + log.warning( + f'The input {key} is "closedLoop" outside the model connection removed for subjson' + ) return final_json -def subjson_from_minimize(json, minimizers:str|list): + +def subjson_from_minimize(json, minimizers: str | list): from nnodely.basic.relation import MAIN_JSON + json = copy.deepcopy(json) sub_json = copy.deepcopy(MAIN_JSON) - if 'Minimizers' in json: - rel_A = [json['Minimizers'][key]['A'] for key in minimizers] - rel_B = [json['Minimizers'][key]['B'] for key in minimizers] + if "Minimizers" in json: + rel_A = [json["Minimizers"][key]["A"] for key in minimizers] + rel_B = [json["Minimizers"][key]["B"] for key in minimizers] relations_name = set(rel_A) | set(rel_B) for rel_name in relations_name: minimizers_json = subjson_from_relation(json, rel_name) sub_json = merge(sub_json, minimizers_json) - sub_json['Minimizers'] = { key : json['Minimizers'][key] for key in minimizers } + sub_json["Minimizers"] = {key: json["Minimizers"][key] for key in minimizers} return sub_json -def stream_to_str(obj, type = 'Stream'): + +def stream_to_str(obj, type="Stream"): from nnodely.visualizer.emptyvisualizer import color, GREEN from pprint import pformat + stream = f" {type} " stream_name = f" {obj.name} {obj.dim} " - title = color((stream).center(80, '='), GREEN, True) + title = color((stream).center(80, "="), GREEN, True) json = color(pformat(obj.json), GREEN) - stream = color((stream_name).center(80, '-'), GREEN, True) - return title + '\n' + json + '\n' + stream - -def plot_structure(json, filename='nnodely_graph', library='matplotlib', view=True): - #json = self.modely.json if json is None else json - # if json is None: - # raise ValueError("No JSON model definition provided. Please provide a valid JSON model definition.") - if library not in ['matplotlib', 'graphviz']: - raise ValueError("Invalid library specified. Use 'matplotlib' or 'graphviz'.") - if library == 'matplotlib': - plot_matplotlib_structure(json, filename, view=view) - elif library == 'graphviz': - plot_graphviz_structure(json, filename, view=view) - -def plot_matplotlib_structure(json, filename='nnodely_graph', view=True): + stream = color((stream_name).center(80, "-"), GREEN, True) + return title + "\n" + json + "\n" + stream + + +def plot_structure(json, filename="nnodely_graph", library="matplotlib", view=True): + # json = self.modely.json if json is None else json + # if json is None: + # raise ValueError("No JSON model definition provided. Please provide a valid JSON model definition.") + if library not in ["matplotlib", "graphviz"]: + raise ValueError("Invalid library specified. Use 'matplotlib' or 'graphviz'.") + if library == "matplotlib": + plot_matplotlib_structure(json, filename, view=view) + elif library == "graphviz": + plot_graphviz_structure(json, filename, view=view) + + +def plot_matplotlib_structure(json, filename="nnodely_graph", view=True): import matplotlib.pyplot as plt from matplotlib import patches from matplotlib.lines import Line2D + layer_positions = {} x, y = 0, 0 # Initial position dy, dx = 1.5, 2.5 # Spacing - ## Layer Inputs: - for input_name, input_type in json['Inputs'].items(): + ## Layer Inputs: + for input_name, input_type in json["Inputs"].items(): layer_positions[input_name] = (x, y) y -= dy - for constant_name in json['Constants'].keys(): + for constant_name in json["Constants"].keys(): layer_positions[constant_name] = (x, y) y -= dy y_limit = abs(y) # Layers Relations: - available_inputs = list(json['Inputs'].keys() | json['Constants'].keys()) - available_outputs = list(set(json['Outputs'].values())) + available_inputs = list(json["Inputs"].keys() | json["Constants"].keys()) + available_outputs = list(set(json["Outputs"].values())) while available_outputs: x += dx y = 0 inputs_to_add, outputs_to_remove = [], [] - for relation_name, (relation_type, dependencies, *_) in json['Relations'].items(): - if all(dep in available_inputs for dep in dependencies) and (relation_name not in available_inputs): + for relation_name, (relation_type, dependencies, *_) in json[ + "Relations" + ].items(): + if all(dep in available_inputs for dep in dependencies) and ( + relation_name not in available_inputs + ): inputs_to_add.append(relation_name) if relation_name in available_outputs: outputs_to_remove.append(relation_name) @@ -283,12 +379,14 @@ def plot_matplotlib_structure(json, filename='nnodely_graph', view=True): y -= dy y_limit = max(y_limit, abs(y)) available_inputs.extend(inputs_to_add) - available_outputs = [out for out in available_outputs if out not in outputs_to_remove] + available_outputs = [ + out for out in available_outputs if out not in outputs_to_remove + ] - ## Layer Outputs: + ## Layer Outputs: x += dx y = 0 - for idx, output_name in enumerate(json['Outputs'].keys()): + for idx, output_name in enumerate(json["Outputs"].keys()): layer_positions[output_name] = (x, y) y -= dy # Move down for the next input x_limit = abs(x) @@ -296,79 +394,165 @@ def plot_matplotlib_structure(json, filename='nnodely_graph', view=True): # Create the plot fig, ax = plt.subplots(figsize=(x_limit, y_limit)) - #fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05) + # fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05) # Plot rectangles for each layer - colors, labels = ['lightgreen', 'lightblue', 'orange', 'lightgray'], ['Inputs', 'Relations', 'Outputs', 'Constants'] - legend_info = [patches.Patch(facecolor=color, edgecolor='black', label=label) for color, label in zip(colors, labels)] - for layer in (json['Inputs'].keys() | json['Outputs'].keys() | json['Relations'].keys() | json['Constants'].keys()): + colors, labels = ( + ["lightgreen", "lightblue", "orange", "lightgray"], + ["Inputs", "Relations", "Outputs", "Constants"], + ) + legend_info = [ + patches.Patch(facecolor=color, edgecolor="black", label=label) + for color, label in zip(colors, labels) + ] + for layer in ( + json["Inputs"].keys() + | json["Outputs"].keys() + | json["Relations"].keys() + | json["Constants"].keys() + ): x1, y1 = layer_positions[layer] - if layer in json['Inputs'].keys(): - color = 'lightgreen' - tag = f'{layer}\ndim: {json["Inputs"][layer]["dim"]}\nWindow: {json["Inputs"][layer]["ntot"]}' - elif layer in json['Outputs'].keys(): - color = 'orange' + if layer in json["Inputs"].keys(): + color = "lightgreen" + tag = f"{layer}\ndim: {json['Inputs'][layer]['dim']}\nWindow: {json['Inputs'][layer]['ntot']}" + elif layer in json["Outputs"].keys(): + color = "orange" tag = layer - elif layer in json['Constants'].keys(): - color = 'lightgray' - tag = f'{layer}\ndim: {json["Constants"][layer]["dim"]}' + elif layer in json["Constants"].keys(): + color = "lightgray" + tag = f"{layer}\ndim: {json['Constants'][layer]['dim']}" else: - color = 'lightblue' - tag = f'{json["Relations"][layer][0]}\n({layer})' - rect = patches.Rectangle((x1, y1), 2, 1, edgecolor='black', facecolor=color) + color = "lightblue" + tag = f"{json['Relations'][layer][0]}\n({layer})" + rect = patches.Rectangle((x1, y1), 2, 1, edgecolor="black", facecolor=color) ax.add_patch(rect) - ax.text(x1 + 1, y1 + 0.5, f"{tag}", ha='center', va='center', fontsize=8, fontweight='bold') + ax.text( + x1 + 1, + y1 + 0.5, + f"{tag}", + ha="center", + va="center", + fontsize=8, + fontweight="bold", + ) # Draw arrows for dependencies - for layer, (_, dependencies, *_) in json['Relations'].items(): + for layer, (_, dependencies, *_) in json["Relations"].items(): x1, y1 = layer_positions[layer] # Get position of the current layer for dep in dependencies: if dep in layer_positions: x2, y2 = layer_positions[dep] # Get position of the dependent layer - ax.annotate("", xy=(x1, y1), xytext=(x2 + 2, y2 + 0.5), arrowprops=dict(arrowstyle="->", color='black', lw=1)) - for out_name, rel_name in json['Outputs'].items(): + ax.annotate( + "", + xy=(x1, y1), + xytext=(x2 + 2, y2 + 0.5), + arrowprops=dict(arrowstyle="->", color="black", lw=1), + ) + for out_name, rel_name in json["Outputs"].items(): x1, y1 = layer_positions[out_name] x2, y2 = layer_positions[rel_name] - ax.annotate("", xy=(x1, y1 + 0.5), xytext=(x2 + 2, y2 + 0.5), - arrowprops=dict(arrowstyle="->", color='black', lw=1)) - for key, state in json['Inputs'].items(): - if 'closedLoop' in state.keys(): + ax.annotate( + "", + xy=(x1, y1 + 0.5), + xytext=(x2 + 2, y2 + 0.5), + arrowprops=dict(arrowstyle="->", color="black", lw=1), + ) + for key, state in json["Inputs"].items(): + if "closedLoop" in state.keys(): x1, y1 = layer_positions[key] - x2, y2 = layer_positions[state['closedLoop']] - #ax.annotate("", xy=(x2+1, y2), xytext=(x2+1, y_limit), arrowprops=dict(arrowstyle="-", color='red', lw=1, linestyle='dashed')) - ax.add_patch(patches.FancyArrowPatch((x2+1, y2), (x2+1, -y_limit), arrowstyle='-', mutation_scale=15, color='red', linestyle='dashed')) - ax.add_patch(patches.FancyArrowPatch((x2+1, -y_limit), (x1-1, -y_limit), arrowstyle='-', mutation_scale=15, color='red', linestyle='dashed')) - ax.add_patch(patches.FancyArrowPatch((x1-1, -y_limit), (x1-1, y1+0.5), arrowstyle='-', mutation_scale=15, color='red', linestyle='dashed')) - ax.add_patch(patches.FancyArrowPatch((x1-1, y1+0.5), (x1, y1+0.5), arrowstyle='->', mutation_scale=15, color='red', linestyle='dashed')) - elif 'connect' in state.keys(): + x2, y2 = layer_positions[state["closedLoop"]] + # ax.annotate("", xy=(x2+1, y2), xytext=(x2+1, y_limit), arrowprops=dict(arrowstyle="-", color='red', lw=1, linestyle='dashed')) + ax.add_patch( + patches.FancyArrowPatch( + (x2 + 1, y2), + (x2 + 1, -y_limit), + arrowstyle="-", + mutation_scale=15, + color="red", + linestyle="dashed", + ) + ) + ax.add_patch( + patches.FancyArrowPatch( + (x2 + 1, -y_limit), + (x1 - 1, -y_limit), + arrowstyle="-", + mutation_scale=15, + color="red", + linestyle="dashed", + ) + ) + ax.add_patch( + patches.FancyArrowPatch( + (x1 - 1, -y_limit), + (x1 - 1, y1 + 0.5), + arrowstyle="-", + mutation_scale=15, + color="red", + linestyle="dashed", + ) + ) + ax.add_patch( + patches.FancyArrowPatch( + (x1 - 1, y1 + 0.5), + (x1, y1 + 0.5), + arrowstyle="->", + mutation_scale=15, + color="red", + linestyle="dashed", + ) + ) + elif "connect" in state.keys(): x1, y1 = layer_positions[key] - x2, y2 = layer_positions[state['connect']] - ax.add_patch(patches.FancyArrowPatch((x1, y1), (x2, y2), arrowstyle='->', mutation_scale=15, color='green', linestyle='dashed')) - - legend_info.extend([Line2D([0], [0], color='black', lw=2, label='Dependency'), - Line2D([0], [0], color='red', lw=2, linestyle='dashed', label='Closed Loop'), - Line2D([0], [0], color='green', lw=2, linestyle='dashed', label='Connect')]) + x2, y2 = layer_positions[state["connect"]] + ax.add_patch( + patches.FancyArrowPatch( + (x1, y1), + (x2, y2), + arrowstyle="->", + mutation_scale=15, + color="green", + linestyle="dashed", + ) + ) + + legend_info.extend( + [ + Line2D([0], [0], color="black", lw=2, label="Dependency"), + Line2D( + [0], [0], color="red", lw=2, linestyle="dashed", label="Closed Loop" + ), + Line2D([0], [0], color="green", lw=2, linestyle="dashed", label="Connect"), + ] + ) # Adjust the plot limits - ax.set_xlim(-dx, x_limit+dx) + ax.set_xlim(-dx, x_limit + dx) ax.set_ylim(-y_limit, dy) - ax.set_aspect('equal') - ax.legend(handles=legend_info, loc='lower right') - ax.axis('off') # Hide axes - - plt.title(f"Neural Network Diagram - Sampling [{json['Info']['SampleTime']}]", fontsize=12, fontweight='bold') + ax.set_aspect("equal") + ax.legend(handles=legend_info, loc="lower right") + ax.axis("off") # Hide axes + + plt.title( + f"Neural Network Diagram - Sampling [{json['Info']['SampleTime']}]", + fontsize=12, + fontweight="bold", + ) ## Save the figure - plt.savefig(filename, format="png", bbox_inches='tight') + plt.savefig(filename, format="png", bbox_inches="tight") if view: plt.show() -def plot_graphviz_structure(json, filename='nnodely_graph', view=True): # pragma: no cover + +def plot_graphviz_structure( + json, filename="nnodely_graph", view=True +): # pragma: no cover import shutil from graphviz import view from graphviz import Digraph # Check if Graphviz is installed - if shutil.which('dot') is None: + if shutil.which("dot") is None: # raise RuntimeError( # "Graphviz does not appear to be installed on your system. " # "Please install it from https://graphviz.org/download/" @@ -378,71 +562,104 @@ def plot_graphviz_structure(json, filename='nnodely_graph', view=True): # pragma "Please install it from https://graphviz.org/download/" ) return - - dot = Digraph(comment='Structured Neural Network') + + dot = Digraph(comment="Structured Neural Network") # Set graph attributes for top-down layout and style - dot.attr(rankdir='LR', size='21') - dot.attr('node', shape='box', style='filled', color='lightgray', fontname='Helvetica') + dot.attr(rankdir="LR", size="21") + dot.attr( + "node", shape="box", style="filled", color="lightgray", fontname="Helvetica" + ) # Add metadata/info box - if 'Info' in json: - info = json['Info'] - info_text = '\n'.join([f"{k}: {v}" for k, v in info.items()]) - dot.node('INFO_BOX', label=f"Model Info\n{info_text}", shape='note', fillcolor='white', fontsize='10') + if "Info" in json: + info = json["Info"] + info_text = "\n".join([f"{k}: {v}" for k, v in info.items()]) + dot.node( + "INFO_BOX", + label=f"Model Info\n{info_text}", + shape="note", + fillcolor="white", + fontsize="10", + ) # Add input nodes - for inp, data in json['Inputs'].items(): - dim = data['dim'] - window = data['sw'] if 'sw' in data else data['tw'] - window_tag = 'sw' if 'sw' in data else 'tw' + for inp, data in json["Inputs"].items(): + dim = data["dim"] + window = data["sw"] if "sw" in data else data["tw"] + window_tag = "sw" if "sw" in data else "tw" label = f"{inp}\nDim: {dim}\nWindow({window_tag}): {window}" - dot.node(inp, label=label, fillcolor='lightgreen') - if 'connect' in data.keys(): - dot.edge(data['connect'], inp, label='connect', color='blue', fontcolor='blue') - if 'closedLoop' in data.keys(): - dot.edge(data['closedLoop'], inp, label='closedLoop', color='red', fontcolor='red') + dot.node(inp, label=label, fillcolor="lightgreen") + if "connect" in data.keys(): + dot.edge( + data["connect"], inp, label="connect", color="blue", fontcolor="blue" + ) + if "closedLoop" in data.keys(): + dot.edge( + data["closedLoop"], + inp, + label="closedLoop", + color="red", + fontcolor="red", + ) # Add constant nodes - if 'Constants' in json: - for const, data in json['Constants'].items(): - dim = data['dim'] + if "Constants" in json: + for const, data in json["Constants"].items(): + dim = data["dim"] label = f"{const}\nDim: {dim}" - dot.node(const, label=label, fillcolor='lightgray') + dot.node(const, label=label, fillcolor="lightgray") # Add relation nodes - for name, rel in json['Relations'].items(): + for name, rel in json["Relations"].items(): op_type = rel[0] parents = rel[1] param1 = rel[2] if len(rel) > 2 else None param2 = rel[3] if len(rel) > 3 else None label = f"{name}\nType: {op_type}" - dot.node(name, label=label, fillcolor='lightblue') - for i in [param1,param2]: + dot.node(name, label=label, fillcolor="lightblue") + for i in [param1, param2]: if isinstance(i, str): - if i in json['Parameters']: - param_dim = json['Parameters'][i]['dim'] - dot.node(i, label=f"{i}\nDim: {param_dim}", shape='ellipse', fillcolor='orange') - dot.edge(i, name, label='Parameter', color='orange', fontcolor='orange') - elif i in json['Functions']: - dot.node(i, label=f"{param1}", shape='ellipse', fillcolor='darkorange') - dot.edge(i, name, label='function', color='darkorange', fontcolor='darkorange') + if i in json["Parameters"]: + param_dim = json["Parameters"][i]["dim"] + dot.node( + i, + label=f"{i}\nDim: {param_dim}", + shape="ellipse", + fillcolor="orange", + ) + dot.edge( + i, name, label="Parameter", color="orange", fontcolor="orange" + ) + elif i in json["Functions"]: + dot.node( + i, label=f"{param1}", shape="ellipse", fillcolor="darkorange" + ) + dot.edge( + i, + name, + label="function", + color="darkorange", + fontcolor="darkorange", + ) for parent in parents: dot.edge(parent, name) # Add output nodes - for out, rel in json['Outputs'].items(): - dot.node(out, fillcolor='lightcoral') + for out, rel in json["Outputs"].items(): + dot.node(out, fillcolor="lightcoral") dot.edge(rel, out) # Add Minimize nodes if present - if 'Minimizers' in json: - for name, rel in json['Minimizers'].items(): - rel_a, rel_b = rel['A'], rel['B'] - loss = rel['loss'] - dot.node(name, label=f"{name}\nLoss:{loss}", shape='ellipse', fillcolor='purple') - dot.edge(rel_a, name, label='Minimize', color='purple', fontcolor='purple') - dot.edge(rel_b, name, label='Minimize', color='purple', fontcolor='purple') + if "Minimizers" in json: + for name, rel in json["Minimizers"].items(): + rel_a, rel_b = rel["A"], rel["B"] + loss = rel["loss"] + dot.node( + name, label=f"{name}\nLoss:{loss}", shape="ellipse", fillcolor="purple" + ) + dot.edge(rel_a, name, label="Minimize", color="purple", fontcolor="purple") + dot.edge(rel_b, name, label="Minimize", color="purple", fontcolor="purple") # Add a legend as a subgraph # with dot.subgraph(name='cluster_legend') as legend: @@ -456,4 +673,6 @@ def plot_graphviz_structure(json, filename='nnodely_graph', view=True): # pragma # legend.edge('LegendRel', 'LegendOutput') # Render the graph - dot.render(filename=filename, view=view, format='svg') # opens in default viewer and saves as SVG \ No newline at end of file + dot.render( + filename=filename, view=view, format="svg" + ) # opens in default viewer and saves as SVG diff --git a/nnodely/support/logger.py b/nnodely/support/logger.py index a94f3f21..f519fbe9 100644 --- a/nnodely/support/logger.py +++ b/nnodely/support/logger.py @@ -3,9 +3,9 @@ BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) -#The background is set with 40 plus the number of the color, and the foreground with 30 +# The background is set with 40 plus the number of the color, and the foreground with 30 -#These are the sequences need to get colored ouput +# These are the sequences need to get colored ouput RESET_SEQ = "\033[0m" COLOR_SEQ = "\033[%dm" COLOR_BOLD_SEQ = "\033[1;%dm" @@ -18,7 +18,7 @@ logging.INFO: BLUE, logging.WARNING: YELLOW, logging.CRITICAL: RED, - logging.ERROR: RED + logging.ERROR: RED, } LEVEL_STRING = { logging.DEBUG: "DEBUG", @@ -26,15 +26,16 @@ logging.WARNING: "WARNING", logging.CRITICAL: "CRITICAL", logging.ERROR: "ERROR", - SUPPRESS: "SUPPRESS" + SUPPRESS: "SUPPRESS", } LOG_LEVEL = logging.INFO class JsonFormatter(logging.Formatter): - FORMAT = "[%(levelname)s][%(name)s:%(filename)s:%(funcName)s:%(lineno)d] %(message)s" # + "" + FORMAT = "[%(levelname)s][%(name)s:%(filename)s:%(funcName)s:%(lineno)d] %(message)s" # + "" FORMAT_WARNING = "[%(funcName)s] %(message)s" FORMAT_INFO = "%(message)s" + def __init__(self): logging.Formatter.__init__(self, self.FORMAT) @@ -54,39 +55,49 @@ def format(self, record): class nnLogger(logging.Logger): levels = [] loggers = [] - params = {'level':None} + params = {"level": None} + def __init__(self, name, level): logging.Logger.__init__(self, name) self.setLevel(max(level, LOG_LEVEL)) - #file = logging.FileHandler('example.log') - #color_formatter = ColoredFormatter(self.COLOR_FORMAT) + # file = logging.FileHandler('example.log') + # color_formatter = ColoredFormatter(self.COLOR_FORMAT) self.console = logging.StreamHandler(sys.stdout) color_formatter = JsonFormatter() self.console.setFormatter(color_formatter) - #self.console.setLevel(logging.CRITICAL) + # self.console.setLevel(logging.CRITICAL) - #logging.getLogger().addHandler(self.console) + # logging.getLogger().addHandler(self.console) self.addHandler(self.console) self.loggers.append(self) self.levels.append(level) - #self.addHandler(file) + # self.addHandler(file) def setAllLevel(self, level): - if self.params['level'] is None or self.params['level'] != level: - self._log(logging.INFO, - COLOR_SEQ % (30 + BLUE) + (f" Loggers to {LEVEL_STRING[level]} ").center(80, '=') + RESET_SEQ, None) - self.params['level'] = level + if self.params["level"] is None or self.params["level"] != level: + self._log( + logging.INFO, + COLOR_SEQ % (30 + BLUE) + + (f" Loggers to {LEVEL_STRING[level]} ").center(80, "=") + + RESET_SEQ, + None, + ) + self.params["level"] = level for ind, logger in enumerate(self.loggers): logger.setLevel(level) def resetAllLevel(self): - if self.params['level'] != 0: - self._log(logging.INFO, COLOR_SEQ % (30 + BLUE) + (" Standard Level Log ").center(80, '=') + RESET_SEQ, None) - self.params['level'] = None + if self.params["level"] != 0: + self._log( + logging.INFO, + COLOR_SEQ % (30 + BLUE) + + (" Standard Level Log ").center(80, "=") + + RESET_SEQ, + None, + ) + self.params["level"] = None for ind, logger in enumerate(self.loggers): logger.setLevel(self.levels[ind]) - - diff --git a/nnodely/support/mathutils.py b/nnodely/support/mathutils.py index 6e6dca45..0663b1de 100644 --- a/nnodely/support/mathutils.py +++ b/nnodely/support/mathutils.py @@ -1,17 +1,22 @@ import torch + def argmax_max(iterable): return max(enumerate(iterable), key=lambda x: x[1]) + def argmin_min(iterable): return min(enumerate(iterable), key=lambda x: x[1]) + def argmax_dict(iterable: dict): return max(iterable.items(), key=lambda x: x[1]) + def argmin_dict(iterable: dict): return min(iterable.items(), key=lambda x: x[1]) + # Linear interpolation function, operating on batches of input data and returning batches of output data def linear_interp(x, x_data, y_data): # Inputs: @@ -28,5 +33,7 @@ def linear_interp(x, x_data, y_data): idx = torch.argmin(torch.abs(x_data[:-1] - x), dim=1) # Linear interpolation - y = y_data[idx] + (y_data[idx + 1] - y_data[idx]) / (x_data[idx + 1] - x_data[idx]) * (x - x_data[idx]) - return y \ No newline at end of file + y = y_data[idx] + (y_data[idx + 1] - y_data[idx]) / ( + x_data[idx + 1] - x_data[idx] + ) * (x - x_data[idx]) + return y diff --git a/nnodely/support/odeint/adjoint.py b/nnodely/support/odeint/adjoint.py index b68b7f14..f3525704 100644 --- a/nnodely/support/odeint/adjoint.py +++ b/nnodely/support/odeint/adjoint.py @@ -2,14 +2,35 @@ import torch import torch.nn as nn from nnodely.support.odeint.my_odeint import SOLVERS, odeint -from nnodely.support.odeint.utils import _check_inputs, _flat_to_shape, _mixed_norm, _all_callback_names, _all_adjoint_callback_names +from nnodely.support.odeint.utils import ( + _check_inputs, + _flat_to_shape, + _mixed_norm, + _all_callback_names, + _all_adjoint_callback_names, +) class OdeintAdjointMethod(torch.autograd.Function): - @staticmethod - def forward(ctx, shapes, func, y0, t, rtol, atol, method, options, event_fn, adjoint_rtol, adjoint_atol, adjoint_method, - adjoint_options, t_requires_grad, *adjoint_params): + def forward( + ctx, + shapes, + func, + y0, + t, + rtol, + atol, + method, + options, + event_fn, + adjoint_rtol, + adjoint_atol, + adjoint_method, + adjoint_options, + t_requires_grad, + *adjoint_params, + ): ctx.shapes = shapes ctx.func = func @@ -21,7 +42,16 @@ def forward(ctx, shapes, func, y0, t, rtol, atol, method, options, event_fn, adj ctx.event_mode = event_fn is not None with torch.no_grad(): - ans = odeint(func, y0, t, rtol=rtol, atol=atol, method=method, options=options, event_fn=event_fn) + ans = odeint( + func, + y0, + t, + rtol=rtol, + atol=atol, + method=method, + options=options, + event_fn=event_fn, + ) if event_fn is None: y = ans @@ -61,8 +91,14 @@ def backward(ctx, *grad_y): ################################## # [-1] because y and grad_y are both of shape (len(t), *y0.shape) - aug_state = [torch.zeros((), dtype=y.dtype, device=y.device), y[-1], grad_y[-1]] # vjp_t, y, vjp_y - aug_state.extend([torch.zeros_like(param) for param in adjoint_params]) # vjp_params + aug_state = [ + torch.zeros((), dtype=y.dtype, device=y.device), + y[-1], + grad_y[-1], + ] # vjp_t, y, vjp_y + aug_state.extend( + [torch.zeros_like(param) for param in adjoint_params] + ) # vjp_params ################################## # Set up backward ODE func # @@ -89,23 +125,32 @@ def augmented_dynamics(t, y_aug): # Workaround for PyTorch bug #39784 _t = torch.as_strided(t, (), ()) # noqa _y = torch.as_strided(y, (), ()) # noqa - _params = tuple(torch.as_strided(param, (), ()) for param in adjoint_params) # noqa + _params = tuple( + torch.as_strided(param, (), ()) for param in adjoint_params + ) # noqa vjp_t, vjp_y, *vjp_params = torch.autograd.grad( - func_eval, (t, y) + adjoint_params, -adj_y, - allow_unused=True, retain_graph=True + func_eval, + (t, y) + adjoint_params, + -adj_y, + allow_unused=True, + retain_graph=True, ) # autograd.grad returns None if no gradient, set to zero. vjp_t = torch.zeros_like(t) if vjp_t is None else vjp_t vjp_y = torch.zeros_like(y) if vjp_y is None else vjp_y - vjp_params = [torch.zeros_like(param) if vjp_param is None else vjp_param - for param, vjp_param in zip(adjoint_params, vjp_params)] + vjp_params = [ + torch.zeros_like(param) if vjp_param is None else vjp_param + for param, vjp_param in zip(adjoint_params, vjp_params) + ] return (vjp_t, func_eval, vjp_y, *vjp_params) # Add adjoint callbacks - for callback_name, adjoint_callback_name in zip(_all_callback_names, _all_adjoint_callback_names): + for callback_name, adjoint_callback_name in zip( + _all_callback_names, _all_adjoint_callback_names + ): try: callback = getattr(func, adjoint_callback_name) except AttributeError: @@ -132,36 +177,78 @@ def augmented_dynamics(t, y_aug): # Run the augmented system backwards in time. aug_state = odeint( - augmented_dynamics, tuple(aug_state), - t[i - 1:i + 1].flip(0), - rtol=adjoint_rtol, atol=adjoint_atol, method=adjoint_method, options=adjoint_options + augmented_dynamics, + tuple(aug_state), + t[i - 1 : i + 1].flip(0), + rtol=adjoint_rtol, + atol=adjoint_atol, + method=adjoint_method, + options=adjoint_options, ) aug_state = [a[1] for a in aug_state] # extract just the t[i - 1] value - aug_state[1] = y[i - 1] # update to use our forward-pass estimate of the state - aug_state[2] += grad_y[i - 1] # update any gradients wrt state at this time point + aug_state[1] = y[ + i - 1 + ] # update to use our forward-pass estimate of the state + aug_state[2] += grad_y[ + i - 1 + ] # update any gradients wrt state at this time point if t_requires_grad: time_vjps[0] = aug_state[0] # Only compute gradient wrt initial time when in event handling mode. if event_mode and t_requires_grad: - time_vjps = torch.cat([time_vjps[0].reshape(-1), torch.zeros_like(_t[1:])]) + time_vjps = torch.cat( + [time_vjps[0].reshape(-1), torch.zeros_like(_t[1:])] + ) adj_y = aug_state[2] adj_params = aug_state[3:] - return (None, None, adj_y, time_vjps, None, None, None, None, None, None, None, None, None, None, *adj_params) - - -def odeint_adjoint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, event_fn=None, - adjoint_rtol=None, adjoint_atol=None, adjoint_method=None, adjoint_options=None, adjoint_params=None): + return ( + None, + None, + adj_y, + time_vjps, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + *adj_params, + ) + + +def odeint_adjoint( + func, + y0, + t, + *, + rtol=1e-7, + atol=1e-9, + method=None, + options=None, + event_fn=None, + adjoint_rtol=None, + adjoint_atol=None, + adjoint_method=None, + adjoint_options=None, + adjoint_params=None, +): # We need this in order to access the variables inside this module, # since we have no other way of getting variables along the execution path. if adjoint_params is None and not isinstance(func, nn.Module): - raise ValueError('func must be an instance of nn.Module to specify the adjoint parameters; alternatively they ' - 'can be specified explicitly via the `adjoint_params` argument. If there are no parameters ' - 'then it is allowable to set `adjoint_params=()`.') + raise ValueError( + "func must be an instance of nn.Module to specify the adjoint parameters; alternatively they " + "can be specified explicitly via the `adjoint_params` argument. If there are no parameters " + "then it is allowable to set `adjoint_params=()`." + ) # Must come before _check_inputs as we don't want to use normalised input (in particular any changes to options) if adjoint_rtol is None: @@ -172,11 +259,17 @@ def odeint_adjoint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=No adjoint_method = method if adjoint_method != method and options is not None and adjoint_options is None: - raise ValueError("If `adjoint_method != method` then we cannot infer `adjoint_options` from `options`. So as " - "`options` has been passed then `adjoint_options` must be passed as well.") + raise ValueError( + "If `adjoint_method != method` then we cannot infer `adjoint_options` from `options`. So as " + "`options` has been passed then `adjoint_options` must be passed as well." + ) if adjoint_options is None: - adjoint_options = {k: v for k, v in options.items() if k != "norm"} if options is not None else {} + adjoint_options = ( + {k: v for k, v in options.items() if k != "norm"} + if options is not None + else {} + ) else: # Avoid in-place modifying a user-specified dict. adjoint_options = adjoint_options.copy() @@ -192,19 +285,38 @@ def odeint_adjoint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=No if len(adjoint_params) != oldlen_: # Some params were excluded. # Issue a warning if a user-specified norm is specified. - if 'norm' in adjoint_options and callable(adjoint_options['norm']): - warnings.warn("An adjoint parameter was passed without requiring gradient. For efficiency this will be " - "excluded from the adjoint pass, and will not appear as a tensor in the adjoint norm.") + if "norm" in adjoint_options and callable(adjoint_options["norm"]): + warnings.warn( + "An adjoint parameter was passed without requiring gradient. For efficiency this will be " + "excluded from the adjoint pass, and will not appear as a tensor in the adjoint norm." + ) # Convert to flattened state. - shapes, func, y0, t, rtol, atol, method, options, event_fn, decreasing_time = _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS) + shapes, func, y0, t, rtol, atol, method, options, event_fn, decreasing_time = ( + _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS) + ) # Handle the adjoint norm function. state_norm = options["norm"] handle_adjoint_norm_(adjoint_options, shapes, state_norm) - ans = OdeintAdjointMethod.apply(shapes, func, y0, t, rtol, atol, method, options, event_fn, adjoint_rtol, adjoint_atol, - adjoint_method, adjoint_options, t.requires_grad, *adjoint_params) + ans = OdeintAdjointMethod.apply( + shapes, + func, + y0, + t, + rtol, + atol, + method, + options, + event_fn, + adjoint_rtol, + adjoint_atol, + adjoint_method, + adjoint_options, + t.requires_grad, + *adjoint_params, + ) if event_fn is None: solution = ans @@ -228,10 +340,14 @@ def find_parameters(module): assert isinstance(module, nn.Module) # If called within DataParallel, parameters won't appear in module.parameters(). - if getattr(module, '_is_replica', False): + if getattr(module, "_is_replica", False): def find_tensor_attributes(module): - tuples = [(k, v) for k, v in module.__dict__.items() if torch.is_tensor(v) and v.requires_grad] + tuples = [ + (k, v) + for k, v in module.__dict__.items() + if torch.is_tensor(v) and v.requires_grad + ] return tuples gen = module._named_members(get_members_fn=find_tensor_attributes) @@ -255,20 +371,21 @@ def default_adjoint_norm(tensor_tuple): else: # `adjoint_options` was explicitly specified by the user... try: - adjoint_norm = adjoint_options['norm'] + adjoint_norm = adjoint_options["norm"] except KeyError: # ...but they did not specify the norm argument. Back to plan A: use the default norm. - adjoint_options['norm'] = default_adjoint_norm + adjoint_options["norm"] = default_adjoint_norm else: # ...and they did specify the norm argument. - if adjoint_norm == 'seminorm': + if adjoint_norm == "seminorm": # They told us they want to use seminorms. Slight modification to plan A: use the default norm, # but ignore the parameter state def adjoint_seminorm(tensor_tuple): t, y, adj_y, *adj_params = tensor_tuple # (If the state is actually a flattened tuple then this will be unpacked again in state_norm.) return max(t.abs(), state_norm(y), state_norm(adj_y)) - adjoint_options['norm'] = adjoint_seminorm + + adjoint_options["norm"] = adjoint_seminorm else: # And they're using their own custom norm. if shapes is None: @@ -285,4 +402,5 @@ def _adjoint_norm(tensor_tuple): y = _flat_to_shape(y, (), shapes) adj_y = _flat_to_shape(adj_y, (), shapes) return adjoint_norm((t, *y, *adj_y, *adj_params)) - adjoint_options['norm'] = _adjoint_norm \ No newline at end of file + + adjoint_options["norm"] = _adjoint_norm diff --git a/nnodely/support/odeint/dopri5.py b/nnodely/support/odeint/dopri5.py index 9d6e3024..6506e280 100644 --- a/nnodely/support/odeint/dopri5.py +++ b/nnodely/support/odeint/dopri5.py @@ -1,35 +1,60 @@ import torch -from nnodely.support.odeint.rk_solvers import _ButcherTableau, RKAdaptiveStepsizeODESolver +from nnodely.support.odeint.rk_solvers import ( + _ButcherTableau, + RKAdaptiveStepsizeODESolver, +) _DORMAND_PRINCE_SHAMPINE_TABLEAU = _ButcherTableau( - alpha=torch.tensor([1 / 5, 3 / 10, 4 / 5, 8 / 9, 1., 1.], dtype=torch.float32), + alpha=torch.tensor([1 / 5, 3 / 10, 4 / 5, 8 / 9, 1.0, 1.0], dtype=torch.float32), beta=[ torch.tensor([1 / 5], dtype=torch.float32), torch.tensor([3 / 40, 9 / 40], dtype=torch.float32), torch.tensor([44 / 45, -56 / 15, 32 / 9], dtype=torch.float32), - torch.tensor([19372 / 6561, -25360 / 2187, 64448 / 6561, -212 / 729], dtype=torch.float32), - torch.tensor([9017 / 3168, -355 / 33, 46732 / 5247, 49 / 176, -5103 / 18656], dtype=torch.float32), - torch.tensor([35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84], dtype=torch.float32), + torch.tensor( + [19372 / 6561, -25360 / 2187, 64448 / 6561, -212 / 729], dtype=torch.float32 + ), + torch.tensor( + [9017 / 3168, -355 / 33, 46732 / 5247, 49 / 176, -5103 / 18656], + dtype=torch.float32, + ), + torch.tensor( + [35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84], + dtype=torch.float32, + ), ], - c_sol=torch.tensor([35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84, 0], dtype=torch.float32), - c_error=torch.tensor([ - 35 / 384 - 1951 / 21600, - 0, - 500 / 1113 - 22642 / 50085, - 125 / 192 - 451 / 720, - -2187 / 6784 - -12231 / 42400, - 11 / 84 - 649 / 6300, - -1. / 60., - ], dtype=torch.float32), + c_sol=torch.tensor( + [35 / 384, 0, 500 / 1113, 125 / 192, -2187 / 6784, 11 / 84, 0], + dtype=torch.float32, + ), + c_error=torch.tensor( + [ + 35 / 384 - 1951 / 21600, + 0, + 500 / 1113 - 22642 / 50085, + 125 / 192 - 451 / 720, + -2187 / 6784 - -12231 / 42400, + 11 / 84 - 649 / 6300, + -1.0 / 60.0, + ], + dtype=torch.float32, + ), ) -DPS_C_MID = torch.tensor([ - 6025192743 / 30085553152 / 2, 0, 51252292925 / 65400821598 / 2, -2691868925 / 45128329728 / 2, - 187940372067 / 1594534317056 / 2, -1776094331 / 19743644256 / 2, 11237099 / 235043384 / 2 -], dtype=torch.float32) +DPS_C_MID = torch.tensor( + [ + 6025192743 / 30085553152 / 2, + 0, + 51252292925 / 65400821598 / 2, + -2691868925 / 45128329728 / 2, + 187940372067 / 1594534317056 / 2, + -1776094331 / 19743644256 / 2, + 11237099 / 235043384 / 2, + ], + dtype=torch.float32, +) class Dopri5Solver(RKAdaptiveStepsizeODESolver): order = 5 tableau = _DORMAND_PRINCE_SHAMPINE_TABLEAU - mid = DPS_C_MID \ No newline at end of file + mid = DPS_C_MID diff --git a/nnodely/support/odeint/fixed_grid.py b/nnodely/support/odeint/fixed_grid.py index 28bbf28d..e17c88bc 100644 --- a/nnodely/support/odeint/fixed_grid.py +++ b/nnodely/support/odeint/fixed_grid.py @@ -15,4 +15,4 @@ class RK4(FixedGridODESolver): def _step_func(self, func, t0, dt, t1, y0): f0 = func(t0, y0) - return rk4_step_func(func, t0, dt, t1, y0, f0=f0), f0 \ No newline at end of file + return rk4_step_func(func, t0, dt, t1, y0, f0=f0), f0 diff --git a/nnodely/support/odeint/my_odeint.py b/nnodely/support/odeint/my_odeint.py index 46c9bdb6..9a9b7f7c 100644 --- a/nnodely/support/odeint/my_odeint.py +++ b/nnodely/support/odeint/my_odeint.py @@ -5,13 +5,15 @@ from nnodely.support.odeint.fixed_grid import Euler, RK4 SOLVERS = { - 'dopri5': Dopri5Solver, - 'euler': Euler, - 'rk4': RK4, + "dopri5": Dopri5Solver, + "euler": Euler, + "rk4": RK4, } -def odeint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, event_fn=None): +def odeint( + func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, event_fn=None +): """Integrate a system of ordinary differential equations. Solves the initial value problem for a non-stiff system of first order ODEs: @@ -52,7 +54,9 @@ def odeint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, even ValueError: if an invalid `method` is provided. """ - shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed = _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS) + shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed = ( + _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS) + ) solver = SOLVERS[method](func=func, y0=y0, rtol=rtol, atol=atol, **options) @@ -73,7 +77,9 @@ def odeint(func, y0, t, *, rtol=1e-7, atol=1e-9, method=None, options=None, even return event_t, solution -def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface=odeint, **kwargs): +def odeint_event( + func, y0, t0, *, event_fn, reverse_time=False, odeint_interface=odeint, **kwargs +): """Automatically links up the gradient from the event time.""" if reverse_time: @@ -84,7 +90,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface event_t, solution = odeint_interface(func, y0, t, event_fn=event_fn, **kwargs) # Dummy values for rtol, atol, method, and options. - shapes, _func, _, t, _, _, _, _, event_fn, _ = _check_inputs(func, y0, t, 0.0, 0.0, None, None, event_fn, SOLVERS) + shapes, _func, _, t, _, _, _, _, event_fn, _ = _check_inputs( + func, y0, t, 0.0, 0.0, None, None, event_fn, SOLVERS + ) if shapes is not None: state_t = torch.cat([s[-1].reshape(-1) for s in solution]) @@ -95,7 +103,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface if reverse_time: event_t = -event_t - event_t, state_t = ImplicitFnGradientRerouting.apply(_func, event_fn, event_t, state_t) + event_t, state_t = ImplicitFnGradientRerouting.apply( + _func, event_fn, event_t, state_t + ) # Return the user expected time value. if reverse_time: @@ -103,7 +113,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface if shapes is not None: state_t = _flat_to_shape(state_t, (), shapes) - solution = tuple(torch.cat([s[:-1], s_t[None]], dim=0) for s, s_t in zip(solution, state_t)) + solution = tuple( + torch.cat([s[:-1], s_t[None]], dim=0) for s, s_t in zip(solution, state_t) + ) else: solution = torch.cat([solution[:-1], state_t[None]], dim=0) @@ -111,10 +123,9 @@ def odeint_event(func, y0, t0, *, event_fn, reverse_time=False, odeint_interface class ImplicitFnGradientRerouting(torch.autograd.Function): - @staticmethod def forward(ctx, func, event_fn, event_t, state_t): - """ event_t is the solution to event_fn """ + """event_t is the solution to event_fn""" ctx.func = func ctx.event_fn = event_fn ctx.save_for_backward(event_t, state_t) @@ -144,4 +155,4 @@ def backward(ctx, grad_t, grad_state): grad_state = grad_state + dstate - return None, None, None, grad_state \ No newline at end of file + return None, None, None, grad_state diff --git a/nnodely/support/odeint/rk_solvers.py b/nnodely/support/odeint/rk_solvers.py index 134a4183..69ad93e4 100644 --- a/nnodely/support/odeint/rk_solvers.py +++ b/nnodely/support/odeint/rk_solvers.py @@ -1,12 +1,21 @@ import collections import warnings import torch -from nnodely.support.odeint.solvers import _handle_unused_kwargs, find_event, AdaptiveStepsizeEventODESolver, FixedGridODESolver +from nnodely.support.odeint.solvers import ( + _handle_unused_kwargs, + find_event, + AdaptiveStepsizeEventODESolver, + FixedGridODESolver, +) -_ButcherTableau = collections.namedtuple('_ButcherTableau', 'alpha, beta, c_sol, c_error') +_ButcherTableau = collections.namedtuple( + "_ButcherTableau", "alpha, beta, c_sol, c_error" +) -_RungeKuttaState = collections.namedtuple('_RungeKuttaState', 'y1, f1, t0, t1, dt, interp_coeff') +_RungeKuttaState = collections.namedtuple( + "_RungeKuttaState", "y1, f1, t0, t1, dt, interp_coeff" +) # Saved state of the Runge Kutta solver. # # Attributes: @@ -18,6 +27,7 @@ # interp_coeff: list of Tensors giving coefficients for polynomial # interpolation between `t0` and `t1`. + def _interp_fit(y0, y1, y_mid, f0, f1, dt): """Fit coefficients for 4th order polynomial interpolation. @@ -41,6 +51,7 @@ def _interp_fit(y0, y1, y_mid, f0, f1, dt): e = y0 return [e, d, c, b, a] + def _interp_evaluate(coefficients, t0, t1, t): """Evaluate polynomial interpolation at the given time point. @@ -54,7 +65,9 @@ def _interp_evaluate(coefficients, t0, t1, t): Polynomial interpolation of the coefficients at time `t`. """ - assert (t0 <= t) & (t <= t1), 'invalid interpolation, fails `t0 <= t <= t1`: {}, {}, {}'.format(t0, t, t1) + assert (t0 <= t) & (t <= t1), ( + "invalid interpolation, fails `t0 <= t <= t1`: {}, {}, {}".format(t0, t, t1) + ) x = (t - t0) / (t1 - t0) x = x.to(coefficients[0].dtype) @@ -66,6 +79,7 @@ def _interp_evaluate(coefficients, t0, t1, t): return total + def _select_initial_step(func, t0, y0, order, rtol, atol, norm, f0=None): """Empirically select a good initial step. @@ -104,15 +118,17 @@ def _select_initial_step(func, t0, y0, order, rtol, atol, norm, f0=None): if d1 <= 1e-15 and d2 <= 1e-15: h1 = torch.max(torch.tensor(1e-6, dtype=dtype, device=device), h0 * 1e-3) else: - h1 = (0.01 / max(d1, d2)) ** (1. / float(order + 1)) + h1 = (0.01 / max(d1, d2)) ** (1.0 / float(order + 1)) h1 = h1.abs() return torch.min(100 * h0, h1).to(t_dtype) + def _compute_error_ratio(error_estimate, rtol, atol, y0, y1, norm): error_tol = atol + rtol * torch.max(y0.abs(), y1.abs()) return norm(error_estimate / error_tol).abs() + @torch.no_grad() def _optimal_step_size(last_step, error_ratio, safety, ifactor, dfactor, order): """Calculate the optimal size for the next step.""" @@ -121,10 +137,13 @@ def _optimal_step_size(last_step, error_ratio, safety, ifactor, dfactor, order): if error_ratio < 1: dfactor = torch.ones((), dtype=last_step.dtype, device=last_step.device) error_ratio = error_ratio.type_as(last_step) - exponent = torch.tensor(order, dtype=last_step.dtype, device=last_step.device).reciprocal() - factor = torch.min(ifactor, torch.max(safety / error_ratio ** exponent, dfactor)) + exponent = torch.tensor( + order, dtype=last_step.dtype, device=last_step.device + ).reciprocal() + factor = torch.min(ifactor, torch.max(safety / error_ratio**exponent, dfactor)) return last_step * factor + class _UncheckedAssign(torch.autograd.Function): @staticmethod def forward(ctx, scratch, value, index): @@ -136,6 +155,7 @@ def forward(ctx, scratch, value, index): def backward(ctx, grad_scratch): return grad_scratch, grad_scratch[ctx.index], None + def _runge_kutta_step(func, y0, f0, t0, dt, t1, tableau): """Take an arbitrary Runge-Kutta step and estimate error. Args: @@ -165,12 +185,12 @@ def _runge_kutta_step(func, y0, f0, t0, dt, t1, tableau): k = torch.empty(*f0.shape, len(tableau.alpha) + 1, dtype=y0.dtype, device=y0.device) k = _UncheckedAssign.apply(k, f0, (..., 0)) for i, (alpha_i, beta_i) in enumerate(zip(tableau.alpha, tableau.beta)): - if alpha_i == 1.: + if alpha_i == 1.0: # Always step to perturbing just before the end time, in case of discontinuities. ti = t1 else: ti = t0 + alpha_i * dt - yi = y0 + torch.sum(k[..., :i + 1] * (beta_i * dt), dim=-1).view_as(f0) + yi = y0 + torch.sum(k[..., : i + 1] * (beta_i * dt), dim=-1).view_as(f0) f = func(ti, yi) k = _UncheckedAssign.apply(k, f, (..., i + 1)) @@ -200,18 +220,24 @@ class RKAdaptiveStepsizeODESolver(AdaptiveStepsizeEventODESolver): tableau: _ButcherTableau mid: torch.Tensor - def __init__(self, func, y0, rtol, atol, - min_step=1e-8, - max_step=float('inf'), - first_step=None, - step_t=None, - jump_t=None, - safety=0.9, - ifactor=10.0, - dfactor=0.2, - max_num_steps=2**20, - dtype=torch.float32, - **kwargs): + def __init__( + self, + func, + y0, + rtol, + atol, + min_step=1e-8, + max_step=float("inf"), + first_step=None, + step_t=None, + jump_t=None, + safety=0.9, + ifactor=10.0, + dfactor=0.2, + max_num_steps=2**20, + dtype=torch.float32, + **kwargs, + ): super(RKAdaptiveStepsizeODESolver, self).__init__(dtype=dtype, y0=y0, **kwargs) # We use mixed precision. y has its original dtype (probably float32), whilst all 'time'-like objects use @@ -224,47 +250,79 @@ def __init__(self, func, y0, rtol, atol, self.atol = torch.as_tensor(atol, dtype=dtype, device=device) self.min_step = torch.as_tensor(min_step, dtype=dtype, device=device) self.max_step = torch.as_tensor(max_step, dtype=dtype, device=device) - self.first_step = None if first_step is None else torch.as_tensor(first_step, dtype=dtype, device=device) + self.first_step = ( + None + if first_step is None + else torch.as_tensor(first_step, dtype=dtype, device=device) + ) self.safety = torch.as_tensor(safety, dtype=dtype, device=device) self.ifactor = torch.as_tensor(ifactor, dtype=dtype, device=device) self.dfactor = torch.as_tensor(dfactor, dtype=dtype, device=device) - self.max_num_steps = torch.as_tensor(max_num_steps, dtype=torch.int32, device=device) + self.max_num_steps = torch.as_tensor( + max_num_steps, dtype=torch.int32, device=device + ) self.dtype = dtype - self.step_t = None if step_t is None else torch.as_tensor(step_t, dtype=dtype, device=device) - self.jump_t = None if jump_t is None else torch.as_tensor(jump_t, dtype=dtype, device=device) + self.step_t = ( + None + if step_t is None + else torch.as_tensor(step_t, dtype=dtype, device=device) + ) + self.jump_t = ( + None + if jump_t is None + else torch.as_tensor(jump_t, dtype=dtype, device=device) + ) # Copy from class to instance to set device - self.tableau = _ButcherTableau(alpha=self.tableau.alpha.to(device=device, dtype=y0.dtype), - beta=[b.to(device=device, dtype=y0.dtype) for b in self.tableau.beta], - c_sol=self.tableau.c_sol.to(device=device, dtype=y0.dtype), - c_error=self.tableau.c_error.to(device=device, dtype=y0.dtype)) + self.tableau = _ButcherTableau( + alpha=self.tableau.alpha.to(device=device, dtype=y0.dtype), + beta=[b.to(device=device, dtype=y0.dtype) for b in self.tableau.beta], + c_sol=self.tableau.c_sol.to(device=device, dtype=y0.dtype), + c_error=self.tableau.c_error.to(device=device, dtype=y0.dtype), + ) self.mid = self.mid.to(device=device, dtype=y0.dtype) @classmethod def valid_callbacks(cls): - return super(RKAdaptiveStepsizeODESolver, cls).valid_callbacks() | {'callback_step', - 'callback_accept_step', - 'callback_reject_step'} + return super(RKAdaptiveStepsizeODESolver, cls).valid_callbacks() | { + "callback_step", + "callback_accept_step", + "callback_reject_step", + } def _before_integrate(self, t): t0 = t[0] f0 = self.func(t[0], self.y0) if self.first_step is None: - first_step = _select_initial_step(self.func, t[0], self.y0, self.order - 1, self.rtol, self.atol, - self.norm, f0=f0) + first_step = _select_initial_step( + self.func, + t[0], + self.y0, + self.order - 1, + self.rtol, + self.atol, + self.norm, + f0=f0, + ) else: first_step = self.first_step - self.rk_state = _RungeKuttaState(self.y0, f0, t[0], t[0], first_step, [self.y0] * 5) + self.rk_state = _RungeKuttaState( + self.y0, f0, t[0], t[0], first_step, [self.y0] * 5 + ) def _advance(self, next_t): """Interpolate through the next time point, integrating as necessary.""" n_steps = 0 while next_t > self.rk_state.t1: - assert n_steps < self.max_num_steps, 'max_num_steps exceeded ({}>={})'.format(n_steps, self.max_num_steps) + assert n_steps < self.max_num_steps, ( + "max_num_steps exceeded ({}>={})".format(n_steps, self.max_num_steps) + ) self.rk_state = self._adaptive_step(self.rk_state) n_steps += 1 - return _interp_evaluate(self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, next_t) + return _interp_evaluate( + self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, next_t + ) def _advance_until_event(self, event_fn): """Returns t, state(t) such that event_fn(t, state(t)) == 0.""" @@ -274,11 +332,17 @@ def _advance_until_event(self, event_fn): n_steps = 0 sign0 = torch.sign(event_fn(self.rk_state.t1, self.rk_state.y1)) while sign0 == torch.sign(event_fn(self.rk_state.t1, self.rk_state.y1)): - assert n_steps < self.max_num_steps, 'max_num_steps exceeded ({}>={})'.format(n_steps, self.max_num_steps) + assert n_steps < self.max_num_steps, ( + "max_num_steps exceeded ({}>={})".format(n_steps, self.max_num_steps) + ) self.rk_state = self._adaptive_step(self.rk_state) n_steps += 1 - interp_fn = lambda t: _interp_evaluate(self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, t) - return find_event(interp_fn, sign0, self.rk_state.t0, self.rk_state.t1, event_fn, self.atol) + interp_fn = lambda t: _interp_evaluate( + self.rk_state.interp_coeff, self.rk_state.t0, self.rk_state.t1, t + ) + return find_event( + interp_fn, sign0, self.rk_state.t0, self.rk_state.t1, event_fn, self.atol + ) def _adaptive_step(self, rk_state): """Take an adaptive Runge-Kutta step to integrate the ODE.""" @@ -300,10 +364,12 @@ def _adaptive_step(self, rk_state): ######################################################## # Assertions # ######################################################## - assert t0 + dt > t0, 'underflow in dt {}'.format(dt.item()) - assert torch.isfinite(y0).all(), 'non-finite values in state `y`: {}'.format(y0) + assert t0 + dt > t0, "underflow in dt {}".format(dt.item()) + assert torch.isfinite(y0).all(), "non-finite values in state `y`: {}".format(y0) - y1, f1, y1_error, k = _runge_kutta_step(self.func, y0, f0, t0, dt, t1, tableau=self.tableau) + y1, f1, y1_error, k = _runge_kutta_step( + self.func, y0, f0, t0, dt, t1, tableau=self.tableau + ) # dtypes: # y1.dtype == self.y0.dtype # f1.dtype == self.y0.dtype @@ -313,7 +379,9 @@ def _adaptive_step(self, rk_state): ######################################################## # Error Ratio # ######################################################## - error_ratio = _compute_error_ratio(y1_error, self.rtol, self.atol, y0, y1, self.norm) + error_ratio = _compute_error_ratio( + y1_error, self.rtol, self.atol, y0, y1, self.norm + ) accept_step = error_ratio <= 1 # Handle min max stepping @@ -339,7 +407,9 @@ def _adaptive_step(self, rk_state): t_next = t0 y_next = y0 f_next = f0 - dt_next = _optimal_step_size(dt, error_ratio, self.safety, self.ifactor, self.dfactor, self.order) + dt_next = _optimal_step_size( + dt, error_ratio, self.safety, self.ifactor, self.dfactor, self.order + ) dt_next = dt_next.clamp(self.min_step, self.max_step) rk_state = _RungeKuttaState(y_next, f_next, t0, t_next, dt_next, interp_coeff) return rk_state @@ -351,17 +421,28 @@ def _interp_fit(self, y0, y1, k, dt): f0 = k[..., 0] f1 = k[..., -1] return _interp_fit(y0, y1, y_mid, f0, f1, dt) - + + class FixedGridFIRKODESolver(FixedGridODESolver): order: int tableau: _ButcherTableau - def __init__(self, func, y0, step_size=None, grid_constructor=None, interp='linear', perturb=False, max_iters=100, **unused_kwargs): + def __init__( + self, + func, + y0, + step_size=None, + grid_constructor=None, + interp="linear", + perturb=False, + max_iters=100, + **unused_kwargs, + ): self.max_iters = max_iters - self.atol = unused_kwargs.pop('atol') - unused_kwargs.pop('rtol', None) - unused_kwargs.pop('norm', None) + self.atol = unused_kwargs.pop("atol") + unused_kwargs.pop("rtol", None) + unused_kwargs.pop("norm", None) _handle_unused_kwargs(self, unused_kwargs) del unused_kwargs @@ -382,13 +463,17 @@ def __init__(self, func, y0, step_size=None, grid_constructor=None, interp='line if grid_constructor is None: self.grid_constructor = self._grid_constructor_from_step_size(step_size) else: - raise ValueError("step_size and grid_constructor are mutually exclusive arguments.") - - self.tableau = _ButcherTableau(alpha=self.tableau.alpha.to(device=self.device, dtype=y0.dtype), - beta=[b.to(device=self.device, dtype=y0.dtype) for b in self.tableau.beta], - c_sol=self.tableau.c_sol.to(device=self.device, dtype=y0.dtype), - c_error=self.tableau.c_error.to(device=self.device, dtype=y0.dtype)) - + raise ValueError( + "step_size and grid_constructor are mutually exclusive arguments." + ) + + self.tableau = _ButcherTableau( + alpha=self.tableau.alpha.to(device=self.device, dtype=y0.dtype), + beta=[b.to(device=self.device, dtype=y0.dtype) for b in self.tableau.beta], + c_sol=self.tableau.c_sol.to(device=self.device, dtype=y0.dtype), + c_error=self.tableau.c_error.to(device=self.device, dtype=y0.dtype), + ) + def _step_func(self, func, t0, dt, t1, y0): if not isinstance(t0, torch.Tensor): t0 = torch.tensor(t0) @@ -397,7 +482,7 @@ def _step_func(self, func, t0, dt, t1, y0): if not isinstance(t1, torch.Tensor): t1 = torch.tensor(t1) f0 = func(t0, y0) - + t_dtype = y0.abs().dtype tol = 1e-8 if t_dtype == torch.float32: @@ -433,26 +518,30 @@ def _step_func(self, func, t0, dt, t1, y0): newf = self._residual(func, k, y, t0, dt, t1) z = newf - f f = newf - J = J + (torch.outer ((z - torch.linalg.vecdot(J,s)),s)) / (torch.dot(s,s)) + J = J + (torch.outer((z - torch.linalg.vecdot(J, s)), s)) / ( + torch.dot(s, s) + ) if not converged: - warnings.warn('Functional iteration did not converge. Solution may be incorrect.') + warnings.warn( + "Functional iteration did not converge. Solution may be incorrect." + ) dy = torch.matmul(k, dt * self.tableau.c_sol) return dy, f0 - + def _residual(self, func, K, y, t0, dt, t1): res = torch.zeros_like(K) for i, (y_i, alpha_i) in enumerate(zip(y, self.tableau.alpha)): - if alpha_i == 1.: + if alpha_i == 1.0: ti = t1 - elif alpha_i == 0.: + elif alpha_i == 0.0: if not torch.all(self.tableau.beta[i]): # Same slope as stored so skip continue ti = t0 else: ti = t0 + alpha_i * dt - res[...,i] = K[...,i] - func(ti, y_i) - return res.flatten() \ No newline at end of file + res[..., i] = K[..., i] - func(ti, y_i) + return res.flatten() diff --git a/nnodely/support/odeint/solvers.py b/nnodely/support/odeint/solvers.py index 39ba70c3..6fb6b9e5 100644 --- a/nnodely/support/odeint/solvers.py +++ b/nnodely/support/odeint/solvers.py @@ -3,13 +3,18 @@ import warnings import torch + def _handle_unused_kwargs(solver, unused_kwargs): if len(unused_kwargs) > 0: - warnings.warn('{}: Unexpected arguments {}'.format(solver.__class__.__name__, unused_kwargs)) + warnings.warn( + "{}: Unexpected arguments {}".format( + solver.__class__.__name__, unused_kwargs + ) + ) + def find_event(interp_fn, sign0, t0, t1, event_fn, tol): with torch.no_grad(): - # Num iterations for the secant method until tolerance is within target. nitrs = torch.ceil(torch.log((t1 - t0) / tol) / math.log(2.0)) @@ -17,13 +22,14 @@ def find_event(interp_fn, sign0, t0, t1, event_fn, tol): t_mid = (t1 + t0) / 2.0 y_mid = interp_fn(t_mid) sign_mid = torch.sign(event_fn(t_mid, y_mid)) - same_as_sign0 = (sign0 == sign_mid) + same_as_sign0 = sign0 == sign_mid t0 = torch.where(same_as_sign0, t_mid, t0) t1 = torch.where(same_as_sign0, t1, t_mid) event_t = (t0 + t1) / 2.0 return event_t, interp_fn(event_t) + class AdaptiveStepsizeODESolver(metaclass=abc.ABCMeta): def __init__(self, dtype, y0, norm, **unused_kwargs): _handle_unused_kwargs(self, unused_kwargs) @@ -46,7 +52,9 @@ def valid_callbacks(cls): return set() def integrate(self, t): - solution = torch.empty(len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device) + solution = torch.empty( + len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device + ) solution[0] = self.y0 t = t.to(self.dtype) self._before_integrate(t) @@ -56,7 +64,6 @@ def integrate(self, t): class AdaptiveStepsizeEventODESolver(AdaptiveStepsizeODESolver, metaclass=abc.ABCMeta): - @abc.abstractmethod def _advance_until_event(self, event_fn): raise NotImplementedError @@ -72,10 +79,19 @@ def integrate_until_event(self, t0, event_fn): class FixedGridODESolver(metaclass=abc.ABCMeta): order: int - def __init__(self, func, y0, step_size=None, grid_constructor=None, interp="linear", perturb=False, **unused_kwargs): - self.atol = unused_kwargs.pop('atol') - unused_kwargs.pop('rtol', None) - unused_kwargs.pop('norm', None) + def __init__( + self, + func, + y0, + step_size=None, + grid_constructor=None, + interp="linear", + perturb=False, + **unused_kwargs, + ): + self.atol = unused_kwargs.pop("atol") + unused_kwargs.pop("rtol", None) + unused_kwargs.pop("norm", None) _handle_unused_kwargs(self, unused_kwargs) del unused_kwargs @@ -96,11 +112,13 @@ def __init__(self, func, y0, step_size=None, grid_constructor=None, interp="line if grid_constructor is None: self.grid_constructor = self._grid_constructor_from_step_size(step_size) else: - raise ValueError("step_size and grid_constructor are mutually exclusive arguments.") + raise ValueError( + "step_size and grid_constructor are mutually exclusive arguments." + ) @classmethod def valid_callbacks(cls): - return {'callback_step'} + return {"callback_step"} @staticmethod def _grid_constructor_from_step_size(step_size): @@ -109,10 +127,14 @@ def _grid_constructor(func, y0, t): end_time = t[-1] niters = torch.ceil((end_time - start_time) / step_size + 1).item() - t_infer = torch.arange(0, niters, dtype=t.dtype, device=t.device) * step_size + start_time + t_infer = ( + torch.arange(0, niters, dtype=t.dtype, device=t.device) * step_size + + start_time + ) t_infer[-1] = t[-1] return t_infer + return _grid_constructor @abc.abstractmethod @@ -123,7 +145,9 @@ def integrate(self, t): time_grid = self.grid_constructor(self.func, self.y0, t) assert time_grid[0] == t[0] and time_grid[-1] == t[-1] - solution = torch.empty(len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device) + solution = torch.empty( + len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device + ) solution[0] = self.y0 j = 1 @@ -139,7 +163,9 @@ def integrate(self, t): solution[j] = self._linear_interp(t0, t1, y0, y1, t[j]) elif self.interp == "cubic": f1 = self.func(t1, y1) - solution[j] = self._cubic_hermite_interp(t0, y0, f0, t1, y1, f1, t[j]) + solution[j] = self._cubic_hermite_interp( + t0, y0, f0, t1, y1, f1, t[j] + ) else: raise ValueError(f"Unknown interpolation method {self.interp}") j += 1 @@ -148,7 +174,9 @@ def integrate(self, t): return solution def integrate_until_event(self, t0, event_fn): - assert self.step_size is not None, "Event handling for fixed step solvers currently requires `step_size` to be provided in options." + assert self.step_size is not None, ( + "Event handling for fixed step solvers currently requires `step_size` to be provided in options." + ) t0 = t0.type_as(self.y0.abs()) y0 = self.y0 @@ -170,10 +198,14 @@ def integrate_until_event(self, t0, event_fn): interp_fn = lambda t: self._linear_interp(t0, t1, y0, y1, t) elif self.interp == "cubic": f1 = self.func(t1, y1) - interp_fn = lambda t: self._cubic_hermite_interp(t0, y0, f0, t1, y1, f1, t) + interp_fn = lambda t: self._cubic_hermite_interp( + t0, y0, f0, t1, y1, f1, t + ) else: raise ValueError(f"Unknown interpolation method {self.interp}") - event_time, y1 = find_event(interp_fn, sign0, t0, t1, event_fn, float(self.atol)) + event_time, y1 = find_event( + interp_fn, sign0, t0, t1, event_fn, float(self.atol) + ) break else: t0, y0 = t1, y1 @@ -182,14 +214,14 @@ def integrate_until_event(self, t0, event_fn): raise RuntimeError(f"Reached maximum number of iterations {max_itrs}.") solution = torch.stack([self.y0, y1], dim=0) return event_time, solution - + def _cubic_hermite_interp(self, t0, y0, f0, t1, y1, f1, t): h = (t - t0) / (t1 - t0) h00 = (1 + 2 * h) * (1 - h) * (1 - h) h10 = h * (1 - h) * (1 - h) h01 = h * h * (3 - 2 * h) h11 = h * h * (h - 1) - dt = (t1 - t0) + dt = t1 - t0 return h00 * y0 + h10 * dt * f0 + h01 * y1 + h11 * dt * f1 def _linear_interp(self, t0, t1, y0, y1, t): @@ -199,4 +231,3 @@ def _linear_interp(self, t0, t1, y0, y1, t): return y1 slope = (t - t0) / (t1 - t0) return y0 + slope * (y1 - y0) - diff --git a/nnodely/support/odeint/utils.py b/nnodely/support/odeint/utils.py index 5ec8d6c5..1d4ab3b4 100644 --- a/nnodely/support/odeint/utils.py +++ b/nnodely/support/odeint/utils.py @@ -1,22 +1,26 @@ import torch import warnings -_all_callback_names = ['callback_step', 'callback_accept_step', 'callback_reject_step'] -_all_adjoint_callback_names = [name + '_adjoint' for name in _all_callback_names] +_all_callback_names = ["callback_step", "callback_accept_step", "callback_reject_step"] +_all_adjoint_callback_names = [name + "_adjoint" for name in _all_callback_names] _null_callback = lambda *args, **kwargs: None + def _linf_norm(tensor): return tensor.abs().max() + def _rms_norm(tensor): return tensor.abs().pow(2).mean().sqrt() + def _zero_norm(tensor): - return 0. + return 0.0 + def _mixed_norm(tensor_tuple): if len(tensor_tuple) == 0: - return 0. + return 0.0 return max([_rms_norm(tensor) for tensor in tensor_tuple]) @@ -41,8 +45,12 @@ def _tuple_tol(name, tol, shapes): except TypeError: return tol tol = tuple(tol) - assert len(tol) == len(shapes), "If using tupled {} it must have the same length as the tuple y0".format(name) - tol = [torch.as_tensor(tol_).expand(shape.numel()) for tol_, shape in zip(tol, shapes)] + assert len(tol) == len(shapes), ( + "If using tupled {} it must have the same length as the tuple y0".format(name) + ) + tol = [ + torch.as_tensor(tol_).expand(shape.numel()) for tol_, shape in zip(tol, shapes) + ] return torch.cat(tol) @@ -59,17 +67,21 @@ def _flat_to_shape(tensor, length, shapes): def _assert_floating(name, t): if not torch.is_floating_point(t): - raise TypeError('`{}` must be a floating point Tensor but is a {}'.format(name, t.type())) + raise TypeError( + "`{}` must be a floating point Tensor but is a {}".format(name, t.type()) + ) def _check_timelike(name, timelike, can_grad): - assert isinstance(timelike, torch.Tensor), '{} must be a torch.Tensor'.format(name) + assert isinstance(timelike, torch.Tensor), "{} must be a torch.Tensor".format(name) _assert_floating(name, timelike) assert timelike.ndimension() == 1, "{} must be one dimensional".format(name) if not can_grad: assert not timelike.requires_grad, "{} cannot require gradient".format(name) diff = timelike[1:] > timelike[:-1] - assert diff.all() or (~diff).all(), '{} must be strictly increasing or decreasing'.format(name) + assert diff.all() or (~diff).all(), ( + "{} must be strictly increasing or decreasing".format(name) + ) class _TupleFunc(torch.nn.Module): @@ -107,7 +119,9 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS): if event_fn is not None: if len(t) != 2: - raise ValueError(f"We require len(t) == 2 when in event handling mode, but got len(t)={len(t)}.") + raise ValueError( + f"We require len(t) == 2 when in event handling mode, but got len(t)={len(t)}." + ) # Combine event functions if the output is multivariate. event_fn = combine_event_functions(event_fn, t[0], y0) @@ -119,10 +133,10 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS): shapes = None is_tuple = not isinstance(y0, torch.Tensor) if is_tuple: - assert isinstance(y0, tuple), 'y0 must be either a torch.Tensor or a tuple' + assert isinstance(y0, tuple), "y0 must be either a torch.Tensor or a tuple" shapes = [y0_.shape for y0_ in y0] - rtol = _tuple_tol('rtol', rtol, shapes) - atol = _tuple_tol('atol', atol, shapes) + rtol = _tuple_tol("rtol", rtol, shapes) + atol = _tuple_tol("atol", atol, shapes) y0 = torch.cat([y0_.reshape(-1) for y0_ in y0]) func = _TupleFunc(func, shapes) if event_fn is not None: @@ -134,18 +148,21 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS): else: options = options.copy() if method is None: - method = 'dopri5' + method = "dopri5" if method not in SOLVERS: - raise ValueError('Invalid method "{}". Must be one of {}'.format(method, - '{"' + '", "'.join(SOLVERS.keys()) + '"}.')) + raise ValueError( + 'Invalid method "{}". Must be one of {}'.format( + method, '{"' + '", "'.join(SOLVERS.keys()) + '"}.' + ) + ) if is_tuple: # We accept tupled input. This is an abstraction that is hidden from the rest of odeint (exception when # returning values), so here we need to maintain the abstraction by wrapping norm functions. - if 'norm' in options: + if "norm" in options: # If the user passed a norm then get that... - norm = options['norm'] + norm = options["norm"] else: # ...otherwise we default to a mixed Linf/L2 norm over tupled input. norm = _mixed_norm @@ -157,10 +174,11 @@ def _check_inputs(func, y0, t, rtol, atol, method, options, event_fn, SOLVERS): def _norm(tensor): y = _flat_to_shape(tensor, (), shapes) return norm(y) - options['norm'] = _norm + + options["norm"] = _norm else: - if 'norm' in options: + if "norm" in options: # No need to change the norm function. pass else: @@ -168,10 +186,10 @@ def _norm(tensor): # Technically we don't need to set that here (RKAdaptiveStepsizeODESolver has it as a default), but it # makes it easier to reason about, in the adjoint norm logic, if we know that options['norm'] is # definitely set to something. - options['norm'] = _rms_norm + options["norm"] = _rms_norm # Normalise time - _check_timelike('t', t, True) + _check_timelike("t", t, True) t_is_reversed = False if len(t) > 1 and t[0] > t[1]: t_is_reversed = True @@ -188,18 +206,20 @@ def _norm(tensor): # For fixed step solvers. try: - _grid_constructor = options['grid_constructor'] + _grid_constructor = options["grid_constructor"] except KeyError: pass else: - options['grid_constructor'] = lambda func, y0, t: -_grid_constructor(func, y0, -t) + options["grid_constructor"] = lambda func, y0, t: ( + -_grid_constructor(func, y0, -t) + ) # For RK solvers. - #_flip_option(options, 'step_t') - #_flip_option(options, 'jump_t') + # _flip_option(options, 'step_t') + # _flip_option(options, 'jump_t') # Can only do after having normalised time - assert (t[1:] > t[:-1]).all(), 't must be strictly increasing or decreasing' + assert (t[1:] > t[:-1]).all(), "t must be strictly increasing or decreasing" # Tol checking if torch.is_tensor(rtol): @@ -214,7 +234,7 @@ def _norm(tensor): # ~Backward compatibility # Add perturb argument to func. - #func = _PerturbFunc(func) + # func = _PerturbFunc(func) # Add callbacks to wrapped_func callback_names = set() @@ -229,12 +249,16 @@ def _norm(tensor): # At the moment all callbacks have the arguments (t0, y0, dt). # These will need adjusting on a per-callback basis if that changes in the future. if is_tuple: + def callback(t0, y0, dt, _callback=callback): y0 = _flat_to_shape(y0, (), shapes) return _callback(t0, y0, dt) + if t_is_reversed: + def callback(t0, y0, dt, _callback=callback): return _callback(-t0, y0, dt) + setattr(func, callback_name, callback) for callback_name in _all_adjoint_callback_names: try: @@ -246,6 +270,10 @@ def callback(t0, y0, dt, _callback=callback): invalid_callbacks = callback_names - SOLVERS[method].valid_callbacks() if len(invalid_callbacks) > 0: - warnings.warn("Solver '{}' does not support callbacks {}".format(method, invalid_callbacks)) + warnings.warn( + "Solver '{}' does not support callbacks {}".format( + method, invalid_callbacks + ) + ) - return shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed \ No newline at end of file + return shapes, func, y0, t, rtol, atol, method, options, event_fn, t_is_reversed diff --git a/nnodely/support/utils.py b/nnodely/support/utils.py index 0aff8e20..fd4ede73 100644 --- a/nnodely/support/utils.py +++ b/nnodely/support/utils.py @@ -13,6 +13,7 @@ ForbiddenTags = keyword.kwlist + class ReadOnlyDict: def __init__(self, data): self._data = data @@ -40,6 +41,7 @@ def values(self): def __repr__(self): from pprint import pformat + return pformat(self._data) def __or__(self, other): @@ -51,6 +53,7 @@ def __or__(self, other): def __str__(self): from nnodely.visualizer.emptyvisualizer import color, GREEN from pprint import pformat + return color(pformat(self._data), GREEN) def __eq__(self, other): @@ -58,19 +61,21 @@ def __eq__(self, other): return self._data == other return self._data == other._data + class ParamDict(ReadOnlyDict): - def __init__(self, data, internal_data = None): + def __init__(self, data, internal_data=None): super().__init__(data) self._internal_data = internal_data if internal_data is not None else {} def __setitem__(self, key, value): - self._data[key]['values'] = value + self._data[key]["values"] = value self._internal_data[key] = self._internal_data[key].new_tensor(value) def __getitem__(self, key): - value = self._data[key]['values'] if 'values' in self._data[key] else None + value = self._data[key]["values"] if "values" in self._data[key] else None return value + def enforce_types(func): @wraps(func) def wrapper(*args, **kwargs): @@ -81,27 +86,35 @@ def wrapper(*args, **kwargs): if len(sig) != len(args): var_type = None for ind, arg in enumerate(args): - if ind < len(list(sig.values())) and list(sig.values())[ind].kind == inspect.Parameter.VAR_POSITIONAL: + if ( + ind < len(list(sig.values())) + and list(sig.values())[ind].kind == inspect.Parameter.VAR_POSITIONAL + ): var_name = list(sig.keys())[ind] var_type = sig.pop(var_name) if var_type: - sig[var_name+str(ind)] = var_type + sig[var_name + str(ind)] = var_type all_args.update(dict(zip(sig, args))) - if 'self' in sig.keys(): - sig.pop('self') - if 'cls' in sig.keys(): - sig.pop('cls') + if "self" in sig.keys(): + sig.pop("self") + if "cls" in sig.keys(): + sig.pop("cls") for arg_name, arg in all_args.items(): - if (arg_name in hints.keys() or arg_name in sig.keys()) and not isinstance(arg,sig[arg_name].annotation): - class_name = func.__qualname__.split('.')[0] + if (arg_name in hints.keys() or arg_name in sig.keys()) and not isinstance( + arg, sig[arg_name].annotation + ): + class_name = func.__qualname__.split(".")[0] if isinstance(sig[arg_name].annotation, types.UnionType): - type_list = [val.__name__ for val in sig[arg_name].annotation.__args__] + type_list = [ + val.__name__ for val in sig[arg_name].annotation.__args__ + ] else: type_list = sig[arg_name].annotation.__name__ raise TypeError( - f"In Function or Class {class_name} the argument '{arg_name}' to be of type {type_list}, but got {type(arg).__name__}") + f"In Function or Class {class_name} the argument '{arg_name}' to be of type {type_list}, but got {type(arg).__name__}" + ) # for arg, arg_type in hints.items(): # if arg in all_args and not isinstance(all_args[arg], arg_type): @@ -112,15 +125,18 @@ def wrapper(*args, **kwargs): return wrapper + def is_notebook(): try: from IPython import get_ipython - if 'IPKernelApp' in get_ipython().config: + + if "IPKernelApp" in get_ipython().config: return True # È un notebook except Exception: pass return False # È uno script + def tensor_to_list(data): if isinstance(data, torch.Tensor): # Converte il tensore in una lista @@ -141,25 +157,35 @@ def tensor_to_list(data): # Altri tipi di dati rimangono invariati return data -def get_batch_size(n_samples, batch_size = None, predicion_samples = 0): + +def get_batch_size(n_samples, batch_size=None, predicion_samples=0): batch_size = batch_size if batch_size is not None else n_samples - predicion_samples = 0 if predicion_samples == -1 else predicion_samples #This value is used to disconnect the connect - batch_size = batch_size if batch_size <= n_samples - predicion_samples else max(0, n_samples - predicion_samples) - check(batch_size > 0, ValueError, f'The batch_size must be greater than 0.') + predicion_samples = ( + 0 if predicion_samples == -1 else predicion_samples + ) # This value is used to disconnect the connect + batch_size = ( + batch_size + if batch_size <= n_samples - predicion_samples + else max(0, n_samples - predicion_samples) + ) + check(batch_size > 0, ValueError, f"The batch_size must be greater than 0.") return batch_size + def check_and_get_list(name_list, available_names, error_fun): if type(name_list) is str: name_list = [name_list] if type(name_list) is list: for name in name_list: - check(name in available_names, IndexError, error_fun(name)) + check(name in available_names, IndexError, error_fun(name)) return name_list + def check(condition, exception, string): if not condition: raise exception(string) + # Function used to verified the number of gradient operations in the graph # def count_gradient_operations(grad_fn): # count = 0 @@ -176,4 +202,4 @@ def check(condition, exception, string): # count = 0 # for key in X.keys(): # count += count_gradient_operations(X[key].grad_fn) -# return count \ No newline at end of file +# return count diff --git a/nnodely/visualizer/__init__.py b/nnodely/visualizer/__init__.py index 43263770..1f668dc4 100644 --- a/nnodely/visualizer/__init__.py +++ b/nnodely/visualizer/__init__.py @@ -1,4 +1,4 @@ from nnodely.visualizer.emptyvisualizer import EmptyVisualizer from nnodely.visualizer.textvisualizer import TextVisualizer from nnodely.visualizer.mplvisualizer import MPLVisualizer -from nnodely.visualizer.mplnotebookvisualizer import MPLNotebookVisualizer \ No newline at end of file +from nnodely.visualizer.mplnotebookvisualizer import MPLNotebookVisualizer diff --git a/nnodely/visualizer/dynamicmpl/functionplot.py b/nnodely/visualizer/dynamicmpl/functionplot.py index c640814a..dbef5a85 100644 --- a/nnodely/visualizer/dynamicmpl/functionplot.py +++ b/nnodely/visualizer/dynamicmpl/functionplot.py @@ -13,17 +13,17 @@ try: # Convert to float and append to buffer data_point = json.loads(line) - name = data_point['name'] - if 'x1' in data_point.keys(): - x0 = data_point['x0'] - x1 = data_point['x1'] + name = data_point["name"] + if "x1" in data_point.keys(): + x0 = data_point["x0"] + x1 = data_point["x1"] else: - x = data_point['x0'] - params = data_point['params'] - input_names = data_point['input_names'] - output = data_point['output'] + x = data_point["x0"] + params = data_point["params"] + input_names = data_point["input_names"] + output = data_point["output"] - if 'x1' in data_point.keys(): + if "x1" in data_point.keys(): plots.plot_3d_function(plt, name, x0, x1, params, output, input_names) else: plots.plot_2d_function(plt, name, x, params, output, input_names) @@ -31,4 +31,3 @@ except ValueError: pass - diff --git a/nnodely/visualizer/dynamicmpl/fuzzyplot.py b/nnodely/visualizer/dynamicmpl/fuzzyplot.py index 6717f8cc..cf99f168 100644 --- a/nnodely/visualizer/dynamicmpl/fuzzyplot.py +++ b/nnodely/visualizer/dynamicmpl/fuzzyplot.py @@ -14,13 +14,13 @@ try: # Convert to float and append to buffer data_point = json.loads(line) - name = data_point['name'] - x = data_point['x'] - chan_centers = data_point['chan_centers'] + name = data_point["name"] + x = data_point["x"] + chan_centers = data_point["chan_centers"] tableau_colors = mcolors.TABLEAU_COLORS num_of_colors = len(list(tableau_colors.keys())) - for ind, key in enumerate(data_point['y'].keys()): - y.append(data_point['y'][key]) + for ind, key in enumerate(data_point["y"].keys()): + y.append(data_point["y"][key]) fig, ax = plt.subplots() ax.cla() @@ -28,4 +28,4 @@ plt.show() except ValueError: - pass \ No newline at end of file + pass diff --git a/nnodely/visualizer/dynamicmpl/resultsplot.py b/nnodely/visualizer/dynamicmpl/resultsplot.py index c94a167f..9db0fbc5 100644 --- a/nnodely/visualizer/dynamicmpl/resultsplot.py +++ b/nnodely/visualizer/dynamicmpl/resultsplot.py @@ -1,6 +1,7 @@ import json import sys import os + # append a new directory to sys.path sys.path.append(os.getcwd()) @@ -17,12 +18,12 @@ try: # Convert to float and append to buffer data_point = json.loads(line) - name_data = data_point['name_data'] - key = data_point['key'] - A = data_point['prediction_A'] - B = data_point['prediction_B'] - data_idxs = data_point['data_idxs'] - sample_time = data_point['sample_time'] + name_data = data_point["name_data"] + key = data_point["key"] + A = data_point["prediction_A"] + B = data_point["prediction_B"] + data_idxs = data_point["data_idxs"] + sample_time = data_point["sample_time"] fig, ax = plt.subplots() ax.cla() @@ -30,4 +31,4 @@ plt.show() except ValueError: - pass \ No newline at end of file + pass diff --git a/nnodely/visualizer/dynamicmpl/trainingplot.py b/nnodely/visualizer/dynamicmpl/trainingplot.py index 0b3b87e4..cd153d30 100644 --- a/nnodely/visualizer/dynamicmpl/trainingplot.py +++ b/nnodely/visualizer/dynamicmpl/trainingplot.py @@ -1,5 +1,6 @@ import sys import os + # append a new directory to sys.path sys.path.append(os.getcwd()) @@ -19,6 +20,7 @@ # Set up the plot fig, ax = plt.subplots() + def update_graph(frame): global last, title, epoch if last > 0: @@ -28,13 +30,13 @@ def update_graph(frame): try: # Convert to float and append to buffer data = json.loads(line) - data_train.append(data['train_losses']) - if data['val_losses']: - data_val.append(data['val_losses']) - title = data['title'] - key = data['key'] - last = data['last'] - epoch = data['epoch'] + data_train.append(data["train_losses"]) + if data["val_losses"]: + data_val.append(data["val_losses"]) + title = data["title"] + key = data["key"] + last = data["last"] + epoch = data["epoch"] # Clear the current plot ax.cla() # Clear the current plot @@ -44,6 +46,7 @@ def update_graph(frame): else: pass + # Use FuncAnimation to update the plot dynamically ani = animation.FuncAnimation(fig, update_graph, interval=10, save_count=20) diff --git a/nnodely/visualizer/emptyvisualizer.py b/nnodely/visualizer/emptyvisualizer.py index d71abb8c..ac72115a 100644 --- a/nnodely/visualizer/emptyvisualizer.py +++ b/nnodely/visualizer/emptyvisualizer.py @@ -8,11 +8,13 @@ BOLD_SEQ = "\033[1m" BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) -def color(msg, color_val = GREEN, bold = False): + +def color(msg, color_val=GREEN, bold=False): if bold: return COLOR_BOLD_SEQ % (30 + color_val) + msg + RESET_SEQ return COLOR_SEQ % (30 + color_val) + msg + RESET_SEQ + class EmptyVisualizer: def __init__(self): pass @@ -23,7 +25,7 @@ def setModely(self, modely): def showModel(self, model): pass - def showaddMinimize(self,variable_name): + def showaddMinimize(self, variable_name): pass def showModelInputWindow(self): @@ -35,13 +37,13 @@ def showModelRelationSamples(self): def showBuiltModel(self): pass - def showWeights(self, weights = None): + def showWeights(self, weights=None): pass - def showFunctions(self, functions = None): + def showFunctions(self, functions=None): pass - def showWeightsInTrain(self, batch = None, epoch = None, weights = None): + def showWeightsInTrain(self, batch=None, epoch=None, weights=None): pass def showDataset(self, name): diff --git a/nnodely/visualizer/mplnotebookvisualizer.py b/nnodely/visualizer/mplnotebookvisualizer.py index 4a9d4428..3d66ac53 100644 --- a/nnodely/visualizer/mplnotebookvisualizer.py +++ b/nnodely/visualizer/mplnotebookvisualizer.py @@ -7,80 +7,109 @@ from nnodely.support.utils import check from mplplots import plots + class MPLNotebookVisualizer(TextVisualizer): - def __init__(self, verbose = 1, *, test = False): + def __init__(self, verbose=1, *, test=False): super().__init__(verbose) self.test = test if self.test: plt.ion() def showEndTraining(self, epoch, train_losses, val_losses): - train_tag = self.modely.running_parameters['train_tag'] - val_tag = self.modely.running_parameters['val_tag'] - for key in self.modely.json['Minimizers'].keys(): + train_tag = self.modely.running_parameters["train_tag"] + val_tag = self.modely.running_parameters["val_tag"] + for key in self.modely.json["Minimizers"].keys(): fig = plt.figure() ax = fig.add_subplot(111) if val_losses: - plots.plot_training(ax, f"Training on {train_tag} and {val_tag}", key, train_losses[key], val_losses[key]) + plots.plot_training( + ax, + f"Training on {train_tag} and {val_tag}", + key, + train_losses[key], + val_losses[key], + ) else: - plots.plot_training(ax, f"Training on {train_tag}", key, train_losses[key]) + plots.plot_training( + ax, f"Training on {train_tag}", key, train_losses[key] + ) plt.show() def showResult(self, name_data): super().showResult(name_data) - for key in self.modely.json['Minimizers'].keys(): + for key in self.modely.json["Minimizers"].keys(): fig = plt.figure() ax = fig.add_subplot(111) - np_data_A = np.array(self.modely.prediction[name_data][key]['A']) + np_data_A = np.array(self.modely.prediction[name_data][key]["A"]) if len(np_data_A.shape) > 3 and np_data_A.shape[1] > 30: - np_data_B = np.array(self.modely.prediction[name_data][key]['B']) + np_data_B = np.array(self.modely.prediction[name_data][key]["B"]) indices = np.linspace(0, np_data_A.shape[1] - 1, 30, dtype=int) data_A = np_data_A[:, indices, :, :].tolist() data_B = np_data_B[:, indices, :, :].tolist() - data_idxs = np.array(self.modely.prediction[name_data]['idxs'])[:,indices].tolist() + data_idxs = np.array(self.modely.prediction[name_data]["idxs"])[ + :, indices + ].tolist() else: - data_A = self.modely.prediction[name_data][key]['A'] - data_B = self.modely.prediction[name_data][key]['B'] - data_idxs = self.modely.prediction[name_data]['idxs'] if len(np_data_A.shape) > 3 else None + data_A = self.modely.prediction[name_data][key]["A"] + data_B = self.modely.prediction[name_data][key]["B"] + data_idxs = ( + self.modely.prediction[name_data]["idxs"] + if len(np_data_A.shape) > 3 + else None + ) - plots.plot_results(ax, name_data, key, data_A, - data_B, data_idxs, self.modely._model_def['Info']["SampleTime"]) + plots.plot_results( + ax, + name_data, + key, + data_A, + data_B, + data_idxs, + self.modely._model_def["Info"]["SampleTime"], + ) plt.show() - def showWeights(self, weights = None): + def showWeights(self, weights=None): pass - def showFunctions(self, functions = None, xlim = None, num_points = 1000): + def showFunctions(self, functions=None, xlim=None, num_points=1000): check(self.modely.neuralized, ValueError, "The model has not been neuralized.") - for fun, value in self.modely._model_def['Functions'].items(): + for fun, value in self.modely._model_def["Functions"].items(): if fun in functions: - if 'functions' in self.modely._model_def['Functions'][fun]: + if "functions" in self.modely._model_def["Functions"][fun]: x, activ_fun = return_fuzzify(value, xlim, num_points) fig = plt.figure() ax = fig.add_subplot(111) - plots.plot_fuzzy(ax, fun, x, activ_fun, value['centers']) - elif 'code': - function_inputs = return_standard_inputs(value, self.modely._model_def, xlim, num_points) - function_output, function_input_list = return_function(value, function_inputs) - if value['n_input'] == 2: + plots.plot_fuzzy(ax, fun, x, activ_fun, value["centers"]) + elif "code": + function_inputs = return_standard_inputs( + value, self.modely._model_def, xlim, num_points + ) + function_output, function_input_list = return_function( + value, function_inputs + ) + if value["n_input"] == 2: x0 = function_inputs[0].reshape(num_points, num_points).tolist() x1 = function_inputs[1].reshape(num_points, num_points).tolist() - output = function_output.reshape(num_points, num_points).tolist() + output = function_output.reshape( + num_points, num_points + ).tolist() params = [] - for i, key in enumerate(value['params_and_consts']): - params += [function_inputs[i + value['n_input']].tolist()] - plots.plot_3d_function(plt, fun, x0, x1, params, output, function_input_list) + for i, key in enumerate(value["params_and_consts"]): + params += [function_inputs[i + value["n_input"]].tolist()] + plots.plot_3d_function( + plt, fun, x0, x1, params, output, function_input_list + ) else: x = function_inputs[0].reshape(num_points).tolist() output = function_output.reshape(num_points).tolist() params = [] - for i, key in enumerate(value['params_and_consts']): - params += [function_inputs[i + value['n_input']].tolist()] - plots.plot_2d_function(plt, fun, x, params, output, function_input_list) + for i, key in enumerate(value["params_and_consts"]): + params += [function_inputs[i + value["n_input"]].tolist()] + plots.plot_2d_function( + plt, fun, x, params, output, function_input_list + ) plt.show() def closePlots(self): plt.close() - - - diff --git a/nnodely/visualizer/mplvisualizer.py b/nnodely/visualizer/mplvisualizer.py index c1a130ad..113bf83a 100644 --- a/nnodely/visualizer/mplvisualizer.py +++ b/nnodely/visualizer/mplvisualizer.py @@ -8,28 +8,41 @@ from nnodely.basic.modeldef import ModelDef from nnodely.support.logger import logging, nnLogger + log = nnLogger(__name__, logging.INFO) + def get_library_path(library_name): spec = importlib.util.find_spec(library_name) if spec is None: raise ImportError(f"Library {library_name} not found") return os.path.dirname(spec.origin) + class MPLVisualizer(TextVisualizer): - def __init__(self, verbose = 1): + def __init__(self, verbose=1): super().__init__(verbose) # Path to the data visualizer script import signal import sys - get_library_path('nnodely') - self.__training_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','trainingplot.py') - self.__time_series_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','resultsplot.py') - self.__fuzzy_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','fuzzyplot.py') - self.__function_visualizer_script = os.path.join(get_library_path('nnodely'),'visualizer','dynamicmpl','functionplot.py') + + get_library_path("nnodely") + self.__training_visualizer_script = os.path.join( + get_library_path("nnodely"), "visualizer", "dynamicmpl", "trainingplot.py" + ) + self.__time_series_visualizer_script = os.path.join( + get_library_path("nnodely"), "visualizer", "dynamicmpl", "resultsplot.py" + ) + self.__fuzzy_visualizer_script = os.path.join( + get_library_path("nnodely"), "visualizer", "dynamicmpl", "fuzzyplot.py" + ) + self.__function_visualizer_script = os.path.join( + get_library_path("nnodely"), "visualizer", "dynamicmpl", "functionplot.py" + ) self.__process_training = {} self.__process_results = {} self.__process_function = {} + def signal_handler(sig, frame): for key in self.__process_training.keys(): self.__process_training[key].terminate() @@ -52,28 +65,41 @@ def showStartTraining(self): def showTraining(self, epoch, train_losses, val_losses): if epoch == 0: for key in self.__process_training.keys(): - if self.__process_training[key] is not None and self.__process_training[key].poll() is None: + if ( + self.__process_training[key] is not None + and self.__process_training[key].poll() is None + ): self.__process_training[key].terminate() self.__process_training[key].wait() self.__process_training[key] = None self.__process_training = {} - for key in self.modely._model_def['Minimizers'].keys(): - self.__process_training[key] = subprocess.Popen(['python', self.__training_visualizer_script], stdin=subprocess.PIPE, text=True) - - num_of_epochs = self.modely.running_parameters['num_of_epochs'] - train_tag = self.modely.running_parameters['train_tag'] - val_tag = self.modely.running_parameters['val_tag'] - if epoch+1 <= num_of_epochs: - for key in self.modely._model_def['Minimizers'].keys(): + for key in self.modely._model_def["Minimizers"].keys(): + self.__process_training[key] = subprocess.Popen( + ["python", self.__training_visualizer_script], + stdin=subprocess.PIPE, + text=True, + ) + + num_of_epochs = self.modely.running_parameters["num_of_epochs"] + train_tag = self.modely.running_parameters["train_tag"] + val_tag = self.modely.running_parameters["val_tag"] + if epoch + 1 <= num_of_epochs: + for key in self.modely._model_def["Minimizers"].keys(): if val_losses: val_loss = val_losses[key][epoch] title = f"Training on {train_tag} and {val_tag}" else: val_loss = [] title = f"Training on {train_tag}" - data = {"title":title, "key": key, "last": num_of_epochs - (epoch + 1), "epoch": epoch, - "train_losses": train_losses[key][epoch], "val_losses": val_loss} + data = { + "title": title, + "key": key, + "last": num_of_epochs - (epoch + 1), + "epoch": epoch, + "train_losses": train_losses[key][epoch], + "val_losses": val_loss, + } try: # Send data to the visualizer process self.__process_training[key].stdin.write(f"{json.dumps(data)}\n") @@ -82,98 +108,139 @@ def showTraining(self, epoch, train_losses, val_losses): self.closeTraining() log.warning("The visualizer process has been closed.") - if epoch+1 == num_of_epochs: - for key in self.modely._model_def['Minimizers'].keys(): + if epoch + 1 == num_of_epochs: + for key in self.modely._model_def["Minimizers"].keys(): if self.__process_training[key] is not None: self.__process_training[key].stdin.close() def showResult(self, name_data): super().showResult(name_data) - check(name_data in self.modely.performance, ValueError, f"Results not available for {name_data}.") + check( + name_data in self.modely.performance, + ValueError, + f"Results not available for {name_data}.", + ) if name_data in self.__process_results: - for key in self.modely._model_def['Minimizers'].keys(): - if key in self.__process_results[name_data] and self.__process_results[name_data][key].poll() is None: + for key in self.modely._model_def["Minimizers"].keys(): + if ( + key in self.__process_results[name_data] + and self.__process_results[name_data][key].poll() is None + ): self.__process_results[name_data][key].terminate() self.__process_results[name_data][key].wait() self.__process_results[name_data][key] = None self.__process_results[name_data] = {} - for key in self.modely._model_def['Minimizers'].keys(): + for key in self.modely._model_def["Minimizers"].keys(): # Start the data visualizer process - self.__process_results[name_data][key] = subprocess.Popen(['python', self.__time_series_visualizer_script], stdin=subprocess.PIPE, - text=True) - np_data_A = np.array(self.modely.prediction[name_data][key]['A']) + self.__process_results[name_data][key] = subprocess.Popen( + ["python", self.__time_series_visualizer_script], + stdin=subprocess.PIPE, + text=True, + ) + np_data_A = np.array(self.modely.prediction[name_data][key]["A"]) if len(np_data_A.shape) > 3 and np_data_A.shape[1] > 30: - np_data_B = np.array(self.modely.prediction[name_data][key]['B']) + np_data_B = np.array(self.modely.prediction[name_data][key]["B"]) indices = np.linspace(0, np_data_A.shape[1] - 1, 30, dtype=int) data_A = np_data_A[:, indices, :, :].tolist() data_B = np_data_B[:, indices, :, :].tolist() - data_idxs = np.array(self.modely.prediction[name_data]['idxs'])[:,indices].tolist() + data_idxs = np.array(self.modely.prediction[name_data]["idxs"])[ + :, indices + ].tolist() else: - data_A = self.modely.prediction[name_data][key]['A'] - data_B = self.modely.prediction[name_data][key]['B'] - data_idxs = self.modely.prediction[name_data]['idxs'] if len(np_data_A.shape) > 3 else None + data_A = self.modely.prediction[name_data][key]["A"] + data_B = self.modely.prediction[name_data][key]["B"] + data_idxs = ( + self.modely.prediction[name_data]["idxs"] + if len(np_data_A.shape) > 3 + else None + ) - data = {"name_data": name_data, - "key": key, - "performance": self.modely.performance[name_data][key], - "prediction_A": data_A, - "prediction_B": data_B, - "data_idxs": data_idxs, - "sample_time": self.modely._model_def['Info']["SampleTime"]} + data = { + "name_data": name_data, + "key": key, + "performance": self.modely.performance[name_data][key], + "prediction_A": data_A, + "prediction_B": data_B, + "data_idxs": data_idxs, + "sample_time": self.modely._model_def["Info"]["SampleTime"], + } try: # Send data to the visualizer process - self.__process_results[name_data][key].stdin.write(f"{json.dumps(data)}\n") + self.__process_results[name_data][key].stdin.write( + f"{json.dumps(data)}\n" + ) self.__process_results[name_data][key].stdin.flush() self.__process_results[name_data][key].stdin.close() except: self.closeResult(self, name_data) log.warning(f"The visualizer {name_data} process has been closed.") - def showWeights(self, weights = None): + def showWeights(self, weights=None): pass - def showFunctions(self, functions = None, xlim = None, num_points = 1000): + def showFunctions(self, functions=None, xlim=None, num_points=1000): check(self.modely.neuralized, ValueError, "The model has not been neuralized.") - for key, value in self.modely._model_def['Functions'].items(): + for key, value in self.modely._model_def["Functions"].items(): if key in functions: - if key in self.__process_function and self.__process_function[key].poll() is None: + if ( + key in self.__process_function + and self.__process_function[key].poll() is None + ): self.__process_function[key].terminate() self.__process_function[key].wait() - if 'functions' in self.modely._model_def['Functions'][key]: + if "functions" in self.modely._model_def["Functions"][key]: x, activ_fun = return_fuzzify(value, xlim, num_points) - data = {"name": key, - "x": x, - "y": activ_fun, - "chan_centers": value['centers']} + data = { + "name": key, + "x": x, + "y": activ_fun, + "chan_centers": value["centers"], + } # Start the data visualizer process - self.__process_function[key] = subprocess.Popen(['python', self.__fuzzy_visualizer_script], - stdin=subprocess.PIPE, - text=True) - elif 'code': + self.__process_function[key] = subprocess.Popen( + ["python", self.__fuzzy_visualizer_script], + stdin=subprocess.PIPE, + text=True, + ) + elif "code": model_def = ModelDef(self.modely._model_def) model_def.updateParameters(self.modely._model) - function_inputs = return_standard_inputs(value, model_def, xlim, num_points) - function_output, function_input_list = return_function(value, function_inputs) + function_inputs = return_standard_inputs( + value, model_def, xlim, num_points + ) + function_output, function_input_list = return_function( + value, function_inputs + ) data = {"name": key} - if value['n_input'] == 2: - data['x0'] = function_inputs[0].reshape(num_points, num_points).tolist() - data['x1'] = function_inputs[1].reshape(num_points, num_points).tolist() - data['output'] = function_output.reshape(num_points, num_points).tolist() + if value["n_input"] == 2: + data["x0"] = ( + function_inputs[0].reshape(num_points, num_points).tolist() + ) + data["x1"] = ( + function_inputs[1].reshape(num_points, num_points).tolist() + ) + data["output"] = function_output.reshape( + num_points, num_points + ).tolist() else: - data['x0'] = function_inputs[0].reshape(num_points).tolist() - data['output'] = function_output.reshape(num_points).tolist() - data['params'] = [] - for i, key in enumerate(value['params_and_consts']): - data['params'] += [function_inputs[i+value['n_input']].tolist()] - data['input_names'] = function_input_list + data["x0"] = function_inputs[0].reshape(num_points).tolist() + data["output"] = function_output.reshape(num_points).tolist() + data["params"] = [] + for i, key in enumerate(value["params_and_consts"]): + data["params"] += [ + function_inputs[i + value["n_input"]].tolist() + ] + data["input_names"] = function_input_list # Start the data visualizer process - self.__process_function[key] = subprocess.Popen(['python', self.__function_visualizer_script], - stdin=subprocess.PIPE, - text=True) + self.__process_function[key] = subprocess.Popen( + ["python", self.__function_visualizer_script], + stdin=subprocess.PIPE, + text=True, + ) try: # Send data to the visualizer process self.__process_function[key].stdin.write(f"{json.dumps(data)}\n") @@ -183,7 +250,7 @@ def showFunctions(self, functions = None, xlim = None, num_points = 1000): self.closeFunctions() log.warning(f"The visualizer {functions} process has been closed.") - def closeFunctions(self, functions = None): + def closeFunctions(self, functions=None): if functions is None: for key in self.__process_function.keys(): self.__process_function[key].terminate() @@ -195,10 +262,14 @@ def closeFunctions(self, functions = None): self.__process_function[key].wait() self.__process_function.pop(key) - def closeTraining(self, minimizer = None): + def closeTraining(self, minimizer=None): if minimizer is None: - for key in self.modely._model_def['Minimizers'].keys(): - if key in self.__process_training and self.__process_training[key] is not None and self.__process_training[key].poll() is None: + for key in self.modely._model_def["Minimizers"].keys(): + if ( + key in self.__process_training + and self.__process_training[key] is not None + and self.__process_training[key].poll() is None + ): self.__process_training[key].terminate() self.__process_training[key].wait() self.__process_training[key] = None @@ -207,9 +278,13 @@ def closeTraining(self, minimizer = None): self.__process_training[minimizer].wait() self.__process_training.pop(minimizer) - def closeResult(self, name_data = None, minimizer = None): + def closeResult(self, name_data=None, minimizer=None): if name_data is None: - check(minimizer is None, ValueError, "If name_data is None, minimizer must be None.") + check( + minimizer is None, + ValueError, + "If name_data is None, minimizer must be None.", + ) for name_data in self.__process_results.keys(): for key in self.__process_results[name_data].keys(): self.__process_results[name_data][key].terminate() diff --git a/nnodely/visualizer/textvisualizer.py b/nnodely/visualizer/textvisualizer.py index f574abee..71e4db95 100644 --- a/nnodely/visualizer/textvisualizer.py +++ b/nnodely/visualizer/textvisualizer.py @@ -4,59 +4,71 @@ from nnodely.support.utils import is_notebook from nnodely.visualizer.emptyvisualizer import EmptyVisualizer, color, GREEN, RED, BLUE + class TextVisualizer(EmptyVisualizer): def __init__(self, verbose=1): self.verbose = verbose - def __title(self,msg, lenght = 80): - print(color((msg).center(lenght, '='), GREEN, True)) + def __title(self, msg, lenght=80): + print(color((msg).center(lenght, "="), GREEN, True)) - def __subtitle(self,msg, lenght = 80): - print(color((msg).center(lenght, '-'), GREEN, True)) + def __subtitle(self, msg, lenght=80): + print(color((msg).center(lenght, "-"), GREEN, True)) def __line(self): - print(color('='.center(80, '='),GREEN)) + print(color("=".center(80, "="), GREEN)) def __singleline(self): - print(color('-'.center(80, '-'),GREEN)) + print(color("-".center(80, "-"), GREEN)) - def __info(self,name, dim =30): - print(color((name).ljust(dim),BLUE)) + def __info(self, name, dim=30): + print(color((name).ljust(dim), BLUE)) - def __paramjson(self,name, value, dim =30): + def __paramjson(self, name, value, dim=30): lines = pformat(value, width=80 - dim).strip().splitlines() - vai = ('\n' + (' ' * dim)).join(x for x in lines) + vai = ("\n" + (" " * dim)).join(x for x in lines) # pformat(value).strip().splitlines().rjust(40) - print(color((name).ljust(dim) + vai,GREEN)) + print(color((name).ljust(dim) + vai, GREEN)) - def __param(self,name, value, dim =30): - print(color((name).ljust(dim) + value,GREEN)) + def __param(self, name, value, dim=30): + print(color((name).ljust(dim) + value, GREEN)) def showModel(self, model): if self.verbose >= 1: self.__title(" nnodely Model ") - print(color(pformat(model),GREEN)) + print(color(pformat(model), GREEN)) self.__line() - def showMinimize(self,variable_name): + def showMinimize(self, variable_name): if self.verbose >= 2: - self.__title(f" Minimize Error of {variable_name} between" - f" {self.modely._model_def['Minimizers'][variable_name]['A']} and" - f" {self.modely._model_def['Minimizers'][variable_name]['B']} with {self.modely._model_def['Minimizers'][variable_name]['loss']} ") + self.__title( + f" Minimize Error of {variable_name} between" + f" {self.modely._model_def['Minimizers'][variable_name]['A']} and" + f" {self.modely._model_def['Minimizers'][variable_name]['B']} with {self.modely._model_def['Minimizers'][variable_name]['loss']} " + ) self.__line() def showModelInputWindow(self): if self.verbose >= 2: - input_ns_backward = {key: value['ns'][0] for key, value in self.modely._model_def['Inputs'].items()} - input_ns_forward = {key: value['ns'][1] for key, value in self.modely._model_def['Inputs'].items()} + input_ns_backward = { + key: value["ns"][0] + for key, value in self.modely._model_def["Inputs"].items() + } + input_ns_forward = { + key: value["ns"][1] + for key, value in self.modely._model_def["Inputs"].items() + } self.__title(" nnodely Model Input Windows ") - #self.__paramjson("time_window_backward:",self.modely.input_tw_backward) - #self.__paramjson("time_window_forward:",self.modely.input_tw_forward) + # self.__paramjson("time_window_backward:",self.modely.input_tw_backward) + # self.__paramjson("time_window_forward:",self.modely.input_tw_forward) self.__paramjson("sample_window_backward:", input_ns_backward) self.__paramjson("sample_window_forward:", input_ns_forward) self.__paramjson("input_n_samples:", self.modely._input_n_samples) - self.__param("max_samples [backw, forw]:", f"[{self.modely._model_def['Info']['ns'][0]},{self.modely._model_def['Info']['ns'][1]}]") - self.__param("max_samples total:",f"{self.modely._max_n_samples}") + self.__param( + "max_samples [backw, forw]:", + f"[{self.modely._model_def['Info']['ns'][0]},{self.modely._model_def['Info']['ns'][1]}]", + ) + self.__param("max_samples total:", f"{self.modely._max_n_samples}") self.__line() def showModelRelationSamples(self): @@ -68,126 +80,205 @@ def showModelRelationSamples(self): def showBuiltModel(self): if self.verbose >= 2: self.__title(" nnodely Built Model ") - print(color(pformat(self.modely._model),GREEN)) + print(color(pformat(self.modely._model), GREEN)) self.__line() - def showWeights(self, weights = None): + def showWeights(self, weights=None): self.__title(" nnodely Models Weights ") for key, param in self.modely.parameters.items(): if weights is None or key in weights: - self.__paramjson(key,param) + self.__paramjson(key, param) self.__line() - def showWeightsInTrain(self, batch = None, epoch = None, weights = None): + def showWeightsInTrain(self, batch=None, epoch=None, weights=None): if self.verbose >= 2: par = self.modely.running_parameters - dim = len(self.modely._model_def['Minimizers']) + dim = len(self.modely._model_def["Minimizers"]) COLOR = BLUE if epoch is not None: - print(color('|' + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, ' ') + '|',COLOR), end='') - print(color((f' Params end epochs {epoch + 1} ').center(20 * (dim + 1) - 1, '-') + '|',COLOR)) + print( + color( + "|" + + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, " ") + + "|", + COLOR, + ), + end="", + ) + print( + color( + (f" Params end epochs {epoch + 1} ").center( + 20 * (dim + 1) - 1, "-" + ) + + "|", + COLOR, + ) + ) if batch is not None: - print(color('|' + (f"{batch + 1}").center(10, ' ') + '|', COLOR), end='') - print(color((f' Params end batch {batch + 1} ').center(20 * (dim + 1) - 1, '-') + '|', COLOR)) + print( + color("|" + (f"{batch + 1}").center(10, " ") + "|", COLOR), end="" + ) + print( + color( + (f" Params end batch {batch + 1} ").center( + 20 * (dim + 1) - 1, "-" + ) + + "|", + COLOR, + ) + ) for key, param in self.modely.parameters.items(): if weights is None or key in weights: - print(color('|' + (f"{key}").center(10, ' ') + '|', COLOR), end='') - print(color((f'{param}').center(20 * (dim + 1) - 1, ' ') + '|', COLOR)) + print(color("|" + (f"{key}").center(10, " ") + "|", COLOR), end="") + print( + color((f"{param}").center(20 * (dim + 1) - 1, " ") + "|", COLOR) + ) if epoch is not None: - print(color('|'+(f'').center(10+20*(dim+1), '-') + '|')) + print(color("|" + (f"").center(10 + 20 * (dim + 1), "-") + "|")) def showDataset(self, name): if self.verbose >= 1: self.__title(" nnodely Model Dataset ") self.__param("Dataset Name:", name) - self.__param("Number of files:", f'{self.modely._file_count}') - self.__param("Total number of samples:", f'{self.modely._num_of_samples[name]}') - for key in self.modely._model_def['Inputs'].keys(): + self.__param("Number of files:", f"{self.modely._file_count}") + self.__param( + "Total number of samples:", f"{self.modely._num_of_samples[name]}" + ) + for key in self.modely._model_def["Inputs"].keys(): if key in self.modely._data[name].keys(): - self.__param(f"Shape of {key}:", f'{self.modely._data[name][key].shape}') + self.__param( + f"Shape of {key}:", f"{self.modely._data[name][key].shape}" + ) self.__line() def showStartTraining(self): if self.verbose >= 1: par = self.modely.running_parameters - dim = len(self.modely._model_def['Minimizers']) - self.__title(" nnodely Training ", 12+(len(self.modely._model_def['Minimizers'])+1)*20) - print(color('|'+(f'Epoch').center(10,' ')+'|'),end='') - for key in self.modely._model_def['Minimizers'].keys(): - print(color((f'{key}').center(19, ' ') + '|'), end='') - print(color((f'Total').center(19, ' ') + '|')) - - print(color('|' + (f' ').center(10, ' ') + '|'), end='') - for key in self.modely._model_def['Minimizers'].keys(): - print(color((f'Loss').center(19, ' ') + '|'),end='') - print(color((f'Loss').center(19, ' ') + '|')) - - print(color('|' + (f' ').center(10, ' ') + '|'), end='') - for key in self.modely._model_def['Minimizers'].keys(): - if par['n_samples_val']: - print(color((f'train').center(9, ' ') + '|'),end='') - print(color((f'val').center(9, ' ') + '|'),end='') + dim = len(self.modely._model_def["Minimizers"]) + self.__title( + " nnodely Training ", + 12 + (len(self.modely._model_def["Minimizers"]) + 1) * 20, + ) + print(color("|" + (f"Epoch").center(10, " ") + "|"), end="") + for key in self.modely._model_def["Minimizers"].keys(): + print(color((f"{key}").center(19, " ") + "|"), end="") + print(color((f"Total").center(19, " ") + "|")) + + print(color("|" + (f" ").center(10, " ") + "|"), end="") + for key in self.modely._model_def["Minimizers"].keys(): + print(color((f"Loss").center(19, " ") + "|"), end="") + print(color((f"Loss").center(19, " ") + "|")) + + print(color("|" + (f" ").center(10, " ") + "|"), end="") + for key in self.modely._model_def["Minimizers"].keys(): + if par["n_samples_val"]: + print(color((f"train").center(9, " ") + "|"), end="") + print(color((f"val").center(9, " ") + "|"), end="") else: - print(color((f'train').center(19, ' ') + '|'), end='') - if par['n_samples_val']: - print(color((f'train').center(9, ' ') + '|'), end='') - print(color((f'val').center(9, ' ') + '|')) + print(color((f"train").center(19, " ") + "|"), end="") + if par["n_samples_val"]: + print(color((f"train").center(9, " ") + "|"), end="") + print(color((f"val").center(9, " ") + "|")) else: - print(color((f'train').center(19, ' ') + '|')) + print(color((f"train").center(19, " ") + "|")) - print(color('|'+(f'').center(10+20*(dim+1), '-') + '|')) + print(color("|" + (f"").center(10 + 20 * (dim + 1), "-") + "|")) def showTraining(self, epoch, train_losses, val_losses): if self.verbose >= 1: eng = lambda val: np.format_float_scientific(val, precision=3) par = self.modely.running_parameters - show_epoch = 1 if par['num_of_epochs'] <= 100 else int(par['num_of_epochs']/100) - dim = len(self.modely._model_def['Minimizers']) - if epoch < par['num_of_epochs']: - print('', end='\r') - print('|' + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, ' ') + '|', end='') + show_epoch = ( + 1 if par["num_of_epochs"] <= 100 else int(par["num_of_epochs"] / 100) + ) + dim = len(self.modely._model_def["Minimizers"]) + if epoch < par["num_of_epochs"]: + print("", end="\r") + print( + "|" + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, " ") + "|", + end="", + ) train_loss = [] val_loss = [] - for key in self.modely._model_def['Minimizers'].keys(): + for key in self.modely._model_def["Minimizers"].keys(): train_loss.append(train_losses[key][epoch]) if val_losses: val_loss.append(val_losses[key][epoch]) - print((f'{eng(train_losses[key][epoch])}').center(9, ' ') + '|', end='') - print((f'{eng(val_losses[key][epoch])}').center(9, ' ') + '|', end='') + print( + (f"{eng(train_losses[key][epoch])}").center(9, " ") + "|", + end="", + ) + print( + (f"{eng(val_losses[key][epoch])}").center(9, " ") + "|", + end="", + ) else: - print((f'{eng(train_losses[key][epoch])}').center(19, ' ') + '|', end='') + print( + (f"{eng(train_losses[key][epoch])}").center(19, " ") + "|", + end="", + ) if val_losses: - print((f'{eng(np.mean(train_loss))}').center(9, ' ') + '|', end='') - print((f'{eng(np.mean(val_loss))}').center(9, ' ') + '|', end='') + print((f"{eng(np.mean(train_loss))}").center(9, " ") + "|", end="") + print((f"{eng(np.mean(val_loss))}").center(9, " ") + "|", end="") else: - print((f'{eng(np.mean(train_loss))}').center(19, ' ') + '|', end='') + print((f"{eng(np.mean(train_loss))}").center(19, " ") + "|", end="") if (epoch + 1) % show_epoch == 0: - print('', end='\r') - print(color('|' + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, ' ') + '|'), end='') - for key in self.modely._model_def['Minimizers'].keys(): + print("", end="\r") + print( + color( + "|" + + (f"{epoch + 1}/{par['num_of_epochs']}").center(10, " ") + + "|" + ), + end="", + ) + for key in self.modely._model_def["Minimizers"].keys(): if val_losses: - print(color((f'{eng(train_losses[key][epoch])}').center(9, ' ') + '|'), end='') - print(color((f'{eng(val_losses[key][epoch])}').center(9, ' ') + '|'), end='') + print( + color( + (f"{eng(train_losses[key][epoch])}").center(9, " ") + + "|" + ), + end="", + ) + print( + color( + (f"{eng(val_losses[key][epoch])}").center(9, " ") + + "|" + ), + end="", + ) else: - print(color((f'{eng(train_losses[key][epoch])}').center(19, ' ') + '|'), end='') + print( + color( + (f"{eng(train_losses[key][epoch])}").center(19, " ") + + "|" + ), + end="", + ) if val_losses: - print(color((f'{eng(np.mean(train_loss))}').center(9, ' ') + '|'), end='') - print(color((f'{eng(np.mean(val_loss))}').center(9, ' ') + '|')) + print( + color((f"{eng(np.mean(train_loss))}").center(9, " ") + "|"), + end="", + ) + print(color((f"{eng(np.mean(val_loss))}").center(9, " ") + "|")) else: - print(color((f'{eng(np.mean(train_loss))}').center(19, ' ') + '|')) + print( + color((f"{eng(np.mean(train_loss))}").center(19, " ") + "|") + ) - if epoch+1 == par['num_of_epochs']: - print(color('|'+(f'').center(10+20*(dim+1), '-') + '|')) + if epoch + 1 == par["num_of_epochs"]: + print(color("|" + (f"").center(10 + 20 * (dim + 1), "-") + "|")) def showTrainingTime(self, time): if self.verbose >= 1: self.__title(" nnodely Training Time ") - self.__param("Total time of Training:", f'{time}') + self.__param("Total time of Training:", f"{time}") self.__line() def showTrainParams(self): @@ -195,93 +286,177 @@ def showTrainParams(self): self.__title(" nnodely Model Train Parameters ") par = self.modely.getTrainingInfo() - self.__paramjson("models:", par['models']) - self.__param("num of epochs:", str(par['num_of_epochs'])) - self.__param("update per epochs:", str(par['update_per_epochs'])) - if par['prediction_samples'] >= 0: + self.__paramjson("models:", par["models"]) + self.__param("num of epochs:", str(par["num_of_epochs"])) + self.__param("update per epochs:", str(par["update_per_epochs"])) + if par["prediction_samples"] >= 0: self.__info("└>len(train_indexes)//(batch_size+step)") else: self.__info("└>(n_samples-batch_size)/batch_size+1") - if par['shuffle_data']: - self.__param('shuffle data:', str(par['shuffle_data'])) + if par["shuffle_data"]: + self.__param("shuffle data:", str(par["shuffle_data"])) - if 'early_stopping' in par and par['early_stopping']: - self.__param('early stopping:', par['early_stopping']) - self.__paramjson('early stopping params:', par['early_stopping_params']) + if "early_stopping" in par and par["early_stopping"]: + self.__param("early stopping:", par["early_stopping"]) + self.__paramjson("early stopping params:", par["early_stopping_params"]) - if par['prediction_samples'] >= 0: + if par["prediction_samples"] >= 0: self.__param("prediction samples:", f"{par['prediction_samples']}") self.__param("step:", f"{par['train_step']}") - self.__paramjson("closed loop:", par['closed_loop']) - self.__paramjson("connect:", par['connect']) + self.__paramjson("closed loop:", par["closed_loop"]) + self.__paramjson("connect:", par["connect"]) self.__param("train dataset:", f"{par['train_tag']}") self.__param("\t- batch size:", f"{par['train_batch_size']}") self.__param("\t- num of samples:", f"{par['n_samples_train']}") - if par['prediction_samples'] >= 0: - self.__param("\t- num of first samples:", f"{par['n_first_samples_train']}") + if par["prediction_samples"] >= 0: + self.__param( + "\t- num of first samples:", f"{par['n_first_samples_train']}" + ) - if par['n_samples_val'] > 0: + if par["n_samples_val"] > 0: self.__param("validation dataset:", f"{par['val_tag']}") self.__param("\t- batch size:", f"{par['val_batch_size']}") self.__param("\t- num of samples:", f"{par['n_samples_val']}") - if par['prediction_samples'] >= 0: - self.__param("\t- num of first samples:", f"{par['n_first_samples_val']}") + if par["prediction_samples"] >= 0: + self.__param( + "\t- num of first samples:", f"{par['n_first_samples_val']}" + ) - if par['n_samples_test'] > 0: + if par["n_samples_test"] > 0: self.__param("test dataset:", f"{par['test_tag']}") self.__param("\t- num of samples:", f"{par['n_samples_test']}") - if 'test_batch_size' in par: + if "test_batch_size" in par: self.__param("\t- batch size:", f"{par['test_batch_size']}") - if par['prediction_samples'] >= 0: - self.__param("\t- num of first samples:", f"{par['n_first_samples_test']}") + if par["prediction_samples"] >= 0: + self.__param( + "\t- num of first samples:", f"{par['n_first_samples_test']}" + ) - self.__paramjson('minimizers:', par['minimizers']) + self.__paramjson("minimizers:", par["minimizers"]) - self.__param("optimizer:", par['optimizer']) - self.__paramjson("optimizer defaults:", par['optimizer_defaults']) - if par['optimizer_params'] is not None: - self.__paramjson("optimizer params:", par['optimizer_params']) + self.__param("optimizer:", par["optimizer"]) + self.__paramjson("optimizer defaults:", par["optimizer_defaults"]) + if par["optimizer_params"] is not None: + self.__paramjson("optimizer params:", par["optimizer_params"]) self.__line() def showResult(self, name_data): eng = lambda val: np.format_float_scientific(val, precision=3) if self.verbose >= 1: - dim_loss = max(5,len(max(self.modely._model_def['Minimizers'].keys(),key=len))) - loss_type_list = set([value["loss"] for ind, (key, value) in enumerate(self.modely._model_def['Minimizers'].items())]) - self.__title(f" nnodely Model Results for {name_data} ", dim_loss + 2 + (len(loss_type_list) + 2) * 20) - print(color('|' + (f'Loss').center(dim_loss, ' ') + '|'), end='') + dim_loss = max( + 5, len(max(self.modely._model_def["Minimizers"].keys(), key=len)) + ) + loss_type_list = set( + [ + value["loss"] + for ind, (key, value) in enumerate( + self.modely._model_def["Minimizers"].items() + ) + ] + ) + self.__title( + f" nnodely Model Results for {name_data} ", + dim_loss + 2 + (len(loss_type_list) + 2) * 20, + ) + print(color("|" + (f"Loss").center(dim_loss, " ") + "|"), end="") for loss in loss_type_list: - print(color((f'{loss}').center(19, ' ') + '|'), end='') - print(color((f'FVU').center(19, ' ') + '|'), end='') - print(color((f'AIC').center(19, ' ') + '|')) + print(color((f"{loss}").center(19, " ") + "|"), end="") + print(color((f"FVU").center(19, " ") + "|"), end="") + print(color((f"AIC").center(19, " ") + "|")) - print(color('|' + (f'').center(dim_loss, ' ') + '|'), end='') + print(color("|" + (f"").center(dim_loss, " ") + "|"), end="") for i in range(len(loss_type_list)): - print(color((f'small better').center(19, ' ') + '|'), end='') - print(color((f'small better').center(19, ' ') + '|'), end='') - print(color((f'lower better').center(19, ' ') + '|')) - - print(color('|' + (f'').center(dim_loss + 20 * (len(loss_type_list) + 2), '-') + '|')) - for ind, (key, value) in enumerate(self.modely._model_def['Minimizers'].items()): - print(color('|'+(f'{key}').center(dim_loss, ' ') + '|'), end='') + print(color((f"small better").center(19, " ") + "|"), end="") + print(color((f"small better").center(19, " ") + "|"), end="") + print(color((f"lower better").center(19, " ") + "|")) + + print( + color( + "|" + + (f"").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + + "|" + ) + ) + for ind, (key, value) in enumerate( + self.modely._model_def["Minimizers"].items() + ): + print(color("|" + (f"{key}").center(dim_loss, " ") + "|"), end="") for loss in list(loss_type_list): if value["loss"] == loss: - print(color((f'{eng(self.modely.performance[name_data][key][value["loss"]])}').center(19, ' ') + '|'), end='') + print( + color( + ( + f"{eng(self.modely.performance[name_data][key][value['loss']])}" + ).center(19, " ") + + "|" + ), + end="", + ) else: - print(color((f' ').center(19, ' ') + '|'), end='') - print(color((f'{eng(self.modely.performance[name_data][key]["fvu"]["total"])}').center(19, ' ') + '|'), end='') - print(color((f'{eng(self.modely.performance[name_data][key]["aic"]["value"])}').center(19, ' ') + '|')) - - print(color('|' + (f'').center(dim_loss + 20 * (len(loss_type_list) + 2), '-') + '|')) - print(color('|'+(f'Total').center(dim_loss, ' ') + '|'), end='') - print(color((f'{eng(self.modely.performance[name_data]["total"]["mean_error"])}').center(len(loss_type_list)*20-1, ' ') + '|'), end='') - print(color((f'{eng(self.modely.performance[name_data]["total"]["fvu"])}').center(19, ' ') + '|'), end='') - print(color((f'{eng(self.modely.performance[name_data]["total"]["aic"])}').center(19, ' ') + '|')) - - print(color('|' + (f'').center(dim_loss + 20 * (len(loss_type_list) + 2), '-') + '|')) + print(color((f" ").center(19, " ") + "|"), end="") + print( + color( + ( + f"{eng(self.modely.performance[name_data][key]['fvu']['total'])}" + ).center(19, " ") + + "|" + ), + end="", + ) + print( + color( + ( + f"{eng(self.modely.performance[name_data][key]['aic']['value'])}" + ).center(19, " ") + + "|" + ) + ) + + print( + color( + "|" + + (f"").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + + "|" + ) + ) + print(color("|" + (f"Total").center(dim_loss, " ") + "|"), end="") + print( + color( + ( + f"{eng(self.modely.performance[name_data]['total']['mean_error'])}" + ).center(len(loss_type_list) * 20 - 1, " ") + + "|" + ), + end="", + ) + print( + color( + ( + f"{eng(self.modely.performance[name_data]['total']['fvu'])}" + ).center(19, " ") + + "|" + ), + end="", + ) + print( + color( + ( + f"{eng(self.modely.performance[name_data]['total']['aic'])}" + ).center(19, " ") + + "|" + ) + ) + + print( + color( + "|" + + (f"").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + + "|" + ) + ) if self.verbose >= 2: self.__title(" Detalied Results ") @@ -316,4 +491,4 @@ def exportReport(self, name, path): if self.verbose >= 1: self.__title(f" Export {name} Report ") self.__param("Report exported in:", path) - self.__line() \ No newline at end of file + self.__line() diff --git a/setup.py b/setup.py index 0ceefd2c..7f68bd37 100644 --- a/setup.py +++ b/setup.py @@ -10,18 +10,20 @@ # with open(version_file, 'w') as f: # f.write(content_new) + def read_version(): - version_file = os.path.join(os.path.dirname(__file__), 'nnodely', '__init__.py') - with open(version_file, 'r') as f: + version_file = os.path.join(os.path.dirname(__file__), "nnodely", "__init__.py") + with open(version_file, "r") as f: for line in f: - if line.startswith('__version__'): + if line.startswith("__version__"): delim = '"' if '"' in line else "'" return line.split(delim)[1] raise RuntimeError("Unable to find version string.") + setup( - name='nnodely', + name="nnodely", version=read_version(), packages=find_packages(exclude=["docs*", "tests*", "imgs*"]), - include_package_data=True -) \ No newline at end of file + include_package_data=True, +) diff --git a/tests/__init__.py b/tests/__init__.py index 9d1b47f2..210b6ce1 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,4 +3,5 @@ # Imposta un backend non-GUI solo su Windows if sys.platform.startswith("win"): import matplotlib - matplotlib.use("Agg") \ No newline at end of file + + matplotlib.use("Agg") diff --git a/tests/test_dataset.py b/tests/test_dataset.py index d497d0c8..fa60f1b0 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -14,458 +14,1333 @@ # This file test the data loading in particular: # The shape and the value of the inputs -train_folder = os.path.join(os.path.dirname(__file__), 'data/') -val_folder = os.path.join(os.path.dirname(__file__), 'val_data/') -test_folder = os.path.join(os.path.dirname(__file__), 'test_data/') +train_folder = os.path.join(os.path.dirname(__file__), "data/") +val_folder = os.path.join(os.path.dirname(__file__), "val_data/") +test_folder = os.path.join(os.path.dirname(__file__), "test_data/") + class ModelyCreateDatasetTest(unittest.TestCase): - def test_build_dataset_simple(self): NeuObj.clearNames() - input = Input('in1') - output = Input('out') + input = Input("in1") + output = Input("out") relation = Fir(input.tw(0.05)) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), relation) + test.addMinimize("out", output.z(-1), relation) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time'] - test.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10,5,1), test._data['dataset_1']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['in1'][0].tolist()) - - self.assertEqual((10,1,1), test._data['dataset_1']['out'].shape) - self.assertEqual([[1.225]], test._data['dataset_1']['out'][0].tolist()) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset_1']['out'].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "theta", + "time", + ] + test.loadData( + name="dataset_1", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 5, 1), test._data["dataset_1"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["dataset_1"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 1, 1), test._data["dataset_1"]["out"].shape) + self.assertEqual([[1.225]], test._data["dataset_1"]["out"][0].tolist()) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["dataset_1"]["out"].tolist(), + ) def test_build_dataset_tuple(self): NeuObj.clearNames() - inputA = Input('inA') - inputB = Input('inB') - out = Output('out', Fir(inputA.tw(0.05)+inputB.tw(0.05))) + inputA = Input("inA") + inputB = Input("inB") + out = Output("out", Fir(inputA.tw(0.05) + inputB.tw(0.05))) test = Modely(visualizer=None) - test.addModel('out', out) + test.addModel("out", out) test.neuralizeModel(0.01) - data_struct = ['','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3',('inA','inB'),'theta','time'] - test.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((11,5,1), test._data['dataset_1']['inA'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['inA'][0].tolist()) - self.assertEqual((11,5,1), test._data['dataset_1']['inB'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['inB'][0].tolist()) - - out = Output('out2', Fir(inputA.tw(0.05))) + data_struct = [ + "", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + ("inA", "inB"), + "theta", + "time", + ] + test.loadData( + name="dataset_1", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((11, 5, 1), test._data["dataset_1"]["inA"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["dataset_1"]["inA"][0].tolist(), + ) + self.assertEqual((11, 5, 1), test._data["dataset_1"]["inB"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["dataset_1"]["inB"][0].tolist(), + ) + + out = Output("out2", Fir(inputA.tw(0.05))) test2 = Modely(visualizer=None) - test2.addModel('out2', out) + test2.addModel("out2", out) test2.neuralizeModel(0.01) - data_struct = ['','y1','x2','y2','','A1x','A1y','B1x','B1y','',('A2x','f'),'A2y','B2x','out','','x3',('inA','inB'),'theta','time'] - test2.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((11,5,1), test._data['dataset_1']['inA'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset_1']['inA'][0].tolist()) + data_struct = [ + "", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + ("A2x", "f"), + "A2y", + "B2x", + "out", + "", + "x3", + ("inA", "inB"), + "theta", + "time", + ] + test2.loadData( + name="dataset_1", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((11, 5, 1), test._data["dataset_1"]["inA"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["dataset_1"]["inA"][0].tolist(), + ) def test_build_dataset_tuple_dim(self): NeuObj.clearNames() - inputA1 = Input('inA1') - inputA = Input('inA', dimensions=3) - inputB = Input('inB', dimensions=3) - out = Output('out', inputA1.sw(1)+Fir(Linear(inputA.tw(0.05)+inputB.tw(0.05)))) + inputA1 = Input("inA1") + inputA = Input("inA", dimensions=3) + inputB = Input("inB", dimensions=3) + out = Output( + "out", inputA1.sw(1) + Fir(Linear(inputA.tw(0.05) + inputB.tw(0.05))) + ) test = Modely(visualizer=None) - test.addModel('out', out) + test.addModel("out", out) test.neuralizeModel(0.01) - data_struct = ['','y1','x2','y2','',('A1x','inA1'),'A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3',('inA','inB'),'theta','time'] - test.loadData(name='dataset_1', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((11,5,3), test._data['dataset_1']['inA'].shape) - self.assertEqual([[0.984,12.493, 0.0],[0.983,12.493, 0.01],[0.982,12.495, 0.02],[0.98,12.498, 0.03],[0.977,12.502, 0.04]], test._data['dataset_1']['inA'][0].tolist()) - self.assertEqual((11,5,3), test._data['dataset_1']['inB'].shape) - self.assertEqual([[0.984, 12.493, 0.0], [0.983, 12.493, 0.01], [0.982, 12.495, 0.02], [0.98, 12.498, 0.03], - [0.977, 12.502, 0.04]], test._data['dataset_1']['inB'][0].tolist()) + data_struct = [ + "", + "y1", + "x2", + "y2", + "", + ("A1x", "inA1"), + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + ("inA", "inB"), + "theta", + "time", + ] + test.loadData( + name="dataset_1", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((11, 5, 3), test._data["dataset_1"]["inA"].shape) + self.assertEqual( + [ + [0.984, 12.493, 0.0], + [0.983, 12.493, 0.01], + [0.982, 12.495, 0.02], + [0.98, 12.498, 0.03], + [0.977, 12.502, 0.04], + ], + test._data["dataset_1"]["inA"][0].tolist(), + ) + self.assertEqual((11, 5, 3), test._data["dataset_1"]["inB"].shape) + self.assertEqual( + [ + [0.984, 12.493, 0.0], + [0.983, 12.493, 0.01], + [0.982, 12.495, 0.02], + [0.98, 12.498, 0.03], + [0.977, 12.502, 0.04], + ], + test._data["dataset_1"]["inB"][0].tolist(), + ) with self.assertRaises(ValueError): - data_struct = ['','y1','x2','y2','',('inA1','inA'),'A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3',('inA','inB'),'theta','time'] - test.loadData(name='dataset_2', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) + data_struct = [ + "", + "y1", + "x2", + "y2", + "", + ("inA1", "inA"), + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + ("inA", "inB"), + "theta", + "time", + ] + test.loadData( + name="dataset_2", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) with self.assertRaises(ValueError): - data_struct = ['','y1','x2','y2','',('inA1','inA'),'A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','inB','theta','time'] - test.loadData(name='dataset_3', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) + data_struct = [ + "", + "y1", + "x2", + "y2", + "", + ("inA1", "inA"), + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "inB", + "theta", + "time", + ] + test.loadData( + name="dataset_3", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) def test_build_multi_dataset_simple(self): NeuObj.clearNames() - input = Input('in1') - output = Input('out') + input = Input("in1") + output = Input("out") relation = Fir(input.tw(0.05)) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), relation) + test.addMinimize("out", output.z(-1), relation) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time'] - - test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "theta", + "time", + ] + + test.loadData( + name="train_dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="validation_dataset", + source=val_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="test_dataset", + source=test_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) self.assertEqual(3, test._Loader__n_datasets) - self.assertEqual((10,5,1), test._data['train_dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['train_dataset']['in1'][0].tolist()) - self.assertEqual((6,5,1), test._data['validation_dataset']['in1'].shape) - self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877]], test._data['validation_dataset']['in1'][0].tolist()) - self.assertEqual((8,5,1), test._data['test_dataset']['in1'].shape) - self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777]], test._data['test_dataset']['in1'][0].tolist()) - - self.assertEqual((10,1,1), test._data['train_dataset']['out'].shape) - self.assertEqual([[1.225]], test._data['train_dataset']['out'][0].tolist()) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['train_dataset']['out'].tolist()) - self.assertEqual((6,1,1), test._data['validation_dataset']['out'].shape) - self.assertEqual([[2.225]], test._data['validation_dataset']['out'][0].tolist()) - self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], test._data['validation_dataset']['out'].tolist()) - self.assertEqual((8,1,1), test._data['test_dataset']['out'].shape) - self.assertEqual([[3.225]], test._data['test_dataset']['out'][0].tolist()) - self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]], [[3.207]]], test._data['test_dataset']['out'].tolist()) - + self.assertEqual((10, 5, 1), test._data["train_dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["train_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((6, 5, 1), test._data["validation_dataset"]["in1"].shape) + self.assertEqual( + [[0.884], [0.883], [0.882], [0.88], [0.877]], + test._data["validation_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((8, 5, 1), test._data["test_dataset"]["in1"].shape) + self.assertEqual( + [[0.784], [0.783], [0.782], [0.78], [0.777]], + test._data["test_dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 1, 1), test._data["train_dataset"]["out"].shape) + self.assertEqual([[1.225]], test._data["train_dataset"]["out"][0].tolist()) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["train_dataset"]["out"].tolist(), + ) + self.assertEqual((6, 1, 1), test._data["validation_dataset"]["out"].shape) + self.assertEqual([[2.225]], test._data["validation_dataset"]["out"][0].tolist()) + self.assertEqual( + [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], + test._data["validation_dataset"]["out"].tolist(), + ) + self.assertEqual((8, 1, 1), test._data["test_dataset"]["out"].shape) + self.assertEqual([[3.225]], test._data["test_dataset"]["out"][0].tolist()) + self.assertEqual( + [ + [[3.225]], + [[3.224]], + [[3.222]], + [[3.22]], + [[3.217]], + [[3.214]], + [[3.211]], + [[3.207]], + ], + test._data["test_dataset"]["out"].tolist(), + ) + def test_build_dataset_medium1(self): NeuObj.clearNames() - input = Input('in1') - output = Input('out') + input = Input("in1") + output = Input("out") rel1 = Fir(input.tw(0.05)) rel2 = Fir(input.tw(0.01)) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1 + rel2) + test.addMinimize("out", output.z(-1), rel1 + rel2) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10,5,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset']['in1'][0].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "theta", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["dataset"]["out"].tolist(), + ) - self.assertEqual((10,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist()) - def test_build_multi_dataset_medium1(self): NeuObj.clearNames() - input = Input('in1') - output = Input('out') + input = Input("in1") + output = Input("out") rel1 = Fir(input.tw(0.05)) rel2 = Fir(input.tw(0.01)) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1 + rel2) + test.addMinimize("out", output.z(-1), rel1 + rel2) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','theta','time'] - - test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "theta", + "time", + ] + + test.loadData( + name="train_dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="validation_dataset", + source=val_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="test_dataset", + source=test_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) self.assertEqual(3, test._Loader__n_datasets) - self.assertEqual((10,5,1), test._data['train_dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['train_dataset']['in1'][0].tolist()) - self.assertEqual((6,5,1), test._data['validation_dataset']['in1'].shape) - self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877]], test._data['validation_dataset']['in1'][0].tolist()) - self.assertEqual((8,5,1), test._data['test_dataset']['in1'].shape) - self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777]], test._data['test_dataset']['in1'][0].tolist()) - - self.assertEqual((10,1,1), test._data['train_dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['train_dataset']['out'].tolist()) - self.assertEqual((6,1,1), test._data['validation_dataset']['out'].shape) - self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], test._data['validation_dataset']['out'].tolist()) - self.assertEqual((8,1,1), test._data['test_dataset']['out'].shape) - self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]], [[3.207]]], test._data['test_dataset']['out'].tolist()) - + self.assertEqual((10, 5, 1), test._data["train_dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["train_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((6, 5, 1), test._data["validation_dataset"]["in1"].shape) + self.assertEqual( + [[0.884], [0.883], [0.882], [0.88], [0.877]], + test._data["validation_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((8, 5, 1), test._data["test_dataset"]["in1"].shape) + self.assertEqual( + [[0.784], [0.783], [0.782], [0.78], [0.777]], + test._data["test_dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 1, 1), test._data["train_dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["train_dataset"]["out"].tolist(), + ) + self.assertEqual((6, 1, 1), test._data["validation_dataset"]["out"].shape) + self.assertEqual( + [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], + test._data["validation_dataset"]["out"].tolist(), + ) + self.assertEqual((8, 1, 1), test._data["test_dataset"]["out"].shape) + self.assertEqual( + [ + [[3.225]], + [[3.224]], + [[3.222]], + [[3.22]], + [[3.217]], + [[3.214]], + [[3.211]], + [[3.207]], + ], + test._data["test_dataset"]["out"].tolist(), + ) + def test_build_dataset_medium2(self): NeuObj.clearNames() - input1 = Input('in1') - input2 = Input('in2') - output = Input('out') + input1 = Input("in1") + input2 = Input("in2") + output = Input("out") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input1.tw(0.01)) rel3 = Fir(input2.tw(0.02)) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1 + rel2 + rel3) + test.addMinimize("out", output.z(-1), rel1 + rel2 + rel3) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10,5,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977]], test._data['dataset']['in1'][0].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 2, 1), test._data["dataset"]["in2"].shape) + self.assertEqual([[12.498], [12.502]], test._data["dataset"]["in2"][0].tolist()) + + self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["dataset"]["out"].tolist(), + ) - self.assertEqual((10,2,1), test._data['dataset']['in2'].shape) - self.assertEqual([[12.498], [12.502]], test._data['dataset']['in2'][0].tolist()) - - self.assertEqual((10,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist()) - def test_build_dataset_complex1(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) - rel2 = Fir(input1.tw([-0.01,0.02])) + rel2 = Fir(input1.tw([-0.01, 0.02])) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1 + rel2) + test.addMinimize("out", output.z(-1), rel1 + rel2) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((9,7,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist()) - - self.assertEqual((9,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['dataset']['out'].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((9, 7, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((9, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + ], + test._data["dataset"]["out"].tolist(), + ) def test_build_multi_dataset_complex1(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) - rel2 = Fir(input1.tw([-0.01,0.02])) + rel2 = Fir(input1.tw([-0.01, 0.02])) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1 + rel2) + test.addMinimize("out", output.z(-1), rel1 + rel2) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - - test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + + test.loadData( + name="train_dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="validation_dataset", + source=val_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="test_dataset", + source=test_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) self.assertEqual(3, test._Loader__n_datasets) - self.assertEqual((9,7,1), test._data['train_dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['train_dataset']['in1'][0].tolist()) - self.assertEqual((5,7,1), test._data['validation_dataset']['in1'].shape) - self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877],[0.873],[0.869]], test._data['validation_dataset']['in1'][0].tolist()) - self.assertEqual((7,7,1), test._data['test_dataset']['in1'].shape) - self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777],[0.773],[0.769]], test._data['test_dataset']['in1'][0].tolist()) - - self.assertEqual((9,1,1), test._data['train_dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['train_dataset']['out'].tolist()) - self.assertEqual((5,1,1), test._data['validation_dataset']['out'].shape) - self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]]], test._data['validation_dataset']['out'].tolist()) - self.assertEqual((7,1,1), test._data['test_dataset']['out'].shape) - self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]]], test._data['test_dataset']['out'].tolist()) - + self.assertEqual((9, 7, 1), test._data["train_dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]], + test._data["train_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((5, 7, 1), test._data["validation_dataset"]["in1"].shape) + self.assertEqual( + [[0.884], [0.883], [0.882], [0.88], [0.877], [0.873], [0.869]], + test._data["validation_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((7, 7, 1), test._data["test_dataset"]["in1"].shape) + self.assertEqual( + [[0.784], [0.783], [0.782], [0.78], [0.777], [0.773], [0.769]], + test._data["test_dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((9, 1, 1), test._data["train_dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + ], + test._data["train_dataset"]["out"].tolist(), + ) + self.assertEqual((5, 1, 1), test._data["validation_dataset"]["out"].shape) + self.assertEqual( + [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]]], + test._data["validation_dataset"]["out"].tolist(), + ) + self.assertEqual((7, 1, 1), test._data["test_dataset"]["out"].shape) + self.assertEqual( + [ + [[3.225]], + [[3.224]], + [[3.222]], + [[3.22]], + [[3.217]], + [[3.214]], + [[3.211]], + ], + test._data["test_dataset"]["out"].tolist(), + ) + def test_build_dataset_complex2(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) - rel2 = Fir(input1.tw([-0.01,0.01])) + rel2 = Fir(input1.tw([-0.01, 0.01])) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1 + rel2) + test.addMinimize("out", output.z(-1), rel1 + rel2) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10,6,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973]], test._data['dataset']['in1'][0].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 6, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["dataset"]["out"].tolist(), + ) - self.assertEqual((10,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist()) - def test_build_dataset_complex3(self): NeuObj.clearNames() - input1 = Input('in1') - input2 = Input('in2') - output = Input('out') + input1 = Input("in1") + input2 = Input("in2") + output = Input("out") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input2.tw(0.02)) - rel3 = Fir(input1.tw([-0.01,0.01])) + rel3 = Fir(input1.tw([-0.01, 0.01])) rel4 = Fir(input2.last()) - fun = Output('out-net',rel1+rel2+rel3+rel4) + fun = Output("out-net", rel1 + rel2 + rel3 + rel4) test = Modely(visualizer=None) - test.addModel('fun', fun) - test.addMinimize('out', output.z(-1), rel1 + rel2 + rel3 + rel4) + test.addModel("fun", fun) + test.addMinimize("out", output.z(-1), rel1 + rel2 + rel3 + rel4) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10,6,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973]], test._data['dataset']['in1'][0].tolist()) - - self.assertEqual((10,2,1), test._data['dataset']['in2'].shape) - self.assertEqual([[12.498], [12.502]], test._data['dataset']['in2'][0].tolist()) - - self.assertEqual((10,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['dataset']['out'].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 6, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 2, 1), test._data["dataset"]["in2"].shape) + self.assertEqual([[12.498], [12.502]], test._data["dataset"]["in2"][0].tolist()) + + self.assertEqual((10, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["dataset"]["out"].tolist(), + ) def test_build_multi_dataset_complex3(self): NeuObj.clearNames() - input1 = Input('in1') - input2 = Input('in2') - output = Input('out') + input1 = Input("in1") + input2 = Input("in2") + output = Input("out") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input2.tw(0.02)) - rel3 = Fir(input1.tw([-0.01,0.01])) + rel3 = Fir(input1.tw([-0.01, 0.01])) rel4 = Fir(input2.last()) - fun = Output('out-net',rel1+rel2+rel3+rel4) + fun = Output("out-net", rel1 + rel2 + rel3 + rel4) test = Modely(visualizer=None) - test.addModel('fun',fun) - test.addMinimize('out', output.z(-1), rel1 + rel2 + rel3 + rel4) + test.addModel("fun", fun) + test.addMinimize("out", output.z(-1), rel1 + rel2 + rel3 + rel4) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="train_dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="validation_dataset", + source=val_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="test_dataset", + source=test_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) self.assertEqual(3, test._Loader__n_datasets) - self.assertEqual((10,6,1), test._data['train_dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973]], test._data['train_dataset']['in1'][0].tolist()) - self.assertEqual((6,6,1), test._data['validation_dataset']['in1'].shape) - self.assertEqual([[0.884],[0.883],[0.882],[0.88],[0.877],[0.873]], test._data['validation_dataset']['in1'][0].tolist()) - self.assertEqual((8,6,1), test._data['test_dataset']['in1'].shape) - self.assertEqual([[0.784],[0.783],[0.782],[0.78],[0.777],[0.773]], test._data['test_dataset']['in1'][0].tolist()) - - self.assertEqual((10,2,1), test._data['train_dataset']['in2'].shape) - self.assertEqual([[12.498], [12.502]], test._data['train_dataset']['in2'][0].tolist()) - self.assertEqual((6,2,1), test._data['validation_dataset']['in2'].shape) - self.assertEqual([[12.498], [12.502]], test._data['validation_dataset']['in2'][0].tolist()) - self.assertEqual((8,2,1), test._data['test_dataset']['in2'].shape) - self.assertEqual([[12.498], [12.502]], test._data['test_dataset']['in2'][0].tolist()) - - self.assertEqual((10,1,1), test._data['train_dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]], [[1.2]]], test._data['train_dataset']['out'].tolist()) - self.assertEqual((6,1,1), test._data['validation_dataset']['out'].shape) - self.assertEqual([[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], test._data['validation_dataset']['out'].tolist()) - self.assertEqual((8,1,1), test._data['test_dataset']['out'].shape) - self.assertEqual([[[3.225]], [[3.224]], [[3.222]], [[3.22]], [[3.217]], [[3.214]], [[3.211]], [[3.207]]], test._data['test_dataset']['out'].tolist()) - + self.assertEqual((10, 6, 1), test._data["train_dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973]], + test._data["train_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((6, 6, 1), test._data["validation_dataset"]["in1"].shape) + self.assertEqual( + [[0.884], [0.883], [0.882], [0.88], [0.877], [0.873]], + test._data["validation_dataset"]["in1"][0].tolist(), + ) + self.assertEqual((8, 6, 1), test._data["test_dataset"]["in1"].shape) + self.assertEqual( + [[0.784], [0.783], [0.782], [0.78], [0.777], [0.773]], + test._data["test_dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((10, 2, 1), test._data["train_dataset"]["in2"].shape) + self.assertEqual( + [[12.498], [12.502]], test._data["train_dataset"]["in2"][0].tolist() + ) + self.assertEqual((6, 2, 1), test._data["validation_dataset"]["in2"].shape) + self.assertEqual( + [[12.498], [12.502]], test._data["validation_dataset"]["in2"][0].tolist() + ) + self.assertEqual((8, 2, 1), test._data["test_dataset"]["in2"].shape) + self.assertEqual( + [[12.498], [12.502]], test._data["test_dataset"]["in2"][0].tolist() + ) + + self.assertEqual((10, 1, 1), test._data["train_dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + [[1.2]], + ], + test._data["train_dataset"]["out"].tolist(), + ) + self.assertEqual((6, 1, 1), test._data["validation_dataset"]["out"].shape) + self.assertEqual( + [[[2.225]], [[2.224]], [[2.222]], [[2.22]], [[2.217]], [[2.214]]], + test._data["validation_dataset"]["out"].tolist(), + ) + self.assertEqual((8, 1, 1), test._data["test_dataset"]["out"].shape) + self.assertEqual( + [ + [[3.225]], + [[3.224]], + [[3.222]], + [[3.22]], + [[3.217]], + [[3.214]], + [[3.211]], + [[3.207]], + ], + test._data["test_dataset"]["out"].tolist(), + ) + def test_build_dataset_complex5(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) - rel2 = Fir(input1.tw([-0.01,0.01])) - rel3 = Fir(input1.tw([-0.02,0.02])) - fun = Output('out-net',rel1+rel2+rel3) + rel2 = Fir(input1.tw([-0.01, 0.01])) + rel3 = Fir(input1.tw([-0.02, 0.02])) + fun = Output("out-net", rel1 + rel2 + rel3) test = Modely(visualizer=None) - test.addModel('fun', fun) - test.addMinimize('out', output.z(-1), fun) + test.addModel("fun", fun) + test.addMinimize("out", output.z(-1), fun) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((9,7,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist()) - - self.assertEqual((9,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['dataset']['out'].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((9, 7, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((9, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + ], + test._data["dataset"]["out"].tolist(), + ) def test_filter_data(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) - rel2 = Fir(input1.tw([-0.01,0.01])) - rel3 = Fir(input1.tw([-0.02,0.02])) - fun = Output('out-net',rel1+rel2+rel3) + rel2 = Fir(input1.tw([-0.01, 0.01])) + rel3 = Fir(input1.tw([-0.02, 0.02])) + fun = Output("out-net", rel1 + rel2 + rel3) test = Modely(visualizer=None) - test.addModel('fun', fun) - test.addMinimize('out', output.z(-1), fun) + test.addModel("fun", fun) + test.addMinimize("out", output.z(-1), fun) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) def filter_fn(sample): - return min(sample['in1']) > 0.957 + return min(sample["in1"]) > 0.957 test.filterData(filter_fn) - self.assertEqual((2,7,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist()) - - self.assertEqual((2,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]]], test._data['dataset']['out'].tolist()) - - test.loadData(name='dataset2', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.filterData(filter_fn, dataset_name='dataset2') - self.assertEqual((2,7,1), test._data['dataset2']['in1'].shape) - self.assertEqual((2, 1, 1), test._data['dataset2']['out'].shape) + self.assertEqual((2, 7, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((2, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual([[[1.225]], [[1.224]]], test._data["dataset"]["out"].tolist()) + + test.loadData( + name="dataset2", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.filterData(filter_fn, dataset_name="dataset2") + self.assertEqual((2, 7, 1), test._data["dataset2"]["in1"].shape) + self.assertEqual((2, 1, 1), test._data["dataset2"]["out"].shape) def test_build_dataset_complex6(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) - rel2 = Fir(input1.tw([-0.01,0.02])) - rel3 = Fir(input1.tw([-0.05,0.01])) - fun = Output('out-net',rel1+rel2+rel3) + rel2 = Fir(input1.tw([-0.01, 0.02])) + rel3 = Fir(input1.tw([-0.05, 0.01])) + fun = Output("out-net", rel1 + rel2 + rel3) test = Modely(visualizer=None) - test.addModel('fun',fun) - test.addMinimize('out', output.z(-1), fun) + test.addModel("fun", fun) + test.addMinimize("out", output.z(-1), fun) test.neuralizeModel(0.01) - data_struct = ['x1','y1','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((9,7,1), test._data['dataset']['in1'].shape) - self.assertEqual([[0.984],[0.983],[0.982],[0.98],[0.977],[0.973],[0.969]], test._data['dataset']['in1'][0].tolist()) - - self.assertEqual((9,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[1.225]], [[1.224]], [[1.222]], [[1.22]], [[1.217]], [[1.214]], [[1.211]], [[1.207]], [[1.204]]], test._data['dataset']['out'].tolist()) + data_struct = [ + "x1", + "y1", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((9, 7, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [[0.984], [0.983], [0.982], [0.98], [0.977], [0.973], [0.969]], + test._data["dataset"]["in1"][0].tolist(), + ) + + self.assertEqual((9, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [ + [[1.225]], + [[1.224]], + [[1.222]], + [[1.22]], + [[1.217]], + [[1.214]], + [[1.211]], + [[1.207]], + [[1.204]], + ], + test._data["dataset"]["out"].tolist(), + ) def test_build_dataset_custom(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) - rel2 = Fir(input1.tw([-0.01,0.02])) - rel3 = Fir(input1.tw([-0.05,0.01])) - fun = Output('out-net',rel1+rel2+rel3) + rel2 = Fir(input1.tw([-0.01, 0.02])) + rel3 = Fir(input1.tw([-0.05, 0.01])) + fun = Output("out-net", rel1 + rel2 + rel3) test = Modely(visualizer=None) - test.addModel('fun',fun) - test.addMinimize('out', output.z(-1), fun) + test.addModel("fun", fun) + test.addMinimize("out", output.z(-1), fun) test.neuralizeModel(0.01) data_x = np.array(range(10)) data_a = 2 data_b = -3 - dataset = {'in1': data_x, 'out': (data_a*data_x) + data_b} - - test.loadData(name='dataset',source=dataset) - self.assertEqual((4,7,1), test._data['dataset']['in1'].shape) - self.assertEqual([[[0],[1],[2],[3],[4],[5],[6]], - [[1],[2],[3],[4],[5],[6],[7]], - [[2],[3],[4],[5],[6],[7],[8]], - [[3],[4],[5],[6],[7],[8],[9]]], - test._data['dataset']['in1'].tolist()) - - self.assertEqual((4,1,1), test._data['dataset']['out'].shape) - self.assertEqual([[[7]],[[9]],[[11]],[[13]]], test._data['dataset']['out'].tolist()) + dataset = {"in1": data_x, "out": (data_a * data_x) + data_b} + + test.loadData(name="dataset", source=dataset) + self.assertEqual((4, 7, 1), test._data["dataset"]["in1"].shape) + self.assertEqual( + [ + [[0], [1], [2], [3], [4], [5], [6]], + [[1], [2], [3], [4], [5], [6], [7]], + [[2], [3], [4], [5], [6], [7], [8]], + [[3], [4], [5], [6], [7], [8], [9]], + ], + test._data["dataset"]["in1"].tolist(), + ) + + self.assertEqual((4, 1, 1), test._data["dataset"]["out"].shape) + self.assertEqual( + [[[7]], [[9]], [[11]], [[13]]], test._data["dataset"]["out"].tolist() + ) def test_build_multi_dataset_custom(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') + input1 = Input("in1") + output = Input("out") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input1.tw([-0.01, 0.02])) rel3 = Fir(input1.tw([-0.05, 0.01])) - fun = Output('out-net', rel1 + rel2 + rel3) + fun = Output("out-net", rel1 + rel2 + rel3) test = Modely(visualizer=None) - test.addModel('fun',fun) - test.addMinimize('out', output.z(-1), fun) + test.addModel("fun", fun) + test.addMinimize("out", output.z(-1), fun) test.neuralizeModel(0.01) train_data_x = np.array(range(10)) @@ -473,174 +1348,247 @@ def test_build_multi_dataset_custom(self): test_data_x = np.array(range(20, 30)) data_a = 2 data_b = -3 - train_dataset = {'in1': train_data_x, 'out': (data_a * train_data_x) + data_b} - val_dataset = {'in1': val_data_x, 'out': (data_a * val_data_x) + data_b} - test_dataset = {'in1': test_data_x, 'out': (data_a * test_data_x) + data_b} + train_dataset = {"in1": train_data_x, "out": (data_a * train_data_x) + data_b} + val_dataset = {"in1": val_data_x, "out": (data_a * val_data_x) + data_b} + test_dataset = {"in1": test_data_x, "out": (data_a * test_data_x) + data_b} - test.loadData(name='train_dataset', source=train_dataset) - test.loadData(name='val_dataset', source=val_dataset) - test.loadData(name='test_dataset', source=test_dataset) + test.loadData(name="train_dataset", source=train_dataset) + test.loadData(name="val_dataset", source=val_dataset) + test.loadData(name="test_dataset", source=test_dataset) self.assertEqual(3, test._Loader__n_datasets) - self.assertEqual((4, 7, 1), test._data['train_dataset']['in1'].shape) - self.assertEqual([[[0], [1], [2], [3], [4], [5], [6]], - [[1], [2], [3], [4], [5], [6], [7]], - [[2], [3], [4], [5], [6], [7], [8]], - [[3], [4], [5], [6], [7], [8], [9]]], - test._data['train_dataset']['in1'].tolist()) - self.assertEqual((4, 7, 1), test._data['val_dataset']['in1'].shape) - self.assertEqual([[[10], [11], [12], [13], [14], [15], [16]], - [[11], [12], [13], [14], [15], [16], [17]], - [[12], [13], [14], [15], [16], [17], [18]], - [[13], [14], [15], [16], [17], [18], [19]]], - test._data['val_dataset']['in1'].tolist()) - self.assertEqual((4, 7, 1), test._data['test_dataset']['in1'].shape) - self.assertEqual([[[20], [21], [22], [23], [24], [25], [26]], - [[21], [22], [23], [24], [25], [26], [27]], - [[22], [23], [24], [25], [26], [27], [28]], - [[23], [24], [25], [26], [27], [28], [29]]], - test._data['test_dataset']['in1'].tolist()) - - self.assertEqual((4, 1, 1), test._data['train_dataset']['out'].shape) - self.assertEqual([[[7]], [[9]], [[11]], [[13]]], test._data['train_dataset']['out'].tolist()) - self.assertEqual((4, 1, 1), test._data['val_dataset']['out'].shape) - self.assertEqual([[[27]], [[29]], [[31]], [[33]]], test._data['val_dataset']['out'].tolist()) - self.assertEqual((4, 1, 1), test._data['test_dataset']['out'].shape) - self.assertEqual([[[47]], [[49]], [[51]], [[53]]], test._data['test_dataset']['out'].tolist()) + self.assertEqual((4, 7, 1), test._data["train_dataset"]["in1"].shape) + self.assertEqual( + [ + [[0], [1], [2], [3], [4], [5], [6]], + [[1], [2], [3], [4], [5], [6], [7]], + [[2], [3], [4], [5], [6], [7], [8]], + [[3], [4], [5], [6], [7], [8], [9]], + ], + test._data["train_dataset"]["in1"].tolist(), + ) + self.assertEqual((4, 7, 1), test._data["val_dataset"]["in1"].shape) + self.assertEqual( + [ + [[10], [11], [12], [13], [14], [15], [16]], + [[11], [12], [13], [14], [15], [16], [17]], + [[12], [13], [14], [15], [16], [17], [18]], + [[13], [14], [15], [16], [17], [18], [19]], + ], + test._data["val_dataset"]["in1"].tolist(), + ) + self.assertEqual((4, 7, 1), test._data["test_dataset"]["in1"].shape) + self.assertEqual( + [ + [[20], [21], [22], [23], [24], [25], [26]], + [[21], [22], [23], [24], [25], [26], [27]], + [[22], [23], [24], [25], [26], [27], [28]], + [[23], [24], [25], [26], [27], [28], [29]], + ], + test._data["test_dataset"]["in1"].tolist(), + ) + + self.assertEqual((4, 1, 1), test._data["train_dataset"]["out"].shape) + self.assertEqual( + [[[7]], [[9]], [[11]], [[13]]], test._data["train_dataset"]["out"].tolist() + ) + self.assertEqual((4, 1, 1), test._data["val_dataset"]["out"].shape) + self.assertEqual( + [[[27]], [[29]], [[31]], [[33]]], test._data["val_dataset"]["out"].tolist() + ) + self.assertEqual((4, 1, 1), test._data["test_dataset"]["out"].shape) + self.assertEqual( + [[[47]], [[49]], [[51]], [[53]]], test._data["test_dataset"]["out"].tolist() + ) def test_vector_input_dataset(self): NeuObj.clearNames() - x = Input('x', dimensions=4) - y = Input('y', dimensions=3) - k = Input('k', dimensions=2) - w = Input('w') + x = Input("x", dimensions=4) + y = Input("y", dimensions=3) + k = Input("k", dimensions=2) + w = Input("w") - - out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) - out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05,offset=-0.02))))) + out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) + out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02))))) test = Modely(visualizer=None) - test.addMinimize('out', out, out2) + test.addMinimize("out", out, out2) test.neuralizeModel(0.01) ## Custom dataset - data_x = np.transpose(np.array( - [np.linspace(1,100,100, dtype=np.float32), - np.linspace(2, 101, 100, dtype=np.float32), - np.linspace(3, 102, 100, dtype=np.float32), - np.linspace(4, 103, 100, dtype=np.float32)])) - data_y = np.transpose(np.array( - [np.linspace(1,100,100, dtype=np.float32) + 10, - np.linspace(2, 101, 100, dtype=np.float32) + 10, - np.linspace(3, 102, 100, dtype=np.float32) + 10])) - data_k = np.transpose(np.array( - [np.linspace(1,100,100, dtype=np.float32) + 20, - np.linspace(2, 101, 100, dtype=np.float32) + 20])) - data_w = np.linspace(1,100,100, dtype=np.float32) + 30 - dataset = {'x': data_x, 'y': data_y, 'w': data_w, 'k': data_k} - - test.loadData(name='dataset', source=dataset) - - self.assertEqual((96, 2, 4), test._data['dataset']['x'].shape) - self.assertEqual((96, 2, 3), test._data['dataset']['y'].shape) - self.assertEqual((96, 1, 2), test._data['dataset']['k'].shape) - self.assertEqual((96, 5, 1), test._data['dataset']['w'].shape) - - self.assertEqual([[4.0, 5.0, 6.0, 7.0], [5.0, 6.0, 7.0, 8.0]], - test._data['dataset']['x'][0].tolist()) - self.assertEqual([[5.0, 6.0, 7.0, 8.0],[6.0, 7.0, 8.0, 9.0]], - test._data['dataset']['x'][1].tolist()) - self.assertEqual([[99, 100, 101, 102], [100, 101, 102, 103]], - test._data['dataset']['x'][-1].tolist()) - self.assertEqual([[98, 99, 100, 101],[99, 100, 101, 102]], - test._data['dataset']['x'][-2].tolist()) - - self.assertEqual([[14.0, 15.0, 16.0], [15.0, 16.0, 17.0]], - test._data['dataset']['y'][0].tolist()) - self.assertEqual([[15.0, 16.0, 17.0], [16.0, 17.0, 18.0]], - test._data['dataset']['y'][1].tolist()) - self.assertEqual([[109, 110, 111], [110, 111, 112]], - test._data['dataset']['y'][-1].tolist()) - self.assertEqual([[108, 109, 110], [109, 110, 111]], - test._data['dataset']['y'][-2].tolist()) - - self.assertEqual([[25.0, 26.0]], - test._data['dataset']['k'][0].tolist()) - self.assertEqual([[26.0, 27.0]], - test._data['dataset']['k'][1].tolist()) - self.assertEqual([[120, 121]], - test._data['dataset']['k'][-1].tolist()) - self.assertEqual([[119, 120]], - test._data['dataset']['k'][-2].tolist()) - - self.assertEqual([[31], [32], [33], [34], [35]], - test._data['dataset']['w'][0].tolist()) - self.assertEqual([[32], [33], [34], [35], [36]], - test._data['dataset']['w'][1].tolist()) - self.assertEqual([[126], [127], [128], [129], [130]], - test._data['dataset']['w'][-1].tolist()) - self.assertEqual([[125], [126], [127], [128], [129]], - test._data['dataset']['w'][-2].tolist()) + data_x = np.transpose( + np.array( + [ + np.linspace(1, 100, 100, dtype=np.float32), + np.linspace(2, 101, 100, dtype=np.float32), + np.linspace(3, 102, 100, dtype=np.float32), + np.linspace(4, 103, 100, dtype=np.float32), + ] + ) + ) + data_y = np.transpose( + np.array( + [ + np.linspace(1, 100, 100, dtype=np.float32) + 10, + np.linspace(2, 101, 100, dtype=np.float32) + 10, + np.linspace(3, 102, 100, dtype=np.float32) + 10, + ] + ) + ) + data_k = np.transpose( + np.array( + [ + np.linspace(1, 100, 100, dtype=np.float32) + 20, + np.linspace(2, 101, 100, dtype=np.float32) + 20, + ] + ) + ) + data_w = np.linspace(1, 100, 100, dtype=np.float32) + 30 + dataset = {"x": data_x, "y": data_y, "w": data_w, "k": data_k} + + test.loadData(name="dataset", source=dataset) + + self.assertEqual((96, 2, 4), test._data["dataset"]["x"].shape) + self.assertEqual((96, 2, 3), test._data["dataset"]["y"].shape) + self.assertEqual((96, 1, 2), test._data["dataset"]["k"].shape) + self.assertEqual((96, 5, 1), test._data["dataset"]["w"].shape) + + self.assertEqual( + [[4.0, 5.0, 6.0, 7.0], [5.0, 6.0, 7.0, 8.0]], + test._data["dataset"]["x"][0].tolist(), + ) + self.assertEqual( + [[5.0, 6.0, 7.0, 8.0], [6.0, 7.0, 8.0, 9.0]], + test._data["dataset"]["x"][1].tolist(), + ) + self.assertEqual( + [[99, 100, 101, 102], [100, 101, 102, 103]], + test._data["dataset"]["x"][-1].tolist(), + ) + self.assertEqual( + [[98, 99, 100, 101], [99, 100, 101, 102]], + test._data["dataset"]["x"][-2].tolist(), + ) + + self.assertEqual( + [[14.0, 15.0, 16.0], [15.0, 16.0, 17.0]], + test._data["dataset"]["y"][0].tolist(), + ) + self.assertEqual( + [[15.0, 16.0, 17.0], [16.0, 17.0, 18.0]], + test._data["dataset"]["y"][1].tolist(), + ) + self.assertEqual( + [[109, 110, 111], [110, 111, 112]], test._data["dataset"]["y"][-1].tolist() + ) + self.assertEqual( + [[108, 109, 110], [109, 110, 111]], test._data["dataset"]["y"][-2].tolist() + ) + + self.assertEqual([[25.0, 26.0]], test._data["dataset"]["k"][0].tolist()) + self.assertEqual([[26.0, 27.0]], test._data["dataset"]["k"][1].tolist()) + self.assertEqual([[120, 121]], test._data["dataset"]["k"][-1].tolist()) + self.assertEqual([[119, 120]], test._data["dataset"]["k"][-2].tolist()) + + self.assertEqual( + [[31], [32], [33], [34], [35]], test._data["dataset"]["w"][0].tolist() + ) + self.assertEqual( + [[32], [33], [34], [35], [36]], test._data["dataset"]["w"][1].tolist() + ) + self.assertEqual( + [[126], [127], [128], [129], [130]], test._data["dataset"]["w"][-1].tolist() + ) + self.assertEqual( + [[125], [126], [127], [128], [129]], test._data["dataset"]["w"][-2].tolist() + ) def test_vector_input_dataset_files(self): NeuObj.clearNames() - x = Input('x', dimensions=4) - y = Input('y', dimensions=3) - k = Input('k', dimensions=2) - w = Input('w') + x = Input("x", dimensions=4) + y = Input("y", dimensions=3) + k = Input("k", dimensions=2) + w = Input("w") - out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) - out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05,offset=-0.02))))) + out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) + out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02))))) test = Modely(visualizer=None) - test.addMinimize('out', out, out2) + test.addMinimize("out", out, out2) test.neuralizeModel(0.01) - data_folder = os.path.join(os.path.dirname(__file__), 'vector_data/') - data_struct = ['x', 'y', '','', '', '', 'k', '', '', '', 'w'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1, delimiter='\t', header=None) - - self.assertEqual((22, 2, 4), test._data['dataset']['x'].shape) - self.assertEqual((22, 2, 3), test._data['dataset']['y'].shape) - self.assertEqual((22, 1, 2), test._data['dataset']['k'].shape) - self.assertEqual((22, 5, 1), test._data['dataset']['w'].shape) - - self.assertEqual([[0.804, 0.825, 0.320, 0.488], [0.805, 0.825, 0.322, 0.485]], - test._data['dataset']['x'][0].tolist()) - self.assertEqual([[0.805, 0.825, 0.322, 0.485],[0.806, 0.824, 0.325, 0.481]], - test._data['dataset']['x'][1].tolist()) - self.assertEqual([[0.806, 0.824, 0.325, 0.481], [0.807, 0.823, 0.329, 0.477]], - test._data['dataset']['x'][-1].tolist()) - self.assertEqual([[0.805, 0.825, 0.322, 0.485],[0.806, 0.824, 0.325, 0.481]], - test._data['dataset']['x'][-2].tolist()) - - self.assertEqual([[0.350, 1.375, 0.586], [0.350, 1.375, 0.585]], - test._data['dataset']['y'][0].tolist()) - self.assertEqual([[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]], - test._data['dataset']['y'][1].tolist()) - self.assertEqual([[0.350, 1.375, 0.584], [0.350, 1.375, 0.582]], - test._data['dataset']['y'][-1].tolist()) - self.assertEqual([[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]], - test._data['dataset']['y'][-2].tolist()) - - self.assertEqual([[0.714, 1.227]], - test._data['dataset']['k'][0].tolist()) - self.assertEqual([[0.712, 1.225]], - test._data['dataset']['k'][1].tolist()) - self.assertEqual([[0.710, 1.224]], - test._data['dataset']['k'][-1].tolist()) - self.assertEqual([[0.712, 1.225]], - test._data['dataset']['k'][-2].tolist()) - - self.assertEqual([[12.493], [12.493], [12.495], [12.498], [12.502]], - test._data['dataset']['w'][0].tolist()) - self.assertEqual([[12.493], [12.495], [12.498], [12.502], [12.508]], - test._data['dataset']['w'][1].tolist()) - self.assertEqual([[12.495], [12.498], [12.502], [12.508], [12.515]], - test._data['dataset']['w'][-1].tolist()) - self.assertEqual([[12.493], [12.495], [12.498], [12.502], [12.508]], - test._data['dataset']['w'][-2].tolist()) + data_folder = os.path.join(os.path.dirname(__file__), "vector_data/") + data_struct = ["x", "y", "", "", "", "", "k", "", "", "", "w"] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=1, + delimiter="\t", + header=None, + ) + + self.assertEqual((22, 2, 4), test._data["dataset"]["x"].shape) + self.assertEqual((22, 2, 3), test._data["dataset"]["y"].shape) + self.assertEqual((22, 1, 2), test._data["dataset"]["k"].shape) + self.assertEqual((22, 5, 1), test._data["dataset"]["w"].shape) + + self.assertEqual( + [[0.804, 0.825, 0.320, 0.488], [0.805, 0.825, 0.322, 0.485]], + test._data["dataset"]["x"][0].tolist(), + ) + self.assertEqual( + [[0.805, 0.825, 0.322, 0.485], [0.806, 0.824, 0.325, 0.481]], + test._data["dataset"]["x"][1].tolist(), + ) + self.assertEqual( + [[0.806, 0.824, 0.325, 0.481], [0.807, 0.823, 0.329, 0.477]], + test._data["dataset"]["x"][-1].tolist(), + ) + self.assertEqual( + [[0.805, 0.825, 0.322, 0.485], [0.806, 0.824, 0.325, 0.481]], + test._data["dataset"]["x"][-2].tolist(), + ) + + self.assertEqual( + [[0.350, 1.375, 0.586], [0.350, 1.375, 0.585]], + test._data["dataset"]["y"][0].tolist(), + ) + self.assertEqual( + [[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]], + test._data["dataset"]["y"][1].tolist(), + ) + self.assertEqual( + [[0.350, 1.375, 0.584], [0.350, 1.375, 0.582]], + test._data["dataset"]["y"][-1].tolist(), + ) + self.assertEqual( + [[0.350, 1.375, 0.585], [0.350, 1.375, 0.584]], + test._data["dataset"]["y"][-2].tolist(), + ) + + self.assertEqual([[0.714, 1.227]], test._data["dataset"]["k"][0].tolist()) + self.assertEqual([[0.712, 1.225]], test._data["dataset"]["k"][1].tolist()) + self.assertEqual([[0.710, 1.224]], test._data["dataset"]["k"][-1].tolist()) + self.assertEqual([[0.712, 1.225]], test._data["dataset"]["k"][-2].tolist()) + + self.assertEqual( + [[12.493], [12.493], [12.495], [12.498], [12.502]], + test._data["dataset"]["w"][0].tolist(), + ) + self.assertEqual( + [[12.493], [12.495], [12.498], [12.502], [12.508]], + test._data["dataset"]["w"][1].tolist(), + ) + self.assertEqual( + [[12.495], [12.498], [12.502], [12.508], [12.515]], + test._data["dataset"]["w"][-1].tolist(), + ) + self.assertEqual( + [[12.493], [12.495], [12.498], [12.502], [12.508]], + test._data["dataset"]["w"][-2].tolist(), + ) ## Load from file ## Try to train the model @@ -649,326 +1597,1145 @@ def test_vector_input_dataset_files(self): def test_multifiles(self): NeuObj.clearNames() - x = Input('x') + x = Input("x") relation = Fir()(x.tw(0.05)) relation.closedLoop(x) - output = Output('out', relation) + output = Output("out", relation) test = Modely(visualizer=None, log_internal=True) - test.addModel('model', output) - test.addMinimize('error', output, x.next()) + test.addModel("model", output) + test.addMinimize("error", output, x.next()) test.neuralizeModel(0.01) ## The folder contains 3 files with 10, 20 and 30 samples respectively - data_struct = ['x'] + data_struct = ["x"] ## each folder contains 3 files with 10, 20 and 30 samples respectively - data_folder = os.path.join(os.path.dirname(__file__), 'multifile/') - data_folder2 = os.path.join(os.path.dirname(__file__), 'multifile2/') + data_folder = os.path.join(os.path.dirname(__file__), "multifile/") + data_folder2 = os.path.join(os.path.dirname(__file__), "multifile2/") ## this folder contains only one file with 50 samples - data_folder3 = os.path.join(os.path.dirname(__file__), 'multifile3/') - test.loadData(name='dataset1', source=data_folder, format=data_struct, skiplines=1) - test.loadData(name='dataset2', source=data_folder2, format=data_struct, skiplines=1) - test.loadData(name='dataset3', source=data_folder3, format=data_struct, skiplines=1) - - self.assertListEqual(list(test._data['dataset1']['x'].shape), [45, 6, 1]) - self.assertListEqual(test._multifile['dataset1'], [5, 20, 45]) - self.assertListEqual(list(test._data['dataset2']['x'].shape), [45, 6, 1]) - self.assertListEqual(test._multifile['dataset2'], [5, 20, 45]) - self.assertListEqual(list(test._data['dataset3']['x'].shape), [45, 6, 1]) - self.assertEqual(test._num_of_samples['dataset1'], 45) ## 5 + 15 + 25 - self.assertEqual(test._num_of_samples['dataset2'], 45) ## 5 + 15 + 25 - self.assertEqual(test._num_of_samples['dataset3'], 45) ## 50 - 5 + data_folder3 = os.path.join(os.path.dirname(__file__), "multifile3/") + test.loadData( + name="dataset1", source=data_folder, format=data_struct, skiplines=1 + ) + test.loadData( + name="dataset2", source=data_folder2, format=data_struct, skiplines=1 + ) + test.loadData( + name="dataset3", source=data_folder3, format=data_struct, skiplines=1 + ) + + self.assertListEqual(list(test._data["dataset1"]["x"].shape), [45, 6, 1]) + self.assertListEqual(test._multifile["dataset1"], [5, 20, 45]) + self.assertListEqual(list(test._data["dataset2"]["x"].shape), [45, 6, 1]) + self.assertListEqual(test._multifile["dataset2"], [5, 20, 45]) + self.assertListEqual(list(test._data["dataset3"]["x"].shape), [45, 6, 1]) + self.assertEqual(test._num_of_samples["dataset1"], 45) ## 5 + 15 + 25 + self.assertEqual(test._num_of_samples["dataset2"], 45) ## 5 + 15 + 25 + self.assertEqual(test._num_of_samples["dataset3"], 45) ## 50 - 5 ## train one dataset using splits - test.trainModel(dataset='dataset1', splits=[80, 10, 10], prediction_samples=3, num_of_epochs=1) + test.trainModel( + dataset="dataset1", + splits=[80, 10, 10], + prediction_samples=3, + num_of_epochs=1, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 36) ## 45 * 0.8 - self.assertEqual(tp['n_samples_val'], 4) ## 45 * 0.1 - self.assertEqual(tp['n_samples_test'], 5) ## 45 * 0.1 - self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]) - self.assertEqual(test.running_parameters['val_indexes'], [0]) + self.assertEqual(tp["n_samples_train"], 36) ## 45 * 0.8 + self.assertEqual(tp["n_samples_val"], 4) ## 45 * 0.1 + self.assertEqual(tp["n_samples_test"], 5) ## 45 * 0.1 + self.assertEqual( + test.running_parameters["train_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + ], + ) + self.assertEqual(test.running_parameters["val_indexes"], [0]) ## train using one dataset for train and one for validation - test.trainModel(train_dataset='dataset1', validation_dataset='dataset2', prediction_samples=3, num_of_epochs=1) + test.trainModel( + train_dataset="dataset1", + validation_dataset="dataset2", + prediction_samples=3, + num_of_epochs=1, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 45) - self.assertEqual(tp['n_samples_val'], 45) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41]) - self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41]) + self.assertEqual(tp["n_samples_train"], 45) + self.assertEqual(tp["n_samples_val"], 45) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual( + test.running_parameters["train_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + ], + ) + self.assertEqual( + test.running_parameters["val_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + ], + ) ## train using two dataset for train and one for validation - test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset='dataset3', prediction_samples=3, num_of_epochs=1) + test.trainModel( + train_dataset=["dataset1", "dataset2"], + validation_dataset="dataset3", + prediction_samples=3, + num_of_epochs=1, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45 - self.assertEqual(tp['n_samples_val'], 45) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86]) - self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41]) + self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45 + self.assertEqual(tp["n_samples_val"], 45) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual( + test.running_parameters["train_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + ], + ) + self.assertEqual( + test.running_parameters["val_indexes"], + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + ], + ) ## train using two dataset for train and two for validation - test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3'], prediction_samples=3, num_of_epochs=1) + test.trainModel( + train_dataset=["dataset1", "dataset2"], + validation_dataset=["dataset2", "dataset3"], + prediction_samples=3, + num_of_epochs=1, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45 - self.assertEqual(tp['n_samples_val'], 90) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86]) - self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86]) + self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45 + self.assertEqual(tp["n_samples_val"], 90) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual( + test.running_parameters["train_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + ], + ) + self.assertEqual( + test.running_parameters["val_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + ], + ) ## train using two dataset for train and two for validation (dataset4 is ignored) - test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3','dataset4'], num_of_epochs=1, prediction_samples=3) + test.trainModel( + train_dataset=["dataset1", "dataset2"], + validation_dataset=["dataset2", "dataset3", "dataset4"], + num_of_epochs=1, + prediction_samples=3, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45 - self.assertEqual(tp['n_samples_val'], 90) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86]) - self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86]) + self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45 + self.assertEqual(tp["n_samples_val"], 90) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual( + test.running_parameters["train_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + ], + ) + self.assertEqual( + test.running_parameters["val_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + ], + ) ## Use all datasets by default test.trainModel(splits=[80, 10, 10], prediction_samples=3) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 108) ## (45+45+45) * 0.8 - self.assertEqual(tp['n_samples_val'], 14) ## 135 * 0.1 - self.assertEqual(tp['n_samples_test'], 13) ## 135 * 0.1 - self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104]) - self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + self.assertEqual(tp["n_samples_train"], 108) ## (45+45+45) * 0.8 + self.assertEqual(tp["n_samples_val"], 14) ## 135 * 0.1 + self.assertEqual(tp["n_samples_test"], 13) ## 135 * 0.1 + self.assertEqual( + test.running_parameters["train_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + ], + ) + self.assertEqual( + test.running_parameters["val_indexes"], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ) ## splits multifile - test.trainModel(dataset=['dataset1', 'dataset2', 'dataset3'], splits=[80, 10, 10], prediction_samples=3) + test.trainModel( + dataset=["dataset1", "dataset2", "dataset3"], + splits=[80, 10, 10], + prediction_samples=3, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 108) ## (45+45+45) * 0.8 - self.assertEqual(tp['n_samples_val'], 14) ## 90 * 0.1 - self.assertEqual(tp['n_samples_test'], 13) ## 90 * 0.1 - self.assertEqual(test.running_parameters['train_indexes'], [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 45, 46, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104]) - self.assertEqual(test.running_parameters['val_indexes'], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + self.assertEqual(tp["n_samples_train"], 108) ## (45+45+45) * 0.8 + self.assertEqual(tp["n_samples_val"], 14) ## 90 * 0.1 + self.assertEqual(tp["n_samples_test"], 13) ## 90 * 0.1 + self.assertEqual( + test.running_parameters["train_indexes"], + [ + 0, + 1, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + ], + ) + self.assertEqual( + test.running_parameters["val_indexes"], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ) ## train one dataset using splits - test.trainModel(dataset='dataset1', splits=[80, 10, 10], num_of_epochs=1) + test.trainModel(dataset="dataset1", splits=[80, 10, 10], num_of_epochs=1) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 36) ## 45 * 0.8 - self.assertEqual(tp['n_samples_val'], 4) ## 45 * 0.1 - self.assertEqual(tp['n_samples_test'], 5) ## 45 * 0.1 - self.assertEqual(test.running_parameters['train_indexes'], list(range(36))) - self.assertEqual(test.running_parameters['val_indexes'], list(range(4))) + self.assertEqual(tp["n_samples_train"], 36) ## 45 * 0.8 + self.assertEqual(tp["n_samples_val"], 4) ## 45 * 0.1 + self.assertEqual(tp["n_samples_test"], 5) ## 45 * 0.1 + self.assertEqual(test.running_parameters["train_indexes"], list(range(36))) + self.assertEqual(test.running_parameters["val_indexes"], list(range(4))) ## train using one dataset for train and one for validation - test.trainModel(train_dataset='dataset1', validation_dataset='dataset2', num_of_epochs=1) + test.trainModel( + train_dataset="dataset1", validation_dataset="dataset2", num_of_epochs=1 + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 45) - self.assertEqual(tp['n_samples_val'], 45) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], list(range(45))) - self.assertEqual(test.running_parameters['val_indexes'], list(range(45))) + self.assertEqual(tp["n_samples_train"], 45) + self.assertEqual(tp["n_samples_val"], 45) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual(test.running_parameters["train_indexes"], list(range(45))) + self.assertEqual(test.running_parameters["val_indexes"], list(range(45))) ## train using two dataset for train and one for validation - test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset='dataset3', num_of_epochs=1) + test.trainModel( + train_dataset=["dataset1", "dataset2"], + validation_dataset="dataset3", + num_of_epochs=1, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45 - self.assertEqual(tp['n_samples_val'], 45) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], list(range(90))) - self.assertEqual(test.running_parameters['val_indexes'], list(range(45))) + self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45 + self.assertEqual(tp["n_samples_val"], 45) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual(test.running_parameters["train_indexes"], list(range(90))) + self.assertEqual(test.running_parameters["val_indexes"], list(range(45))) ## train using two dataset for train and two for validation - test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3'], num_of_epochs=1) + test.trainModel( + train_dataset=["dataset1", "dataset2"], + validation_dataset=["dataset2", "dataset3"], + num_of_epochs=1, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45 - self.assertEqual(tp['n_samples_val'], 90) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], list(range(90))) - self.assertEqual(test.running_parameters['val_indexes'], list(range(90))) + self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45 + self.assertEqual(tp["n_samples_val"], 90) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual(test.running_parameters["train_indexes"], list(range(90))) + self.assertEqual(test.running_parameters["val_indexes"], list(range(90))) ## train using two dataset for train and two for validation (dataset4 is ignored) - test.trainModel(train_dataset=['dataset1', 'dataset2'], validation_dataset=['dataset2','dataset3','dataset4'], num_of_epochs=1) + test.trainModel( + train_dataset=["dataset1", "dataset2"], + validation_dataset=["dataset2", "dataset3", "dataset4"], + num_of_epochs=1, + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 90) ## 45 + 45 - self.assertEqual(tp['n_samples_val'], 90) - self.assertEqual(tp['n_samples_test'], 0) - self.assertEqual(test.running_parameters['train_indexes'], list(range(90))) - self.assertEqual(test.running_parameters['val_indexes'], list(range(90))) + self.assertEqual(tp["n_samples_train"], 90) ## 45 + 45 + self.assertEqual(tp["n_samples_val"], 90) + self.assertEqual(tp["n_samples_test"], 0) + self.assertEqual(test.running_parameters["train_indexes"], list(range(90))) + self.assertEqual(test.running_parameters["val_indexes"], list(range(90))) ## splits multifile - test.trainModel(dataset=['dataset1', 'dataset2', 'dataset3'], splits=[80, 10, 10]) + test.trainModel( + dataset=["dataset1", "dataset2", "dataset3"], splits=[80, 10, 10] + ) tp = test.getTrainingInfo() - self.assertEqual(tp['n_samples_train'], 108) ## (45+45+45) * 0.8 - self.assertEqual(tp['n_samples_val'], 14) ## 90 * 0.1 - self.assertEqual(tp['n_samples_test'], 13) ## 90 * 0.1 - self.assertEqual(test.running_parameters['train_indexes'], list(range(108))) - self.assertEqual(test.running_parameters['val_indexes'], list(range(14))) + self.assertEqual(tp["n_samples_train"], 108) ## (45+45+45) * 0.8 + self.assertEqual(tp["n_samples_val"], 14) ## 90 * 0.1 + self.assertEqual(tp["n_samples_test"], 13) ## 90 * 0.1 + self.assertEqual(test.running_parameters["train_indexes"], list(range(108))) + self.assertEqual(test.running_parameters["val_indexes"], list(range(14))) def test_multifiles_2(self): NeuObj.clearNames() - x = Input('x') - y = Input('y') - relation = Fir()(x.tw(0.05))+Fir(y.sw([-2,2])) + x = Input("x") + y = Input("y") + relation = Fir()(x.tw(0.05)) + Fir(y.sw([-2, 2])) relation.closedLoop(x) - output = Output('out', relation) + output = Output("out", relation) test = Modely(visualizer=None, log_internal=True) - test.addModel('model', output) - test.addMinimize('error', output, x.next()) + test.addModel("model", output) + test.addMinimize("error", output, x.next()) test.neuralizeModel(0.01) ## The folder contains 3 files with 10, 20 and 30 samples respectively - data_struct = ['x', 'y'] - data_folder = os.path.join(os.path.dirname(__file__), 'multifile/') - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1) - self.assertListEqual(list(test._data['dataset']['x'].shape), [42, 6, 1]) - self.assertListEqual(list(test._data['dataset']['y'].shape), [42, 4, 1]) - self.assertListEqual(test._multifile['dataset'], [4, 18, 42]) + data_struct = ["x", "y"] + data_folder = os.path.join(os.path.dirname(__file__), "multifile/") + test.loadData( + name="dataset", source=data_folder, format=data_struct, skiplines=1 + ) + self.assertListEqual(list(test._data["dataset"]["x"].shape), [42, 6, 1]) + self.assertListEqual(list(test._data["dataset"]["y"].shape), [42, 4, 1]) + self.assertListEqual(test._multifile["dataset"], [4, 18, 42]) def test_dataframe_multidimensional(self): import pandas as pd + NeuObj.clearNames() - x = Input('x', dimensions=4) - y = Input('y', dimensions=3) - k = Input('k', dimensions=2) - w = Input('w') + x = Input("x", dimensions=4) + y = Input("y", dimensions=3) + k = Input("k", dimensions=2) + w = Input("w") - out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) - out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05,offset=-0.02))))) + out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) + out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02))))) test = Modely(visualizer=None) - test.addMinimize('out', out, out2) + test.addMinimize("out", out, out2) test.neuralizeModel(0.01) # Create a DataFrame with random values for each input - df = pd.DataFrame({ - 'x': [np.array([1.0,2.0,3.0,4.0]) for _ in range(10)], - 'y': [np.array([5.0,6.0,7.0]) for _ in range(10)], - 'k': [np.array([8.0,9.0]) for _ in range(10)], - 'w': np.array([10.0,11.0,12.0,13.0,14.0,15.0,16.0,17.0,18.0,19.0])}) - - test.loadData(name='dataset', source=df) - self.assertEqual((6, 2, 4), test._data['dataset']['x'].shape) - self.assertEqual((6, 2, 3), test._data['dataset']['y'].shape) - self.assertEqual((6, 1, 2), test._data['dataset']['k'].shape) - self.assertEqual((6, 5, 1), test._data['dataset']['w'].shape) + df = pd.DataFrame( + { + "x": [np.array([1.0, 2.0, 3.0, 4.0]) for _ in range(10)], + "y": [np.array([5.0, 6.0, 7.0]) for _ in range(10)], + "k": [np.array([8.0, 9.0]) for _ in range(10)], + "w": np.array( + [10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0] + ), + } + ) + + test.loadData(name="dataset", source=df) + self.assertEqual((6, 2, 4), test._data["dataset"]["x"].shape) + self.assertEqual((6, 2, 3), test._data["dataset"]["y"].shape) + self.assertEqual((6, 1, 2), test._data["dataset"]["k"].shape) + self.assertEqual((6, 5, 1), test._data["dataset"]["w"].shape) def test_dataframe_single_dimension(self): import pandas as pd + NeuObj.clearNames() - x = Input('x') - y = Input('y') - k = Input('k') - w = Input('w') + x = Input("x") + y = Input("y") + k = Input("k") + w = Input("w") - out = Output('out', Fir(x.tw(0.02) + y.tw(0.02))) - out2 = Output('out2', Fir(k.last()) + Fir(w.tw(0.05,offset=-0.02))) + out = Output("out", Fir(x.tw(0.02) + y.tw(0.02))) + out2 = Output("out2", Fir(k.last()) + Fir(w.tw(0.05, offset=-0.02))) test = Modely(visualizer=None) - test.addMinimize('out', out, out2) + test.addMinimize("out", out, out2) test.neuralizeModel(0.01) # Create a DataFrame with random values for each input - df = pd.DataFrame({ - 'x': np.linspace(1,100,100, dtype=np.float32), - 'y': np.linspace(1,100,100, dtype=np.float32), - 'k': np.linspace(1,100,100, dtype=np.float32), - 'w': np.linspace(1,100,100, dtype=np.float32)}) - - test.loadData(name='dataset', source=df) - self.assertEqual((96, 2, 1), test._data['dataset']['x'].shape) - self.assertEqual((96, 2, 1), test._data['dataset']['y'].shape) - self.assertEqual((96, 1, 1), test._data['dataset']['k'].shape) - self.assertEqual((96, 5, 1), test._data['dataset']['w'].shape) + df = pd.DataFrame( + { + "x": np.linspace(1, 100, 100, dtype=np.float32), + "y": np.linspace(1, 100, 100, dtype=np.float32), + "k": np.linspace(1, 100, 100, dtype=np.float32), + "w": np.linspace(1, 100, 100, dtype=np.float32), + } + ) + + test.loadData(name="dataset", source=df) + self.assertEqual((96, 2, 1), test._data["dataset"]["x"].shape) + self.assertEqual((96, 2, 1), test._data["dataset"]["y"].shape) + self.assertEqual((96, 1, 1), test._data["dataset"]["k"].shape) + self.assertEqual((96, 5, 1), test._data["dataset"]["w"].shape) def test_dataframe_resampling(self): import pandas as pd + NeuObj.clearNames() - x = Input('x') - y = Input('y') - k = Input('k') - w = Input('w') + x = Input("x") + y = Input("y") + k = Input("k") + w = Input("w") - out = Output('out', Fir(x.tw(1.0) + y.tw(1.0))) - out2 = Output('out2', Fir(k.last()) + Fir(w.tw(2.5,offset=-1.0))) + out = Output("out", Fir(x.tw(1.0) + y.tw(1.0))) + out2 = Output("out2", Fir(k.last()) + Fir(w.tw(2.5, offset=-1.0))) test = Modely(visualizer=None) - test.addMinimize('out', out, out2) + test.addMinimize("out", out, out2) test.neuralizeModel(0.5) # Create a DataFrame with random values for each input - df = pd.DataFrame({ - 'time': np.array([1.0,1.5,2.0,4.0,4.5,5.0,7.0,7.5,8.0,8.5], dtype=np.float32), - 'x': np.linspace(1,10,10, dtype=np.float32), - 'y': np.linspace(1,10,10, dtype=np.float32), - 'k': np.linspace(1,10,10, dtype=np.float32), - 'w': np.linspace(1,10,10, dtype=np.float32)}) - - test.loadData(name='dataset1', source=df, resampling=True) - self.assertEqual((12, 2, 1), test._data['dataset1']['x'].shape) - self.assertEqual((12, 2, 1), test._data['dataset1']['y'].shape) - self.assertEqual((12, 1, 1), test._data['dataset1']['k'].shape) - self.assertEqual((12, 5, 1), test._data['dataset1']['w'].shape) - - df['time'] = pd.to_datetime(df['time'], unit='s') - df = df.set_index('time', drop=True) - test.loadData(name='dataset2', source=df, resampling=True) - self.assertEqual((12, 2, 1), test._data['dataset2']['x'].shape) - self.assertEqual((12, 2, 1), test._data['dataset2']['y'].shape) - self.assertEqual((12, 1, 1), test._data['dataset2']['k'].shape) - self.assertEqual((12, 5, 1), test._data['dataset2']['w'].shape) - - df2 = pd.DataFrame({ - 'x': np.linspace(1,10,10, dtype=np.float32), - 'y': np.linspace(1,10,10, dtype=np.float32), - 'k': np.linspace(1,10,10, dtype=np.float32), - 'w': np.linspace(1,10,10, dtype=np.float32)}) + df = pd.DataFrame( + { + "time": np.array( + [1.0, 1.5, 2.0, 4.0, 4.5, 5.0, 7.0, 7.5, 8.0, 8.5], dtype=np.float32 + ), + "x": np.linspace(1, 10, 10, dtype=np.float32), + "y": np.linspace(1, 10, 10, dtype=np.float32), + "k": np.linspace(1, 10, 10, dtype=np.float32), + "w": np.linspace(1, 10, 10, dtype=np.float32), + } + ) + + test.loadData(name="dataset1", source=df, resampling=True) + self.assertEqual((12, 2, 1), test._data["dataset1"]["x"].shape) + self.assertEqual((12, 2, 1), test._data["dataset1"]["y"].shape) + self.assertEqual((12, 1, 1), test._data["dataset1"]["k"].shape) + self.assertEqual((12, 5, 1), test._data["dataset1"]["w"].shape) + + df["time"] = pd.to_datetime(df["time"], unit="s") + df = df.set_index("time", drop=True) + test.loadData(name="dataset2", source=df, resampling=True) + self.assertEqual((12, 2, 1), test._data["dataset2"]["x"].shape) + self.assertEqual((12, 2, 1), test._data["dataset2"]["y"].shape) + self.assertEqual((12, 1, 1), test._data["dataset2"]["k"].shape) + self.assertEqual((12, 5, 1), test._data["dataset2"]["w"].shape) + + df2 = pd.DataFrame( + { + "x": np.linspace(1, 10, 10, dtype=np.float32), + "y": np.linspace(1, 10, 10, dtype=np.float32), + "k": np.linspace(1, 10, 10, dtype=np.float32), + "w": np.linspace(1, 10, 10, dtype=np.float32), + } + ) with self.assertRaises(TypeError): - test.loadData(name='dataset3', source=df2, resampling=True) + test.loadData(name="dataset3", source=df2, resampling=True) def test_load_data_modalities(self): import pandas as pd + NeuObj.clearNames() - x = Input('x') + x = Input("x") relation = Fir()(x.tw(0.05)) relation.closedLoop(x) - output = Output('out', relation) + output = Output("out", relation) test = Modely(visualizer=None, log_internal=True) - test.addModel('model', output) - test.addMinimize('error', output, x.next()) + test.addModel("model", output) + test.addMinimize("error", output, x.next()) test.neuralizeModel(0.01) ## Case 1: directory with files - data_struct = ['x'] - data_folder = os.path.join(os.path.dirname(__file__), 'multifile/') - test.loadData(name='dataset_directory', source=data_folder, format=data_struct, skiplines=1) - self.assertListEqual(list(test._data['dataset_directory']['x'].shape), [45, 6, 1]) - self.assertListEqual(test._multifile['dataset_directory'], [5, 20, 45]) + data_struct = ["x"] + data_folder = os.path.join(os.path.dirname(__file__), "multifile/") + test.loadData( + name="dataset_directory", + source=data_folder, + format=data_struct, + skiplines=1, + ) + self.assertListEqual( + list(test._data["dataset_directory"]["x"].shape), [45, 6, 1] + ) + self.assertListEqual(test._multifile["dataset_directory"], [5, 20, 45]) ## Case 2: dictionary - train_data_x = np.array(10*[10] + 20*[20] + 30*[30], dtype=np.float32) - train_dataset = {'x': train_data_x, 'time': np.array(range(60), dtype=np.float32)} - test.loadData(name='dataset_dictionary', source=train_dataset, ) - self.assertListEqual(list(test._data['dataset_dictionary']['x'].shape), [55, 6, 1]) + train_data_x = np.array(10 * [10] + 20 * [20] + 30 * [30], dtype=np.float32) + train_dataset = { + "x": train_data_x, + "time": np.array(range(60), dtype=np.float32), + } + test.loadData( + name="dataset_dictionary", + source=train_dataset, + ) + self.assertListEqual( + list(test._data["dataset_dictionary"]["x"].shape), [55, 6, 1] + ) ## Case 3: pandas DataFrame - df = pd.DataFrame({ - 'time': np.array(range(60), dtype=np.float32), - 'x': np.array(10*[10] + 20*[20] + 30*[30], dtype=np.float32)}) - test.loadData(name='dataset_pandas', source=df, resampling=True) - self.assertListEqual(list(test._data['dataset_pandas']['x'].shape), [5896, 6, 1]) + df = pd.DataFrame( + { + "time": np.array(range(60), dtype=np.float32), + "x": np.array(10 * [10] + 20 * [20] + 30 * [30], dtype=np.float32), + } + ) + test.loadData(name="dataset_pandas", source=df, resampling=True) + self.assertListEqual( + list(test._data["dataset_pandas"]["x"].shape), [5896, 6, 1] + ) diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 71d27a97..b2c3b3a8 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -2,25 +2,32 @@ import subprocess import os + class TestDocumentation(unittest.TestCase): def test_generate_docs(self): # Path to the Sphinx documentation source directory - docs_source_dir = os.path.join(os.path.dirname(__file__), '..', 'docs') + docs_source_dir = os.path.join(os.path.dirname(__file__), "..", "docs") # Path to the output directory for the generated documentation - docs_output_dir = os.path.join(docs_source_dir, '_build', 'html') + docs_output_dir = os.path.join(docs_source_dir, "_build", "html") # Command to generate the documentation - #TODO Check fail-on-warning command = ['sphinx-build', '--fail-on-warning', '-b', 'html', docs_source_dir, docs_output_dir] - command = ['sphinx-build', '-b', 'html', docs_source_dir, docs_output_dir] + # TODO Check fail-on-warning command = ['sphinx-build', '--fail-on-warning', '-b', 'html', docs_source_dir, docs_output_dir] + command = ["sphinx-build", "-b", "html", docs_source_dir, docs_output_dir] # Run the command and capture the output result = subprocess.run(command, capture_output=True, text=True) # Check if the command was successful - self.assertEqual(result.returncode, 0, f"Documentation generation failed: {result.stderr}") + self.assertEqual( + result.returncode, 0, f"Documentation generation failed: {result.stderr}" + ) # Optionally, check if the output directory contains the expected files - self.assertTrue(os.path.exists(docs_output_dir), "Output directory does not exist") - self.assertTrue(os.path.isfile(os.path.join(docs_output_dir, 'index.html')), - "index.html not found in output directory") \ No newline at end of file + self.assertTrue( + os.path.exists(docs_output_dir), "Output directory does not exist" + ) + self.assertTrue( + os.path.isfile(os.path.join(docs_output_dir, "index.html")), + "index.html not found in output directory", + ) diff --git a/tests/test_export.py b/tests/test_export.py index cd36e464..07eb76e0 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -1,4 +1,4 @@ -import os, unittest, torch, shutil +import os, unittest, torch, shutil import numpy as np from nnodely import * @@ -11,12 +11,17 @@ # 11 Tests # Test of export and import the network to a file in different format -class ModelyExportTest(unittest.TestCase): +class ModelyExportTest(unittest.TestCase): def TestAlmostEqual(self, data1, data2, precision=4): - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) if type(data1) == type(data2) == list: - self.assertEqual(len(data1),len(data2)) + self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): self.TestAlmostEqual(pred, label, precision=precision) else: @@ -26,28 +31,32 @@ def __init__(self, *args, **kwargs): super(ModelyExportTest, self).__init__(*args, **kwargs) clearNames() - self.result_path = './results' + self.result_path = "./results" self.test = Modely(visualizer=None, seed=42, workspace=self.result_path) - x = Input('x') - y = Input('y') - z = Input('z') + x = Input("x") + y = Input("y") + z = Input("z") ## create the relations def myFun(K1, p1, p2): return K1 * p1 * p2 - K_x = Parameter('k_x', dimensions=1, tw=1, init='init_constant', init_params={'value': 1}) - K_y = Parameter('k_y', dimensions=1, tw=1) - w = Parameter('w', dimensions=1, tw=1, init='init_constant', init_params={'value': 1}) - t = Parameter('t', dimensions=1, tw=1) - c_v = Constant('c_v', tw=1, values=[[1], [2]]) + K_x = Parameter( + "k_x", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} + ) + K_y = Parameter("k_y", dimensions=1, tw=1) + w = Parameter( + "w", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} + ) + t = Parameter("t", dimensions=1, tw=1) + c_v = Constant("c_v", tw=1, values=[[1], [2]]) c = 5 - w_5 = Parameter('w_5', dimensions=1, tw=5) - t_5 = Parameter('t_5', dimensions=1, tw=5) + w_5 = Parameter("w_5", dimensions=1, tw=5) + t_5 = Parameter("t_5", dimensions=1, tw=5) c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]] - c_5_2 = Constant('c_5_2', tw=5, values=c_5) - parfun_x = ParamFun(myFun, parameters_and_constants=[K_x,c_v]) + c_5_2 = Constant("c_5_2", tw=5, values=c_5) + parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v]) parfun_y = ParamFun(myFun, parameters_and_constants=[K_y]) parfun_z = ParamFun(myFun) fir_w = Fir(W=w_5)(x.tw(5)) @@ -61,24 +70,28 @@ def fuzzyfun(x): fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1)) fuzzyTriang = Fuzzify(centers=[1, 2, 3, 7])(x.tw(1)) - out = Output('out', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1),c_v))) - out2 = Output('out2', Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) - out3 = Output('out3', Add(fir_w, fir_t)) - out4 = Output('out4', Linear(output_dimension=1)(fuzzy+fuzzyTriang)) - out5 = Output('out5', Fir(time_part) + Fir(sample_select)) - out6 = Output('out6', LocalModel(output_function=Fir())(x.tw(1), fuzzy)) + out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))) + out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) + out3 = Output("out3", Add(fir_w, fir_t)) + out4 = Output("out4", Linear(output_dimension=1)(fuzzy + fuzzyTriang)) + out5 = Output("out5", Fir(time_part) + Fir(sample_select)) + out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy)) with self.assertRaises(TypeError): parfun_z(x.tw(5), t_5, c_5) - out7 = Output('out7', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + Fir(parfun_z(x.tw(5), t_5, c_5_2))) - - self.test.addModel('modelA', out) - self.test.addModel('modelB', [out2, out3, out4]) - self.test.addModel('modelC', [out4, out5, out6]) - self.test.addModel('modelD', [out7]) - self.test.addMinimize('error1', x.last(), out) - self.test.addMinimize('error2', y.last(), out3, loss_function='rmse') - self.test.addMinimize('error3', z.last(), out6, loss_function='rmse') - self.test.addMinimize('error4', z.last(), out7, loss_function='rmse') + out7 = Output( + "out7", + Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + + Fir(parfun_z(x.tw(5), t_5, c_5_2)), + ) + + self.test.addModel("modelA", out) + self.test.addModel("modelB", [out2, out3, out4]) + self.test.addModel("modelC", [out4, out5, out6]) + self.test.addModel("modelD", [out7]) + self.test.addMinimize("error1", x.last(), out) + self.test.addMinimize("error2", y.last(), out3, loss_function="rmse") + self.test.addMinimize("error3", z.last(), out6, loss_function="rmse") + self.test.addMinimize("error4", z.last(), out7, loss_function="rmse") def test_export_pt(self): if os.path.exists(self.test.getWorkspace()): @@ -87,21 +100,36 @@ def test_export_pt(self): # Export torch file .pt # Save torch model and load it self.test.neuralizeModel(0.5) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.saveTorchModel() self.test.neuralizeModel(clear_model=True) # The new_out is different from the old_out because the model is cleared - new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) # The new_out_after_load is the same as the old_out because the model is loaded with the same parameters self.test.loadTorchModel() - new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out_after_load = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) with self.assertRaises(AssertionError): self.assertEqual(old_out, new_out) self.assertEqual(old_out, new_out_after_load) with self.assertRaises(RuntimeError): - test2 = Modely(visualizer=None, workspace = self.result_path) + test2 = Modely(visualizer=None, workspace=self.result_path) # You need not neuralized model to load a torch model test2.loadTorchModel() @@ -116,10 +144,20 @@ def test_export_json_not_neuralized(self): # Save a not neuralized nnodely json model and load it self.test.saveModel() # Save a model without parameter values and samples values with self.assertRaises(RuntimeError): - self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.loadModel() # Load the nnodely model without parameter values with self.assertRaises(RuntimeError): - self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) test2 = Modely(visualizer=None, workspace=self.test.getWorkspace()) test2.loadModel() # Load the nnodely model with parameter values with self.assertRaises(AttributeError): @@ -139,16 +177,36 @@ def test_export_json_untrained(self): # Save a untrained nnodely json model and load it # the new_out and new_out_after_load are different because the model saved model is not trained self.test.neuralizeModel(0.5) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.saveModel() # Save a model without parameter values self.test.neuralizeModel(clear_model=True) # Create a new torch model - new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.loadModel() # Load the nnodely model without parameter values # Use the preloaded torch model for inference with self.assertRaises(RuntimeError): - self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.neuralizeModel(0.5) - new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out_after_load = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) with self.assertRaises(AssertionError): self.assertEqual(old_out, new_out) with self.assertRaises(AssertionError): @@ -164,15 +222,35 @@ def test_export_json_trained(self): # Export json of nnodely model with parameter valuess # The old_out is the same as the new_out_after_load because the model is loaded with the same parameters self.test.neuralizeModel(0.5) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.saveModel() # Save the model with and without parameter values self.test.neuralizeModel(clear_model=True) # Create a new torch model - new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.loadModel() # Load the nnodely model with parameter values with self.assertRaises(RuntimeError): - self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.neuralizeModel() - new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out_after_load = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) with self.assertRaises(AssertionError): self.assertEqual(old_out, new_out) with self.assertRaises(AssertionError): @@ -188,15 +266,30 @@ def test_import_json_new_object(self): os.makedirs(self.result_path, exist_ok=True) # Import nnodely json model in a new object self.test.neuralizeModel(0.5) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.neuralizeModel() self.test.saveModel() # Save the model with and without parameter values test2 = Modely(visualizer=None, workspace=self.test.getWorkspace()) test2.loadModel() # Load the nnodely model with parameter values with self.assertRaises(RuntimeError): - test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + test2( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) test2.neuralizeModel() - new_model_out_after_load = test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_model_out_after_load = test2( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.assertEqual(old_out, new_model_out_after_load) if os.path.exists(self.test.getWorkspace()): @@ -209,19 +302,34 @@ def test_export_torch_script(self): # Export and import of a torch script .py # The old_out is the same as the new_out_after_load because the model is loaded with the same parameters with self.assertRaises(RuntimeError): - self.test.exportPythonModel() # The model is not neuralized yet + self.test.exportPythonModel() # The model is not neuralized yet self.test.neuralizeModel(0.5) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.exportPythonModel() # Export the trace model self.test.neuralizeModel(clear_model=True) # Create a new torch model - new_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.importPythonModel() # Import the tracer model with self.assertRaises(RuntimeError): - self.test.exportPythonModel() # The model is traced + self.test.exportPythonModel() # The model is traced # Perform inference with the imported tracer model - new_out_after_load = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + new_out_after_load = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) with self.assertRaises(AssertionError): - self.assertEqual(old_out, new_out) + self.assertEqual(old_out, new_out) self.assertEqual(old_out, new_out_after_load) if os.path.exists(self.test.getWorkspace()): @@ -232,15 +340,25 @@ def test_export_torch_script_new_object(self): shutil.rmtree(self.test.getWorkspace()) os.makedirs(self.result_path, exist_ok=True) # Import of a torch script .py - self.test.neuralizeModel(0.5,clear_model=True) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + self.test.neuralizeModel(0.5, clear_model=True) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.exportPythonModel() # Export the trace model self.test.neuralizeModel(clear_model=True) test2 = Modely(visualizer=None, workspace=self.test.getWorkspace()) test2.importPythonModel() # Load the nnodely model with parameter values with self.assertRaises(RuntimeError): - test2.exportPythonModel() # The model is traced - new_out_after_load = test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + test2.exportPythonModel() # The model is traced + new_out_after_load = test2( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.assertEqual(old_out, new_out_after_load) if os.path.exists(self.test.getWorkspace()): @@ -254,16 +372,28 @@ def test_export_trained_torch_script(self): data_x = np.arange(0.0, 1, 0.1) data_y = np.arange(0.0, 1, 0.1) a, b = -1.0, 2.0 - dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y} - params = {'num_of_epochs': 1, 'lr': 0.01} - self.test.neuralizeModel(0.5,clear_model=True) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + params = {"num_of_epochs": 1, "lr": 0.01} + self.test.neuralizeModel(0.5, clear_model=True) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.exportPythonModel() # Export the trace model - self.test.loadData(name='dataset', source=dataset) # Create the dataset - self.test.trainModel(optimizer='SGD', training_params=params) # Train the traced model - new_out_after_train = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + self.test.loadData(name="dataset", source=dataset) # Create the dataset + self.test.trainModel( + optimizer="SGD", training_params=params + ) # Train the traced model + new_out_after_train = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) with self.assertRaises(AssertionError): - self.assertEqual(old_out, new_out_after_train) + self.assertEqual(old_out, new_out_after_train) if os.path.exists(self.test.getWorkspace()): shutil.rmtree(self.test.getWorkspace()) @@ -274,25 +404,44 @@ def test_export_torch_script_new_object_train(self): os.makedirs(self.result_path, exist_ok=True) # Perform training on an imported new tracer model self.test.neuralizeModel(0.5, clear_model=True) - old_out = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + old_out = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) self.test.exportPythonModel() # Export the trace model data_x = np.arange(0.0, 1, 0.1) data_y = np.arange(0.0, 1, 0.1) a, b = -1.0, 2.0 - dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y} - params = {'num_of_epochs': 1, 'lr': 0.01} - self.test.loadData(name='dataset', source=dataset) # Create the dataset - self.test.trainModel(optimizer='SGD', training_params=params) # Train the traced model - old_out_after_train = self.test({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + params = {"num_of_epochs": 1, "lr": 0.01} + self.test.loadData(name="dataset", source=dataset) # Create the dataset + self.test.trainModel( + optimizer="SGD", training_params=params + ) # Train the traced model + old_out_after_train = self.test( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) with self.assertRaises(AssertionError): - self.assertEqual(old_out, old_out_after_train) + self.assertEqual(old_out, old_out_after_train) test2 = Modely(visualizer=None, workspace=self.test.getWorkspace()) test2.importPythonModel() # Load the nnodely model with parameter values - test2.loadData(name='dataset', source=dataset) # Create the dataset - test2.trainModel(optimizer='SGD', training_params=params) # Train the traced model - new_out_after_train = test2({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'y': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}) + test2.loadData(name="dataset", source=dataset) # Create the dataset + test2.trainModel( + optimizer="SGD", training_params=params + ) # Train the traced model + new_out_after_train = test2( + { + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "y": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + } + ) with self.assertRaises(AssertionError): - self.assertEqual(old_out, new_out_after_train) + self.assertEqual(old_out, new_out_after_train) self.assertEqual(old_out_after_train, new_out_after_train) if os.path.exists(self.test.getWorkspace()): @@ -305,17 +454,42 @@ def test_export_onnx(self): self.test.neuralizeModel(0.5, clear_model=True) # Export all network with minimize - self.test.exportONNX(inputs_order=['x', 'y', 'z'], outputs_order=['out', 'out2', 'out3', 'out4', 'out5', 'out6', 'out7']) # Export the onnx model + self.test.exportONNX( + inputs_order=["x", "y", "z"], + outputs_order=["out", "out2", "out3", "out4", "out5", "out6", "out7"], + ) # Export the onnx model # Export the all models in onnx format - self.test.exportONNX(models=['modelA','modelB','modelC','modelD'], inputs_order=['x', 'y'], outputs_order=['out', 'out2', 'out3', 'out4', 'out5', 'out6']) # Export the onnx model + self.test.exportONNX( + models=["modelA", "modelB", "modelC", "modelD"], + inputs_order=["x", "y"], + outputs_order=["out", "out2", "out3", "out4", "out5", "out6"], + ) # Export the onnx model # Export only the modelB in onnx format - self.test.exportONNX(inputs_order=['x', 'y'], outputs_order=['out3', 'out4', 'out2'], models=['modelB']) # Export the onnx model - self.assertTrue(os.path.exists(os.path.join(self.test.getWorkspace(), 'onnx', 'net.onnx'))) - self.assertTrue(os.path.exists(os.path.join(self.test.getWorkspace(), 'onnx', 'net_modelA_modelB_modelC_modelD.onnx'))) - self.assertTrue(os.path.exists(os.path.join(self.test.getWorkspace(), 'onnx', 'net_modelB.onnx'))) + self.test.exportONNX( + inputs_order=["x", "y"], + outputs_order=["out3", "out4", "out2"], + models=["modelB"], + ) # Export the onnx model + self.assertTrue( + os.path.exists(os.path.join(self.test.getWorkspace(), "onnx", "net.onnx")) + ) + self.assertTrue( + os.path.exists( + os.path.join( + self.test.getWorkspace(), + "onnx", + "net_modelA_modelB_modelC_modelD.onnx", + ) + ) + ) + self.assertTrue( + os.path.exists( + os.path.join(self.test.getWorkspace(), "onnx", "net_modelB.onnx") + ) + ) if os.path.exists(self.test.getWorkspace()): - shutil.rmtree(self.test.getWorkspace()) + shutil.rmtree(self.test.getWorkspace()) def test_export_report(self): if os.path.exists(self.test.getWorkspace()): @@ -327,18 +501,25 @@ def test_export_report(self): data_x = np.arange(0.0, 10, 0.1) data_y = np.arange(0.0, 10, 0.1) a, b = -1.0, 2.0 - dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y} - params = {'num_of_epochs': 20, 'lr': 0.0005} - self.test.loadData(name='dataset', source=dataset) # Create the dataset - self.test.trainModel(optimizer='SGD', training_params=params) # Train the traced model + dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + params = {"num_of_epochs": 20, "lr": 0.0005} + self.test.loadData(name="dataset", source=dataset) # Create the dataset + self.test.trainModel( + optimizer="SGD", training_params=params + ) # Train the traced model self.test.exportReport() - self.test.loadData(name='dataset2', source=dataset) # Create the dataset - self.test.trainAndAnalyze(optimizer='SGD', train_dataset='dataset', validation_dataset='dataset2', training_params=params) # Train the traced model + self.test.loadData(name="dataset2", source=dataset) # Create the dataset + self.test.trainAndAnalyze( + optimizer="SGD", + train_dataset="dataset", + validation_dataset="dataset2", + training_params=params, + ) # Train the traced model self.test.exportReport() - self.test.removeMinimize(['error1','error2','error3','error4']) + self.test.removeMinimize(["error1", "error2", "error3", "error4"]) self.test.exportReport() if os.path.exists(self.test.getWorkspace()): - shutil.rmtree(self.test.getWorkspace()) \ No newline at end of file + shutil.rmtree(self.test.getWorkspace()) diff --git a/tests/test_export_recurrent.py b/tests/test_export_recurrent.py index 0874ca54..915a51a7 100644 --- a/tests/test_export_recurrent.py +++ b/tests/test_export_recurrent.py @@ -11,47 +11,65 @@ # 11 Tests # Test of export and import the network to a file in different format -class ModelyExportTest(unittest.TestCase): +class ModelyExportTest(unittest.TestCase): def TestAlmostEqual(self, data1, data2, precision=4): - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) if type(data1) == type(data2) == list: - self.assertEqual(len(data1),len(data2)) + self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): self.TestAlmostEqual(pred, label, precision=precision) else: self.assertAlmostEqual(data1, data2, places=precision) - def test_export_and_import_train_python_module(self): NeuObj.clearNames() - result_path = 'results' - network_name = 'net' + result_path = "results" + network_name = "net" test = Modely(visualizer=None, seed=42, workspace=result_path) - x = Input('x') - y = Input('y') - z = Input('z') - target = Input('target') - a = Parameter('a', dimensions=1, sw=1, values=[[1]]) - b = Parameter('b', dimensions=1, sw=1, values=[[1]]) - c = Parameter('c', dimensions=1, sw=1, values=[[1]]) + x = Input("x") + y = Input("y") + z = Input("z") + target = Input("target") + a = Parameter("a", dimensions=1, sw=1, values=[[1]]) + b = Parameter("b", dimensions=1, sw=1, values=[[1]]) + c = Parameter("c", dimensions=1, sw=1, values=[[1]]) fir_x = Fir(W=a)(x.last()) fir_y = Fir(W=b)(y.last()) fir_z = Fir(W=c)(z.last()) - data_x, data_y, data_z = np.random.rand(20), np.random.rand(20), np.random.rand(20) - dataset = {'x':data_x, 'y':data_y, 'z':data_z, 'target':3*data_x + 3*data_y + 3*data_z} + data_x, data_y, data_z = ( + np.random.rand(20), + np.random.rand(20), + np.random.rand(20), + ) + dataset = { + "x": data_x, + "y": data_y, + "z": data_z, + "target": 3 * data_x + 3 * data_y + 3 * data_z, + } fir_x.connect(y) sum_rel = fir_x + fir_y + fir_z sum_rel.closedLoop(z) - out = Output('out', sum_rel) - test.addModel('model', out) - test.addMinimize('error', target.last(), out) + out = Output("out", sum_rel) + test.addModel("model", out) + test.addMinimize("error", target.last(), out) test.neuralizeModel(0.5) - test.loadData(name='test_dataset', source=dataset) + test.loadData(name="test_dataset", source=dataset) ## Train - test.trainModel(optimizer='SGD', training_params={'num_of_epochs': 1, 'lr': 0.0001, 'train_batch_size': 1}, splits=[100,0,0], prediction_samples=10) + test.trainModel( + optimizer="SGD", + training_params={"num_of_epochs": 1, "lr": 0.0001, "train_batch_size": 1}, + splits=[100, 0, 0], + prediction_samples=10, + ) ## Inference - sample = {'x':[1], 'y':[2], 'z':[3], 'target':[18]} + sample = {"x": [1], "y": [2], "z": [3], "target": [18]} train_result = test(sample) train_parameters = test.parameters # Export the model @@ -60,352 +78,520 @@ def test_export_and_import_train_python_module(self): test.importPythonModel(name=network_name) # Inference with imported model self.assertEqual(train_result, test(sample)) - self.assertEqual(train_parameters['a'], test.parameters['a']) - self.assertEqual(train_parameters['b'], test.parameters['b']) - self.assertEqual(train_parameters['c'], test.parameters['c']) + self.assertEqual(train_parameters["a"], test.parameters["a"]) + self.assertEqual(train_parameters["b"], test.parameters["b"]) + self.assertEqual(train_parameters["c"], test.parameters["c"]) if os.path.exists(test.getWorkspace()): shutil.rmtree(test.getWorkspace()) def test_export_and_import_python_module(self): NeuObj.clearNames() - result_path = 'results' - network_name = 'exported_model' + result_path = "results" + network_name = "exported_model" test = Modely(visualizer=None, seed=42, workspace=result_path) - x = Input('x') - y = Input('y') - z = Input('z') - a = Parameter('a', dimensions=1, sw=1, values=[[1]]) - b = Parameter('b', dimensions=1, sw=1, values=[[1]]) - c = Parameter('c', dimensions=1, sw=1, values=[[1]]) + x = Input("x") + y = Input("y") + z = Input("z") + a = Parameter("a", dimensions=1, sw=1, values=[[1]]) + b = Parameter("b", dimensions=1, sw=1, values=[[1]]) + c = Parameter("c", dimensions=1, sw=1, values=[[1]]) fir_x = Fir(W=a)(x.last()) fir_y = Fir(W=b)(y.last()) fir_z = Fir(W=c)(z.last()) fir_x.connect(y) sum_rel = fir_x + fir_y + fir_z sum_rel.closedLoop(z) - out = Output('out', sum_rel) - test.addModel('model', out) + out = Output("out", sum_rel) + test.addModel("model", out) test.neuralizeModel(0.5) ## Inference - sample = {'x':[1], 'y':[2], 'z':[3]} + sample = {"x": [1], "y": [2], "z": [3]} inference_result = test(sample) - self.assertEqual(inference_result['out'], [5.0]) + self.assertEqual(inference_result["out"], [5.0]) # Export the model test.exportPythonModel(name=network_name) ## Load the exported model.py ## Import the python exported module - #from results.exported_model import RecurrentModel - module = importlib.import_module(result_path+'.'+network_name) - RecurrentModel = getattr(module, 'RecurrentModel') + # from results.exported_model import RecurrentModel + module = importlib.import_module(result_path + "." + network_name) + RecurrentModel = getattr(module, "RecurrentModel") model = RecurrentModel() # Create dummy input data - dummy_input = {'x': torch.ones(5, 1, 1, 1), 'target': torch.ones(10, 1, 1, 1), 'y': torch.zeros(1,1,1), 'z':torch.zeros(1,1,1)} # Adjust the shape as needed + dummy_input = { + "x": torch.ones(5, 1, 1, 1), + "target": torch.ones(10, 1, 1, 1), + "y": torch.zeros(1, 1, 1), + "z": torch.zeros(1, 1, 1), + } # Adjust the shape as needed # Inference with imported model with torch.no_grad(): output = model(dummy_input) - self.assertEqual(output['out'], [torch.tensor([[[2.]]]), torch.tensor([[[4.]]]), torch.tensor([[[6.]]]), torch.tensor([[[8.]]]), torch.tensor([[[10.]]])]) + self.assertEqual( + output["out"], + [ + torch.tensor([[[2.0]]]), + torch.tensor([[[4.0]]]), + torch.tensor([[[6.0]]]), + torch.tensor([[[8.0]]]), + torch.tensor([[[10.0]]]), + ], + ) if os.path.exists(test.getWorkspace()): shutil.rmtree(test.getWorkspace()) def test_export_and_import_onnx_module(self): NeuObj.clearNames() - result_path = 'results' + result_path = "results" test = Modely(visualizer=None, seed=42, workspace=result_path) - x = Input('x') - y = Input('y') - z = Input('z') - a = Parameter('a', dimensions=1, sw=1, values=[[1]]) - b = Parameter('b', dimensions=1, sw=1, values=[[1]]) - c = Parameter('c', dimensions=1, sw=1, values=[[1]]) + x = Input("x") + y = Input("y") + z = Input("z") + a = Parameter("a", dimensions=1, sw=1, values=[[1]]) + b = Parameter("b", dimensions=1, sw=1, values=[[1]]) + c = Parameter("c", dimensions=1, sw=1, values=[[1]]) fir_x = Fir(W=a)(x.last()) fir_y = Fir(W=b)(y.last()) fir_z = Fir(W=c)(z.last()) fir_x.connect(y) sum_rel = fir_x + fir_y + fir_z sum_rel.closedLoop(z) - out = Output('out', sum_rel) - test.addModel('model', out) + out = Output("out", sum_rel) + test.addModel("model", out) test.neuralizeModel(0.5) ## Inference - sample = {'x':[1], 'y':[2], 'z':[3]} + sample = {"x": [1], "y": [2], "z": [3]} inference_result = test(sample) - self.assertEqual(inference_result['out'], [5.0]) + self.assertEqual(inference_result["out"], [5.0]) ## Export in ONNX format - test.exportONNX(['x','y','z'],['out']) # Export the onnx model + test.exportONNX(["x", "y", "z"], ["out"]) # Export the onnx model ## ONNX IMPORT - dummy_input = {'x':np.ones(shape=(3, 1, 1, 1)).astype(np.float32), - 'y':np.ones(shape=(1, 1, 1)).astype(np.float32), - 'z':np.ones(shape=(1, 1, 1)).astype(np.float32)} - outputs = Modely(visualizer=None,workspace=result_path).onnxInference(dummy_input) + dummy_input = { + "x": np.ones(shape=(3, 1, 1, 1)).astype(np.float32), + "y": np.ones(shape=(1, 1, 1)).astype(np.float32), + "z": np.ones(shape=(1, 1, 1)).astype(np.float32), + } + outputs = Modely(visualizer=None, workspace=result_path).onnxInference( + dummy_input + ) # Get the output - expected_output = np.array([[[[3.]]], [[[5.]]], [[[7.]]]], dtype=np.float32) + expected_output = np.array([[[[3.0]]], [[[5.0]]], [[[7.0]]]], dtype=np.float32) self.assertEqual(outputs[0].tolist(), expected_output.tolist()) # The connected variable is not needed - sample = {'x':[3],'z':[5]} + sample = {"x": [3], "z": [5]} inference_result = test(sample) - dummy_input = {'x':3*np.ones(shape=(1, 1, 1, 1)).astype(np.float32), - 'y':7*np.ones(shape=(1, 1, 1)).astype(np.float32), - 'z':5*np.ones(shape=(1, 1, 1)).astype(np.float32)} - outputs = Modely(visualizer=None, workspace=result_path).onnxInference(dummy_input) - self.assertEqual(outputs[0][0][0], inference_result['out']) + dummy_input = { + "x": 3 * np.ones(shape=(1, 1, 1, 1)).astype(np.float32), + "y": 7 * np.ones(shape=(1, 1, 1)).astype(np.float32), + "z": 5 * np.ones(shape=(1, 1, 1)).astype(np.float32), + } + outputs = Modely(visualizer=None, workspace=result_path).onnxInference( + dummy_input + ) + self.assertEqual(outputs[0][0][0], inference_result["out"]) if os.path.exists(test.getWorkspace()): shutil.rmtree(test.getWorkspace()) def test_export_and_import_onnx_module_easy(self): NeuObj.clearNames() - result_path = 'results' + result_path = "results" test = Modely(visualizer=None, seed=42, workspace=result_path) - num_cycle = Input('num_cycle') - x = Input('x') - fir_x = Fir()(x.last()+1.0) + num_cycle = Input("num_cycle") + x = Input("x") + fir_x = Fir()(x.last() + 1.0) fir_x.closedLoop(x) - out1 = Output('out1', fir_x) - out2 = Output('out2', num_cycle.last()+1.0) - test.addModel('model', [out1,out2]) + out1 = Output("out1", fir_x) + out2 = Output("out2", num_cycle.last() + 1.0) + test.addModel("model", [out1, out2]) test.neuralizeModel(0.5) ## Export in ONNX format - test.exportONNX(['x','num_cycle'],['out1','out2']) # Export the onnx model - output_nodely = test({'num_cycle':np.ones(shape=(10)).astype(np.float32).tolist(), 'x':np.ones(shape=(1)).astype(np.float32).tolist()}) + test.exportONNX(["x", "num_cycle"], ["out1", "out2"]) # Export the onnx model + output_nodely = test( + { + "num_cycle": np.ones(shape=(10)).astype(np.float32).tolist(), + "x": np.ones(shape=(1)).astype(np.float32).tolist(), + } + ) ## ONNX IMPORT - outputs = Modely(visualizer=None,workspace=result_path).onnxInference(inputs={'num_cycle':np.ones(shape=(10, 1, 1, 1)).astype(np.float32), 'x':np.ones(shape=(1, 1, 1)).astype(np.float32)}) - self.assertEqual(output_nodely['out1'], outputs[0].squeeze().tolist()) - self.assertEqual(output_nodely['out2'], outputs[1].squeeze().tolist()) + outputs = Modely(visualizer=None, workspace=result_path).onnxInference( + inputs={ + "num_cycle": np.ones(shape=(10, 1, 1, 1)).astype(np.float32), + "x": np.ones(shape=(1, 1, 1)).astype(np.float32), + } + ) + self.assertEqual(output_nodely["out1"], outputs[0].squeeze().tolist()) + self.assertEqual(output_nodely["out2"], outputs[1].squeeze().tolist()) if os.path.exists(test.getWorkspace()): shutil.rmtree(test.getWorkspace()) def test_export_and_import_onnx_module_complex(self): # Create nnodely structure - result_path = 'results' - network_name = 'vehicle' + result_path = "results" + network_name = "vehicle" vehicle = Modely(visualizer=None, seed=2, workspace=result_path) # Dimensions of the layers - n = 25 + n = 25 na = 21 - #Create neural model inputs - velocity = Input('vel') - brake = Input('brk') - gear = Input('gear') - torque = Input('trq') - altitude = Input('alt',dimensions=na) - acc = Input('acc') + # Create neural model inputs + velocity = Input("vel") + brake = Input("brk") + gear = Input("gear") + torque = Input("trq") + altitude = Input("alt", dimensions=na) + acc = Input("acc") # Create neural network relations - air_drag_force = Linear(b=True)(velocity.last()**2) - breaking_force = -Relu(Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n))) - gravity_force = Linear(W_init='init_constant', W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last()) - fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last()) - local_model = LocalModel(input_function=lambda: Fir(W_init = 'init_negexp', W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})) + air_drag_force = Linear(b=True)(velocity.last() ** 2) + breaking_force = -Relu( + Fir( + W_init="init_negexp", + W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3}, + )(brake.sw(n)) + ) + gravity_force = Linear( + W_init="init_constant", W_init_params={"value": 0}, dropout=0.1, W="gravity" + )(altitude.last()) + fuzzi_gear = Fuzzify(6, range=[2, 7], functions="Rectangular")(gear.last()) + local_model = LocalModel( + input_function=lambda: Fir( + W_init="init_negexp", + W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3}, + ) + ) engine_force = local_model(torque.sw(n), fuzzi_gear) # Create neural network output - out = Output('accelleration', air_drag_force+breaking_force+gravity_force+engine_force) + out = Output( + "accelleration", + air_drag_force + breaking_force + gravity_force + engine_force, + ) # Add the neural model to the nnodely structure and neuralization of the model - vehicle.addModel('acc',out) - vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse') + vehicle.addModel("acc", out) + vehicle.addMinimize("acc_error", acc.last(), out, loss_function="rmse") vehicle.neuralizeModel(0.05) # Load the training and the validation dataset - data_struct = ['vel','trq','brk','gear','alt','acc'] - data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'vehicle_data') - vehicle.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1) + data_struct = ["vel", "trq", "brk", "gear", "alt", "acc"] + data_folder = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "vehicle_data" + ) + vehicle.loadData( + name="dataset", source=data_folder, format=data_struct, skiplines=1 + ) # Inference - model_sample = vehicle.getSamples('dataset', window=1) + model_sample = vehicle.getSamples("dataset", window=1) model_inference = vehicle(model_sample, sampled=True, prediction_samples=1) ## Export the Onnx Model - vehicle.exportONNX(['vel','brk','gear','trq','alt'],['accelleration'], network_name, models='acc') + vehicle.exportONNX( + ["vel", "brk", "gear", "trq", "alt"], + ["accelleration"], + network_name, + models="acc", + ) # Onnx Import - outputs = Modely(visualizer=None).onnxInference(model_sample, name=network_name, model_folder=os.path.join(result_path,'onnx')) - self.assertEqual(outputs[0][0], model_inference['accelleration']) + outputs = Modely(visualizer=None).onnxInference( + model_sample, + name=network_name, + model_folder=os.path.join(result_path, "onnx"), + ) + self.assertEqual(outputs[0][0], model_inference["accelleration"]) if os.path.exists(vehicle.getWorkspace()): shutil.rmtree(vehicle.getWorkspace()) def test_export_python_module_recurrent(self): NeuObj.clearNames() - result_path = 'results' - network_name = 'net' + result_path = "results" + network_name = "net" test = Modely(visualizer=None, seed=42, workspace=result_path) - input1 = Input('input1') - input2 = Input('input2', dimensions=3) - input3 = Input('input3') - input4 = Input('input4', dimensions=3) - state1 = Input('state1') - state2 = Input('state2', dimensions=3) + input1 = Input("input1") + input2 = Input("input2", dimensions=3) + input3 = Input("input3") + input4 = Input("input4", dimensions=3) + state1 = Input("state1") + state2 = Input("state2", dimensions=3) rel_1 = Linear(b=True)(input1.last()) + Linear(b=True)(input3.last()) rel_1.closedLoop(state1) - rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear(output_dimension=3, b=True)(input4.last()) + rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear( + output_dimension=3, b=True + )(input4.last()) rel_2.closedLoop(state2) - out1 = Output('out1', rel_1) - out2 = Output('out2', rel_2) - out3 = Output('input1-out', input1.last()) - out4 = Output('input2-out', input2.last()) - out5 = Output('input3-out', input3.sw(4)) - out6 = Output('input4-out', input4.sw(4)) - out7 = Output('state1-out', state1.last()) - out8 = Output('state2-out', state2.last()) + out1 = Output("out1", rel_1) + out2 = Output("out2", rel_2) + out3 = Output("input1-out", input1.last()) + out4 = Output("input2-out", input2.last()) + out5 = Output("input3-out", input3.sw(4)) + out6 = Output("input4-out", input4.sw(4)) + out7 = Output("state1-out", state1.last()) + out8 = Output("state2-out", state2.last()) - test.addModel('model', [out1, out2, out3, out4, out5, out6, out7, out8]) + test.addModel("model", [out1, out2, out3, out4, out5, out6, out7, out8]) test.neuralizeModel() test.exportPythonModel(name=network_name) ## Load the exported model.py ## Import the python exported module - #from results.net import RecurrentModel - module = importlib.import_module(result_path+'.'+network_name) - RecurrentModel = getattr(module, 'RecurrentModel') + # from results.net import RecurrentModel + module = importlib.import_module(result_path + "." + network_name) + RecurrentModel = getattr(module, "RecurrentModel") recurrent_model = RecurrentModel() ## Without Horizon and without batch - recurrent_sample = {'input1': torch.rand(size=(1,1,1,1), dtype=torch.float32), - 'input2': torch.rand(size=(1,1,1,3), dtype=torch.float32), - 'input3': torch.rand(size=(1,1,4,1), dtype=torch.float32), - 'input4': torch.rand(size=(1,1,4,3), dtype=torch.float32)} - recurrent_sample['state1'] = torch.rand(size=(1,1,1), dtype=torch.float32) - recurrent_sample['state2'] = torch.rand(size=(1,1,3), dtype=torch.float32) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out1']).shape), [1,1,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out2']).shape), [1,1,1,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input1-out']).shape), [1,1,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input2-out']).shape), [1,1,1,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input3-out']).shape), [1,1,4,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input4-out']).shape), [1,1,4,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state1-out']).shape), [1,1,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state2-out']).shape), [1,1,1,3]) + recurrent_sample = { + "input1": torch.rand(size=(1, 1, 1, 1), dtype=torch.float32), + "input2": torch.rand(size=(1, 1, 1, 3), dtype=torch.float32), + "input3": torch.rand(size=(1, 1, 4, 1), dtype=torch.float32), + "input4": torch.rand(size=(1, 1, 4, 3), dtype=torch.float32), + } + recurrent_sample["state1"] = torch.rand(size=(1, 1, 1), dtype=torch.float32) + recurrent_sample["state2"] = torch.rand(size=(1, 1, 3), dtype=torch.float32) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["out1"]).shape), + [1, 1, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["out2"]).shape), + [1, 1, 1, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input1-out"]).shape), + [1, 1, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input2-out"]).shape), + [1, 1, 1, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input3-out"]).shape), + [1, 1, 4, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input4-out"]).shape), + [1, 1, 4, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["state1-out"]).shape), + [1, 1, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["state2-out"]).shape), + [1, 1, 1, 3], + ) ## With Horizon and without batch - recurrent_sample = {'input1': torch.rand(size=(5,1,1,1), dtype=torch.float32), - 'input2': torch.rand(size=(5,1,1,3), dtype=torch.float32), - 'input3': torch.rand(size=(5,1,4,1), dtype=torch.float32), - 'input4': torch.rand(size=(5,1,4,3), dtype=torch.float32)} - recurrent_sample['state1'] = torch.rand(size=(1,1,1), dtype=torch.float32) - recurrent_sample['state2'] = torch.rand(size=(1,1,3), dtype=torch.float32) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out1']).shape), [5,1,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out2']).shape), [5,1,1,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input1-out']).shape), [5,1,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input2-out']).shape), [5,1,1,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input3-out']).shape), [5,1,4,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input4-out']).shape), [5,1,4,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state1-out']).shape), [5,1,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state2-out']).shape), [5,1,1,3]) + recurrent_sample = { + "input1": torch.rand(size=(5, 1, 1, 1), dtype=torch.float32), + "input2": torch.rand(size=(5, 1, 1, 3), dtype=torch.float32), + "input3": torch.rand(size=(5, 1, 4, 1), dtype=torch.float32), + "input4": torch.rand(size=(5, 1, 4, 3), dtype=torch.float32), + } + recurrent_sample["state1"] = torch.rand(size=(1, 1, 1), dtype=torch.float32) + recurrent_sample["state2"] = torch.rand(size=(1, 1, 3), dtype=torch.float32) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["out1"]).shape), + [5, 1, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["out2"]).shape), + [5, 1, 1, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input1-out"]).shape), + [5, 1, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input2-out"]).shape), + [5, 1, 1, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input3-out"]).shape), + [5, 1, 4, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input4-out"]).shape), + [5, 1, 4, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["state1-out"]).shape), + [5, 1, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["state2-out"]).shape), + [5, 1, 1, 3], + ) ## With Horizon and with batch - recurrent_sample = {'input1': torch.rand(size=(5,2,1,1), dtype=torch.float32), - 'input2': torch.rand(size=(5,2,1,3), dtype=torch.float32), - 'input3': torch.rand(size=(5,2,4,1), dtype=torch.float32), - 'input4': torch.rand(size=(5,2,4,3), dtype=torch.float32)} - recurrent_sample['state1'] = torch.rand(size=(2,1,1), dtype=torch.float32) - recurrent_sample['state2'] = torch.rand(size=(2,1,3), dtype=torch.float32) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out1']).shape), [5,2,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['out2']).shape), [5,2,1,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input1-out']).shape), [5,2,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input2-out']).shape), [5,2,1,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input3-out']).shape), [5,2,4,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['input4-out']).shape), [5,2,4,3]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state1-out']).shape), [5,2,1,1]) - self.assertListEqual(list(torch.stack(recurrent_model(recurrent_sample)['state2-out']).shape), [5,2,1,3]) + recurrent_sample = { + "input1": torch.rand(size=(5, 2, 1, 1), dtype=torch.float32), + "input2": torch.rand(size=(5, 2, 1, 3), dtype=torch.float32), + "input3": torch.rand(size=(5, 2, 4, 1), dtype=torch.float32), + "input4": torch.rand(size=(5, 2, 4, 3), dtype=torch.float32), + } + recurrent_sample["state1"] = torch.rand(size=(2, 1, 1), dtype=torch.float32) + recurrent_sample["state2"] = torch.rand(size=(2, 1, 3), dtype=torch.float32) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["out1"]).shape), + [5, 2, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["out2"]).shape), + [5, 2, 1, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input1-out"]).shape), + [5, 2, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input2-out"]).shape), + [5, 2, 1, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input3-out"]).shape), + [5, 2, 4, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["input4-out"]).shape), + [5, 2, 4, 3], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["state1-out"]).shape), + [5, 2, 1, 1], + ) + self.assertListEqual( + list(torch.stack(recurrent_model(recurrent_sample)["state2-out"]).shape), + [5, 2, 1, 3], + ) if os.path.exists(test.getWorkspace()): shutil.rmtree(test.getWorkspace()) def test_export_onnx_module_recurrent(self): - result_path = 'results' - test = Modely(visualizer=None, seed=42, workspace= result_path) - input1 = Input('input1') - input2 = Input('input2', dimensions=3) - input3 = Input('input3') - input4 = Input('input4', dimensions=3) - state1 = Input('state1') - state2 = Input('state2', dimensions=3) + result_path = "results" + test = Modely(visualizer=None, seed=42, workspace=result_path) + input1 = Input("input1") + input2 = Input("input2", dimensions=3) + input3 = Input("input3") + input4 = Input("input4", dimensions=3) + state1 = Input("state1") + state2 = Input("state2", dimensions=3) rel_1 = Linear(b=True)(input1.last()) + Linear(b=True)(input3.last()) rel_1.closedLoop(state1) - rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear(output_dimension=3, b=True)(input4.last()) + rel_2 = Linear(output_dimension=3, b=True)(input2.last()) + Linear( + output_dimension=3, b=True + )(input4.last()) rel_2.closedLoop(state2) - out1 = Output('out1', rel_1) - out2 = Output('out2', rel_2) - out3 = Output('out_input1', input1.last()) - out4 = Output('out_input2', input2.last()) - out5 = Output('out_input3', input3.sw(4)) - out6 = Output('out_input4', input4.sw(4)) - out7 = Output('out_state1', state1.last()) - out8 = Output('out_state2', state2.last()) + out1 = Output("out1", rel_1) + out2 = Output("out2", rel_2) + out3 = Output("out_input1", input1.last()) + out4 = Output("out_input2", input2.last()) + out5 = Output("out_input3", input3.sw(4)) + out6 = Output("out_input4", input4.sw(4)) + out7 = Output("out_state1", state1.last()) + out8 = Output("out_state2", state2.last()) - test.addModel('model', [out1, out2, out3, out4, out5, out6, out7, out8]) + test.addModel("model", [out1, out2, out3, out4, out5, out6, out7, out8]) test.neuralizeModel() - test.exportONNX(inputs_order=['input1','input2','input3','input4','state1','state2'],outputs_order=['out1', 'out2', 'out_input1', 'out_input2', 'out_input3', 'out_input4', 'out_state1', 'out_state2']) + test.exportONNX( + inputs_order=["input1", "input2", "input3", "input4", "state1", "state2"], + outputs_order=[ + "out1", + "out2", + "out_input1", + "out_input2", + "out_input3", + "out_input4", + "out_state1", + "out_state2", + ], + ) ## Without Horizon and without batch - recurrent_sample = {'input1': np.random.rand(1,1,1,1).astype(np.float32), - 'input2': np.random.rand(1,1,1,3).astype(np.float32), - 'input3': np.random.rand(1,1,4,1).astype(np.float32), - 'input4': np.random.rand(1,1,4,3).astype(np.float32)} - recurrent_sample['state1'] = np.random.rand(1,1,1).astype(np.float32) - recurrent_sample['state2'] = np.random.rand(1,1,3).astype(np.float32) - inference = Modely(visualizer=None).onnxInference(recurrent_sample, model_folder=os.path.join(test.getWorkspace(),'onnx')) - self.assertListEqual(list(inference[0].shape), [1,1,1,1]) - self.assertListEqual(list(inference[1].shape), [1,1,1,3]) - self.assertListEqual(list(inference[2].shape), [1,1,1,1]) - self.assertListEqual(list(inference[3].shape), [1,1,1,3]) - self.assertListEqual(list(inference[4].shape), [1,1,4,1]) - self.assertListEqual(list(inference[5].shape), [1,1,4,3]) - self.assertListEqual(list(inference[6].shape), [1,1,1,1]) - self.assertListEqual(list(inference[7].shape), [1,1,1,3]) + recurrent_sample = { + "input1": np.random.rand(1, 1, 1, 1).astype(np.float32), + "input2": np.random.rand(1, 1, 1, 3).astype(np.float32), + "input3": np.random.rand(1, 1, 4, 1).astype(np.float32), + "input4": np.random.rand(1, 1, 4, 3).astype(np.float32), + } + recurrent_sample["state1"] = np.random.rand(1, 1, 1).astype(np.float32) + recurrent_sample["state2"] = np.random.rand(1, 1, 3).astype(np.float32) + inference = Modely(visualizer=None).onnxInference( + recurrent_sample, model_folder=os.path.join(test.getWorkspace(), "onnx") + ) + self.assertListEqual(list(inference[0].shape), [1, 1, 1, 1]) + self.assertListEqual(list(inference[1].shape), [1, 1, 1, 3]) + self.assertListEqual(list(inference[2].shape), [1, 1, 1, 1]) + self.assertListEqual(list(inference[3].shape), [1, 1, 1, 3]) + self.assertListEqual(list(inference[4].shape), [1, 1, 4, 1]) + self.assertListEqual(list(inference[5].shape), [1, 1, 4, 3]) + self.assertListEqual(list(inference[6].shape), [1, 1, 1, 1]) + self.assertListEqual(list(inference[7].shape), [1, 1, 1, 3]) ## With Horizon and without batch - recurrent_sample = {'input1': np.random.rand(5,1,1,1).astype(np.float32), - 'input2': np.random.rand(5,1,1,3).astype(np.float32), - 'input3': np.random.rand(5,1,4,1).astype(np.float32), - 'input4': np.random.rand(5,1,4,3).astype(np.float32)} - recurrent_sample['state1'] = np.random.rand(1,1,1).astype(np.float32) - recurrent_sample['state2'] = np.random.rand(1,1,3).astype(np.float32) - inference = Modely(visualizer=None,workspace=result_path).onnxInference(recurrent_sample) - self.assertListEqual(list(inference[0].shape), [5,1,1,1]) - self.assertListEqual(list(inference[1].shape), [5,1,1,3]) - self.assertListEqual(list(inference[2].shape), [5,1,1,1]) - self.assertListEqual(list(inference[3].shape), [5,1,1,3]) - self.assertListEqual(list(inference[4].shape), [5,1,4,1]) - self.assertListEqual(list(inference[5].shape), [5,1,4,3]) - self.assertListEqual(list(inference[6].shape), [5,1,1,1]) - self.assertListEqual(list(inference[7].shape), [5,1,1,3]) + recurrent_sample = { + "input1": np.random.rand(5, 1, 1, 1).astype(np.float32), + "input2": np.random.rand(5, 1, 1, 3).astype(np.float32), + "input3": np.random.rand(5, 1, 4, 1).astype(np.float32), + "input4": np.random.rand(5, 1, 4, 3).astype(np.float32), + } + recurrent_sample["state1"] = np.random.rand(1, 1, 1).astype(np.float32) + recurrent_sample["state2"] = np.random.rand(1, 1, 3).astype(np.float32) + inference = Modely(visualizer=None, workspace=result_path).onnxInference( + recurrent_sample + ) + self.assertListEqual(list(inference[0].shape), [5, 1, 1, 1]) + self.assertListEqual(list(inference[1].shape), [5, 1, 1, 3]) + self.assertListEqual(list(inference[2].shape), [5, 1, 1, 1]) + self.assertListEqual(list(inference[3].shape), [5, 1, 1, 3]) + self.assertListEqual(list(inference[4].shape), [5, 1, 4, 1]) + self.assertListEqual(list(inference[5].shape), [5, 1, 4, 3]) + self.assertListEqual(list(inference[6].shape), [5, 1, 1, 1]) + self.assertListEqual(list(inference[7].shape), [5, 1, 1, 3]) # ## With Horizon and with batch - recurrent_sample = {'input1': np.random.rand(5,2,1,1).astype(np.float32), - 'input2': np.random.rand(5,2,1,3).astype(np.float32), - 'input3': np.random.rand(5,2,4,1).astype(np.float32), - 'input4': np.random.rand(5,2,4,3).astype(np.float32)} - recurrent_sample['state1'] = np.random.rand(2,1,1).astype(np.float32) - recurrent_sample['state2'] = np.random.rand(2,1,3).astype(np.float32) - inference = Modely(visualizer=None).onnxInference(recurrent_sample, model_folder=os.path.join(test.getWorkspace(),'onnx'), name='net') - self.assertListEqual(list(inference[0].shape), [5,2,1,1]) - self.assertListEqual(list(inference[1].shape), [5,2,1,3]) - self.assertListEqual(list(inference[2].shape), [5,2,1,1]) - self.assertListEqual(list(inference[3].shape), [5,2,1,3]) - self.assertListEqual(list(inference[4].shape), [5,2,4,1]) - self.assertListEqual(list(inference[5].shape), [5,2,4,3]) - self.assertListEqual(list(inference[6].shape), [5,2,1,1]) - self.assertListEqual(list(inference[7].shape), [5,2,1,3]) + recurrent_sample = { + "input1": np.random.rand(5, 2, 1, 1).astype(np.float32), + "input2": np.random.rand(5, 2, 1, 3).astype(np.float32), + "input3": np.random.rand(5, 2, 4, 1).astype(np.float32), + "input4": np.random.rand(5, 2, 4, 3).astype(np.float32), + } + recurrent_sample["state1"] = np.random.rand(2, 1, 1).astype(np.float32) + recurrent_sample["state2"] = np.random.rand(2, 1, 3).astype(np.float32) + inference = Modely(visualizer=None).onnxInference( + recurrent_sample, + model_folder=os.path.join(test.getWorkspace(), "onnx"), + name="net", + ) + self.assertListEqual(list(inference[0].shape), [5, 2, 1, 1]) + self.assertListEqual(list(inference[1].shape), [5, 2, 1, 3]) + self.assertListEqual(list(inference[2].shape), [5, 2, 1, 1]) + self.assertListEqual(list(inference[3].shape), [5, 2, 1, 3]) + self.assertListEqual(list(inference[4].shape), [5, 2, 4, 1]) + self.assertListEqual(list(inference[5].shape), [5, 2, 4, 3]) + self.assertListEqual(list(inference[6].shape), [5, 2, 1, 1]) + self.assertListEqual(list(inference[7].shape), [5, 2, 1, 3]) if os.path.exists(test.getWorkspace()): shutil.rmtree(test.getWorkspace()) @@ -413,69 +599,98 @@ def test_export_onnx_module_recurrent(self): def test_export_and_import_python_module_complex_recurrent(self): NeuObj.clearNames() # Create nnodely structure - result_path = 'results' - network_name = 'vehicle' + result_path = "results" + network_name = "vehicle" vehicle = Modely(visualizer=None, seed=2, workspace=result_path) # Dimensions of the layers - n = 25 + n = 25 na = 21 - #Create neural model inputs - velocity = Input('vel') - brake = Input('brk') - gear = Input('gear') - torque = Input('trq') - altitude = Input('alt',dimensions=na) - acc = Input('acc') + # Create neural model inputs + velocity = Input("vel") + brake = Input("brk") + gear = Input("gear") + torque = Input("trq") + altitude = Input("alt", dimensions=na) + acc = Input("acc") # Create neural network relations - air_drag_force = Linear(b=True)(velocity.last()**2) - breaking_force = -Relu(Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n))) - gravity_force = Linear(W_init=init_constant, W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last()) - fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last()) - local_model = LocalModel(input_function=lambda: Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})) + air_drag_force = Linear(b=True)(velocity.last() ** 2) + breaking_force = -Relu( + Fir( + W_init=init_negexp, + W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3}, + )(brake.sw(n)) + ) + gravity_force = Linear( + W_init=init_constant, W_init_params={"value": 0}, dropout=0.1, W="gravity" + )(altitude.last()) + fuzzi_gear = Fuzzify(6, range=[2, 7], functions="Rectangular")(gear.last()) + local_model = LocalModel( + input_function=lambda: Fir( + W_init=init_negexp, + W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3}, + ) + ) engine_force = local_model(torque.sw(n), fuzzi_gear) - sum_rel = air_drag_force+breaking_force+gravity_force+engine_force + sum_rel = air_drag_force + breaking_force + gravity_force + engine_force sum_rel.closedLoop(velocity) # Create neural network output - out = Output('accelleration', sum_rel) + out = Output("accelleration", sum_rel) # Add the neural model to the nnodely structure and neuralization of the model - vehicle.addModel('acc',[out]) - vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse') + vehicle.addModel("acc", [out]) + vehicle.addMinimize("acc_error", acc.last(), out, loss_function="rmse") vehicle.neuralizeModel(0.05) # Load the training and the validation dataset - data_struct = ['vel','trq','brk','gear','alt','acc'] - data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'vehicle_data') - vehicle.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1) + data_struct = ["vel", "trq", "brk", "gear", "alt", "acc"] + data_folder = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "vehicle_data" + ) + vehicle.loadData( + name="dataset", source=data_folder, format=data_struct, skiplines=1 + ) # Inference - sample = vehicle.getSamples('dataset', window=3) + sample = vehicle.getSamples("dataset", window=3) model_inference = vehicle(sample, sampled=True, prediction_samples=3) vehicle.exportPythonModel(name=network_name) loaded_vehicle = Modely(visualizer=None, workspace=vehicle.getWorkspace()) loaded_vehicle.importPythonModel(name=network_name) - model_import_inference = loaded_vehicle(sample, sampled=True, prediction_samples=3) - self.assertEqual(model_inference['accelleration'], model_import_inference['accelleration']) + model_import_inference = loaded_vehicle( + sample, sampled=True, prediction_samples=3 + ) + self.assertEqual( + model_inference["accelleration"], model_import_inference["accelleration"] + ) ## Load the exported model.py ## Import the python exported module - #from results.vehicle import RecurrentModel - module = importlib.import_module(result_path+'.'+network_name) - RecurrentModel = getattr(module, 'RecurrentModel') + # from results.vehicle import RecurrentModel + module = importlib.import_module(result_path + "." + network_name) + RecurrentModel = getattr(module, "RecurrentModel") recurrent_model = RecurrentModel() - sample = vehicle.getSamples('dataset', window=3) - recurrent_sample = {key: torch.tensor(np.array(value), dtype=torch.float32).unsqueeze(1) for key, value in sample.items()} - recurrent_sample['vel'] = torch.zeros(1,1,1) - model_sample = {key: value for key, value in sample.items() if key != 'vel'} - self.TestAlmostEqual([item.detach().item() for item in recurrent_model(recurrent_sample)['accelleration']], vehicle(model_sample, sampled=True, prediction_samples=3)['accelleration']) + sample = vehicle.getSamples("dataset", window=3) + recurrent_sample = { + key: torch.tensor(np.array(value), dtype=torch.float32).unsqueeze(1) + for key, value in sample.items() + } + recurrent_sample["vel"] = torch.zeros(1, 1, 1) + model_sample = {key: value for key, value in sample.items() if key != "vel"} + self.TestAlmostEqual( + [ + item.detach().item() + for item in recurrent_model(recurrent_sample)["accelleration"] + ], + vehicle(model_sample, sampled=True, prediction_samples=3)["accelleration"], + ) if os.path.exists(vehicle.getWorkspace()): shutil.rmtree(vehicle.getWorkspace()) @@ -483,62 +698,100 @@ def test_export_and_import_python_module_complex_recurrent(self): def test_export_and_import_onnx_module_complex_recurrent(self): NeuObj.clearNames() # Create nnodely structure - result_path = 'results' - network_name = 'vehicle' - vehicle = Modely(visualizer=None, seed=42, workspace= result_path) + result_path = "results" + network_name = "vehicle" + vehicle = Modely(visualizer=None, seed=42, workspace=result_path) # Dimensions of the layers - n = 25 + n = 25 na = 21 - #Create neural model inputs - velocity = Input('vel') - brake = Input('brk') - gear = Input('gear') - torque = Input('trq') - altitude = Input('alt',dimensions=na) - acc = Input('acc') + # Create neural model inputs + velocity = Input("vel") + brake = Input("brk") + gear = Input("gear") + torque = Input("trq") + altitude = Input("alt", dimensions=na) + acc = Input("acc") # Create neural network relations - air_drag_force = Linear(b=True)(velocity.last()**2) - breaking_force = -Relu(Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})(brake.sw(n))) - gravity_force = Linear(W_init=init_constant, W_init_params={'value':0}, dropout=0.1, W='gravity')(altitude.last()) - fuzzi_gear = Fuzzify(6, range=[2,7], functions='Rectangular')(gear.last()) - local_model = LocalModel(input_function=lambda: Fir(W_init = init_negexp, W_init_params={'size_index':0, 'first_value':0.002, 'lambda':3})) + air_drag_force = Linear(b=True)(velocity.last() ** 2) + breaking_force = -Relu( + Fir( + W_init=init_negexp, + W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3}, + )(brake.sw(n)) + ) + gravity_force = Linear( + W_init=init_constant, W_init_params={"value": 0}, dropout=0.1, W="gravity" + )(altitude.last()) + fuzzi_gear = Fuzzify(6, range=[2, 7], functions="Rectangular")(gear.last()) + local_model = LocalModel( + input_function=lambda: Fir( + W_init=init_negexp, + W_init_params={"size_index": 0, "first_value": 0.002, "lambda": 3}, + ) + ) engine_force = local_model(torque.sw(n), fuzzi_gear) - sum_rel = air_drag_force+breaking_force+gravity_force+engine_force + sum_rel = air_drag_force + breaking_force + gravity_force + engine_force sum_rel.closedLoop(velocity) # Create neural network output - out = Output('accelleration', sum_rel) + out = Output("accelleration", sum_rel) # Add the neural model to the nnodely structure and neuralization of the model - vehicle.addModel('acc',[out]) - vehicle.addMinimize('acc_error', acc.last(), out, loss_function='rmse') + vehicle.addModel("acc", [out]) + vehicle.addMinimize("acc_error", acc.last(), out, loss_function="rmse") vehicle.neuralizeModel(0.05) # Load the training and the validation dataset - data_struct = ['vel','trq','brk','gear','alt','acc'] - data_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)),'vehicle_data') - vehicle.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1) + data_struct = ["vel", "trq", "brk", "gear", "alt", "acc"] + data_folder = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "vehicle_data" + ) + vehicle.loadData( + name="dataset", source=data_folder, format=data_struct, skiplines=1 + ) ## Export the Onnx Model vehicle.exportONNX(name=network_name) - model_sample = vehicle.getSamples('dataset', window=1) + model_sample = vehicle.getSamples("dataset", window=1) model_inference = vehicle(model_sample, sampled=True, prediction_samples=1) ## ONNX IMPORT - onnx_sample = {key: (np.expand_dims(value, axis=1).astype(np.float32) if key != 'vel' else value) for key, value in model_sample.items()} - outputs = Modely(visualizer=None).onnxInference(onnx_sample, name=network_name, model_folder=os.path.join(result_path,'onnx')) - self.assertEqual(outputs[0][0], model_inference['accelleration']) - - model_sample = vehicle.getSamples('dataset', window=3) - onnx_sample = {key: (np.expand_dims(value, axis=1).astype(np.float32) if key != 'vel' else np.expand_dims(np.array(value[0], dtype=np.float32), axis=0)) for key, value in model_sample.items()} + onnx_sample = { + key: ( + np.expand_dims(value, axis=1).astype(np.float32) + if key != "vel" + else value + ) + for key, value in model_sample.items() + } + outputs = Modely(visualizer=None).onnxInference( + onnx_sample, + name=network_name, + model_folder=os.path.join(result_path, "onnx"), + ) + self.assertEqual(outputs[0][0], model_inference["accelleration"]) + + model_sample = vehicle.getSamples("dataset", window=3) + onnx_sample = { + key: ( + np.expand_dims(value, axis=1).astype(np.float32) + if key != "vel" + else np.expand_dims(np.array(value[0], dtype=np.float32), axis=0) + ) + for key, value in model_sample.items() + } model_inference = vehicle(model_sample, sampled=True, prediction_samples=3) - outputs = Modely(visualizer=None, workspace=result_path).onnxInference(onnx_sample, name=network_name) - self.assertEqual(outputs[0].squeeze().tolist(), model_inference['accelleration']) + outputs = Modely(visualizer=None, workspace=result_path).onnxInference( + onnx_sample, name=network_name + ) + self.assertEqual( + outputs[0].squeeze().tolist(), model_inference["accelleration"] + ) if os.path.exists(vehicle.getWorkspace()): shutil.rmtree(vehicle.getWorkspace()) @@ -547,62 +800,76 @@ def test_export_sw_on_stream_sw_complex(self): NeuObj.clearNames() Stream.resetCount() # Create nnodely structure - result_path = 'results' - network_name = 'swnet' - input = Input('inin') + result_path = "results" + network_name = "swnet" + input = Input("inin") sw_7 = input.sw(7) - out61 = Output('out61', sw_7.sw(6)) - out62 = Output('out62', SamplePart(sw_7,1,7)) + out61 = Output("out61", sw_7.sw(6)) + out62 = Output("out62", SamplePart(sw_7, 1, 7)) test = Modely(visualizer=None, workspace=result_path) - test.addModel('out', [out61,out62]) + test.addModel("out", [out61, out62]) test.neuralizeModel() sample = [14, 1, 2, 3, 4, 5, 6] - results = test({'inin':sample}) - - test.exportONNX(inputs_order=['inin','SamplePart1_sw1'],outputs_order=['out61','out62'],name=network_name) - outputs = Modely(visualizer=None, workspace=result_path).onnxInference({'inin':np.array([[[[14],[1],[2],[3],[4],[5],[6]]]]).astype(np.float32),'SamplePart1_sw1':np.array([[[0],[0],[0],[0],[0],[0]]]).astype(np.float32)}, name=network_name) - self.assertEqual(outputs[0].squeeze().tolist(), results['out61'][0]) - self.assertEqual(outputs[1].squeeze().tolist(), results['out62'][0]) - self.assertEqual(results['out61'][0], results['out62'][0]) + results = test({"inin": sample}) + + test.exportONNX( + inputs_order=["inin", "SamplePart1_sw1"], + outputs_order=["out61", "out62"], + name=network_name, + ) + outputs = Modely(visualizer=None, workspace=result_path).onnxInference( + { + "inin": np.array([[[[14], [1], [2], [3], [4], [5], [6]]]]).astype( + np.float32 + ), + "SamplePart1_sw1": np.array([[[0], [0], [0], [0], [0], [0]]]).astype( + np.float32 + ), + }, + name=network_name, + ) + self.assertEqual(outputs[0].squeeze().tolist(), results["out61"][0]) + self.assertEqual(outputs[1].squeeze().tolist(), results["out62"][0]) + self.assertEqual(results["out61"][0], results["out62"][0]) if os.path.exists(test.getWorkspace()): shutil.rmtree(test.getWorkspace()) def test_partial_model_export(self): - #We have 4 networks, A, B, C and D + # We have 4 networks, A, B, C and D # Connection inside model 1 - #Aout is connected to Bin1 - #Bout is closed_loop to Ain2 - #Bout is clodes_loop to Bin2 + # Aout is connected to Bin1 + # Bout is closed_loop to Ain2 + # Bout is clodes_loop to Bin2 # Connection inside model 2 - #Cout is connected to Din1 + # Cout is connected to Din1 # Connection outside models in model 2 - #Dout closed_loop to Cin1 - #Cout connect to Din2 + # Dout closed_loop to Cin1 + # Cout connect to Din2 # Connection outside models between models - #Dout closed_loop to Ain1 - #Aout connected to Din3 - #Bout closed_loop to Cin2 + # Dout closed_loop to Ain1 + # Aout connected to Din3 + # Bout closed_loop to Cin2 - result_path = 'results' + result_path = "results" NeuObj.clearNames() - #log.setAllLevel(logging.INFO) + # log.setAllLevel(logging.INFO) - #Network A and B -> Model1 - Ain1 = Input('Ain1') - Ain2 = Input('Ain2') - Bin1 = Input('Bin1') - Bin2 = Input('Bin2') - Bin3 = Input('Bin3') + # Network A and B -> Model1 + Ain1 = Input("Ain1") + Ain2 = Input("Ain2") + Bin1 = Input("Bin1") + Bin2 = Input("Bin2") + Bin3 = Input("Bin3") - pA = Parameter('PA', sw=1, values=[[3.0]]) - pB = Parameter('PB', sw=1, values=[[-5.0]]) + pA = Parameter("PA", sw=1, values=[[3.0]]) + pB = Parameter("PB", sw=1, values=[[-5.0]]) Aout = (Ain1.last() + Ain2.last()) * pA Aout.connect(Bin1) @@ -611,337 +878,437 @@ def test_partial_model_export(self): Bout.closedLoop(Ain2) Bout.closedLoop(Bin2) - modelA = Output('Aout',Aout) - modelB = Output('Bout',Bout) + modelA = Output("Aout", Aout) + modelB = Output("Bout", Bout) - Cin1 = Input('Cin1') - Cin2 = Input('Cin2') - Din1 = Input('Din1') - Din2 = Input('Din2') - Din3 = Input('Din3') + Cin1 = Input("Cin1") + Cin2 = Input("Cin2") + Din1 = Input("Din1") + Din2 = Input("Din2") + Din3 = Input("Din3") - pC = Parameter('PC', sw=1, values=[[-1.0]]) - pD = Parameter('PD', sw=1, values=[[2.0]]) + pC = Parameter("PC", sw=1, values=[[-1.0]]) + pD = Parameter("PD", sw=1, values=[[2.0]]) Cout = (Cin1.last() + Cin2.last()) * pC Cout.connect(Din1) Dout = (Din1.last() + Din2.last() + Din3.last()) * pD - modelC = Output('Cout',Cout) - modelD = Output('Dout',Dout) + modelC = Output("Cout", Cout) + modelD = Output("Dout", Dout) m = Modely(workspace=result_path, visualizer=None) with self.assertRaises(RuntimeError): - m.addModel('modelA', [modelA]) + m.addModel("modelA", [modelA]) with self.assertRaises(RuntimeError): - m.addModel('modelB', [modelB]) - - m.addModel('model1', [modelA, modelB]) - m.addModel('model2', [modelC, modelD]) - - init_inputs = {'Din3': [1], 'Din2': [1], 'Cin1': [1], 'Cin2': [1], 'Ain1': [1], 'Bin3': [1]} - init_states = {'Din1': [1], 'Bin2':[1], 'Ain2':[1], 'Bin1': [1]} - init_states_diff = {'Din1': [12], 'Bin2': [1], 'Ain2': [1], 'Bin1': [12]} + m.addModel("modelB", [modelB]) + + m.addModel("model1", [modelA, modelB]) + m.addModel("model2", [modelC, modelD]) + + init_inputs = { + "Din3": [1], + "Din2": [1], + "Cin1": [1], + "Cin2": [1], + "Ain1": [1], + "Bin3": [1], + } + init_states = {"Din1": [1], "Bin2": [1], "Ain2": [1], "Bin1": [1]} + init_states_diff = {"Din1": [12], "Bin2": [1], "Ain2": [1], "Bin1": [12]} # Target with states = 0.0 - results_target = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0], - 'Cout': [(1.0 + 1.0) * -1.0], - 'Bout': [(3.0 + 1.0 + 0.0) * -5.0], - 'Aout': [(1.0 + 0.0) * 3.0]} + results_target = { + "Dout": [(-2.0 + 1.0 + 1.0) * 2.0], + "Cout": [(1.0 + 1.0) * -1.0], + "Bout": [(3.0 + 1.0 + 0.0) * -5.0], + "Aout": [(1.0 + 0.0) * 3.0], + } # Target with states = 1.0 - results_target_state = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0], - 'Cout': [(1.0 + 1.0) * -1.0], - 'Bout': [(6.0 + 1.0 + 1.0) * -5.0], - 'Aout': [(1.0 + 1.0) * 3.0]} + results_target_state = { + "Dout": [(-2.0 + 1.0 + 1.0) * 2.0], + "Cout": [(1.0 + 1.0) * -1.0], + "Bout": [(6.0 + 1.0 + 1.0) * -5.0], + "Aout": [(1.0 + 1.0) * 3.0], + } m.neuralizeModel() self.assertEqual(results_target, m(init_inputs)) - self.assertEqual(results_target_state, m(init_inputs|init_states)) - self.assertEqual(results_target_state, m(init_inputs|init_states_diff)) + self.assertEqual(results_target_state, m(init_inputs | init_states)) + self.assertEqual(results_target_state, m(init_inputs | init_states_diff)) ## Test loading of all models m.saveTorchModel() l = Modely(workspace=result_path, visualizer=None) - l.addModel('model1', [modelA, modelB]) - l.addModel('model2', [modelC, modelD]) + l.addModel("model1", [modelA, modelB]) + l.addModel("model2", [modelC, modelD]) l.neuralizeModel() - l.parameters['PA'] = [[22.0]] + l.parameters["PA"] = [[22.0]] l.loadTorchModel() self.assertEqual(results_target, l(init_inputs)) - self.assertEqual(results_target_state, l(init_inputs|init_states)) - self.assertEqual(results_target_state, l(init_inputs|init_states_diff)) + self.assertEqual(results_target_state, l(init_inputs | init_states)) + self.assertEqual(results_target_state, l(init_inputs | init_states_diff)) m.saveModel() l = Modely(workspace=result_path, visualizer=None) l.loadModel() l.neuralizeModel() self.assertEqual(results_target, l(init_inputs)) - self.assertEqual(results_target_state, l(init_inputs|init_states)) - self.assertEqual(results_target_state, l(init_inputs|init_states_diff)) + self.assertEqual(results_target_state, l(init_inputs | init_states)) + self.assertEqual(results_target_state, l(init_inputs | init_states_diff)) m.exportPythonModel() l = Modely(workspace=result_path, visualizer=None) l.importPythonModel() self.assertEqual(results_target, l(init_inputs)) - self.assertEqual(results_target_state, l(init_inputs|init_states)) - self.assertEqual(results_target_state, l(init_inputs|init_states_diff)) - - m.exportONNX(outputs_order=['Dout', 'Cout', 'Bout', 'Aout']) - init_inputs_ox = {'Din1':np.array([[[7.0]]]).astype(np.float32), - 'Bin2':np.array([[[0]]]).astype(np.float32), - 'Ain2':np.array([[[0]]]).astype(np.float32), - 'Bin1':np.array([[[12.0]]]).astype(np.float32)} - results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_ox | - {'Din2': np.array([[[[1]]]]).astype(np.float32), - 'Din3': np.array([[[[1]]]]).astype(np.float32), - 'Cin1': np.array([[[[1]]]]).astype(np.float32), - 'Cin2': np.array([[[[1]]]]).astype(np.float32), - 'Ain1':np.array([[[[1]]]]).astype(np.float32), - 'Bin3': np.array([[[[1]]]]).astype(np.float32)}) - self.assertEqual([[[[[0.0]]]], [[[[-2.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results) + self.assertEqual(results_target_state, l(init_inputs | init_states)) + self.assertEqual(results_target_state, l(init_inputs | init_states_diff)) + + m.exportONNX(outputs_order=["Dout", "Cout", "Bout", "Aout"]) + init_inputs_ox = { + "Din1": np.array([[[7.0]]]).astype(np.float32), + "Bin2": np.array([[[0]]]).astype(np.float32), + "Ain2": np.array([[[0]]]).astype(np.float32), + "Bin1": np.array([[[12.0]]]).astype(np.float32), + } + results = Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs_ox + | { + "Din2": np.array([[[[1]]]]).astype(np.float32), + "Din3": np.array([[[[1]]]]).astype(np.float32), + "Cin1": np.array([[[[1]]]]).astype(np.float32), + "Cin2": np.array([[[[1]]]]).astype(np.float32), + "Ain1": np.array([[[[1]]]]).astype(np.float32), + "Bin3": np.array([[[[1]]]]).astype(np.float32), + } + ) + self.assertEqual( + [[[[[0.0]]]], [[[[-2.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results + ) ## Testing loading of model1 - results_target_m1 = {'Bout': [-20.0], 'Aout': [3.0]} - results_target_state_m1 = {'Bout': [(6.0 + 1.0 + 1.0) * -5.0], - 'Aout': [(1.0 + 1.0) * 3.0]} - m.saveTorchModel(models='model1') + results_target_m1 = {"Bout": [-20.0], "Aout": [3.0]} + results_target_state_m1 = { + "Bout": [(6.0 + 1.0 + 1.0) * -5.0], + "Aout": [(1.0 + 1.0) * 3.0], + } + m.saveTorchModel(models="model1") l = Modely(workspace=result_path, visualizer=None) - l.addModel('model1', [modelA, modelB]) + l.addModel("model1", [modelA, modelB]) l.neuralizeModel() - l.parameters['PA'] = [[22.0]] - l.loadTorchModel(name='net_model1') + l.parameters["PA"] = [[22.0]] + l.loadTorchModel(name="net_model1") self.assertEqual(results_target_m1, l(init_inputs)) self.assertEqual(results_target_state_m1, l(init_inputs | init_states)) self.assertEqual(results_target_state_m1, l(init_inputs | init_states_diff)) - m.saveModel(models='model1') + m.saveModel(models="model1") l = Modely(workspace=result_path, visualizer=None) - l.loadModel(name='net_model1') + l.loadModel(name="net_model1") l.neuralizeModel() self.assertEqual(results_target_m1, l(init_inputs)) self.assertEqual(results_target_state_m1, l(init_inputs | init_states)) self.assertEqual(results_target_state_m1, l(init_inputs | init_states_diff)) - m.exportPythonModel(models='model1') + m.exportPythonModel(models="model1") l = Modely(workspace=result_path, visualizer=None) - l.importPythonModel(name='net_model1') + l.importPythonModel(name="net_model1") self.assertEqual(results_target_m1, l(init_inputs)) self.assertEqual(results_target_state_m1, l(init_inputs | init_states)) self.assertEqual(results_target_state_m1, l(init_inputs | init_states_diff)) - m.exportONNX(models='model1', outputs_order=['Bout', 'Aout']) - init_inputs_m1_ox = {'Bin2':np.array([[[0]]]).astype(np.float32), - 'Ain2':np.array([[[0]]]).astype(np.float32), - 'Bin1':np.array([[[12.0]]]).astype(np.float32)} - results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_m1_ox | - {'Ain1':np.array([[[[1]]]]).astype(np.float32), - 'Bin3': np.array([[[[1]]]]).astype(np.float32)}, name='net_model1') + m.exportONNX(models="model1", outputs_order=["Bout", "Aout"]) + init_inputs_m1_ox = { + "Bin2": np.array([[[0]]]).astype(np.float32), + "Ain2": np.array([[[0]]]).astype(np.float32), + "Bin1": np.array([[[12.0]]]).astype(np.float32), + } + results = Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs_m1_ox + | { + "Ain1": np.array([[[[1]]]]).astype(np.float32), + "Bin3": np.array([[[[1]]]]).astype(np.float32), + }, + name="net_model1", + ) self.assertEqual([[[[[-20.0]]]], [[[[3.0]]]]], results) ### Add the connect on model2 ### - m.addConnect(Cout,Din2) - m.addClosedLoop(Dout,Cin1) + m.addConnect(Cout, Din2) + m.addClosedLoop(Dout, Cin1) m.neuralizeModel() - init_inputs_2 = {'Din3': [1], 'Cin2': [1], 'Ain1': [1], 'Bin3': [1]} - init_states_2 = {'Din1': [1], 'Cin1': [1], 'Din2': [1], 'Bin2':[1], 'Ain2':[1], 'Bin1': [1]} - init_states_diff_2 = {'Din1': [12], 'Cin1': [1], 'Din2': [30], 'Bin2': [1], 'Ain2': [1], 'Bin1': [12]} + init_inputs_2 = {"Din3": [1], "Cin2": [1], "Ain1": [1], "Bin3": [1]} + init_states_2 = { + "Din1": [1], + "Cin1": [1], + "Din2": [1], + "Bin2": [1], + "Ain2": [1], + "Bin1": [1], + } + init_states_diff_2 = { + "Din1": [12], + "Cin1": [1], + "Din2": [30], + "Bin2": [1], + "Ain2": [1], + "Bin1": [12], + } # Target with states = 0.0 - results_target_2 = {'Dout': [(-1.0 - 1.0 + 1.0) * 2.0], - 'Cout': [(0.0 + 1.0) * -1.0], - 'Bout': [(3.0 + 1.0 + 0.0) * -5.0], - 'Aout': [(1.0 + 0.0) * 3.0]} + results_target_2 = { + "Dout": [(-1.0 - 1.0 + 1.0) * 2.0], + "Cout": [(0.0 + 1.0) * -1.0], + "Bout": [(3.0 + 1.0 + 0.0) * -5.0], + "Aout": [(1.0 + 0.0) * 3.0], + } # Target with states = 1.0 - results_target_state_2 = {'Dout': [(-2.0 - 2.0 + 1.0) * 2.0], - 'Cout': [(1.0 + 1.0) * -1.0], - 'Bout': [(6.0 + 1.0 + 1.0) * -5.0], - 'Aout': [(1.0 + 1.0) * 3.0]} + results_target_state_2 = { + "Dout": [(-2.0 - 2.0 + 1.0) * 2.0], + "Cout": [(1.0 + 1.0) * -1.0], + "Bout": [(6.0 + 1.0 + 1.0) * -5.0], + "Aout": [(1.0 + 1.0) * 3.0], + } self.assertEqual(results_target_2, m(init_inputs_2)) - self.assertEqual(results_target_state_2, m(init_inputs_2|init_states_2)) - self.assertEqual(results_target_state_2, m(init_inputs_2|init_states_diff_2)) + self.assertEqual(results_target_state_2, m(init_inputs_2 | init_states_2)) + self.assertEqual(results_target_state_2, m(init_inputs_2 | init_states_diff_2)) ## Test loading of all models m.saveTorchModel() l = Modely(workspace=result_path, visualizer=None) - l.addModel('model1', [modelA, modelB]) - l.addModel('model2', [modelC, modelD]) + l.addModel("model1", [modelA, modelB]) + l.addModel("model2", [modelC, modelD]) l.neuralizeModel() - l.parameters['PD'] = [[22.0]] + l.parameters["PD"] = [[22.0]] l.loadTorchModel() self.assertEqual(results_target, l(init_inputs)) - self.assertEqual(results_target_state, l(init_inputs|init_states)) - self.assertEqual(results_target_state, l(init_inputs|init_states_diff)) + self.assertEqual(results_target_state, l(init_inputs | init_states)) + self.assertEqual(results_target_state, l(init_inputs | init_states_diff)) l2 = Modely(workspace=result_path, visualizer=None) - l2.addModel('model1', [modelA, modelB]) - l2.addModel('model2', [modelC, modelD]) - l2.addConnect(Cout,Din2) - l2.addClosedLoop(Dout,Cin1) + l2.addModel("model1", [modelA, modelB]) + l2.addModel("model2", [modelC, modelD]) + l2.addConnect(Cout, Din2) + l2.addClosedLoop(Dout, Cin1) l2.neuralizeModel() - l.parameters['PA'] = [[22.0]] + l.parameters["PA"] = [[22.0]] l2.loadTorchModel() self.assertEqual(results_target_2, l2(init_inputs_2)) - self.assertEqual(results_target_state_2, l2(init_inputs_2|init_states_2)) - self.assertEqual(results_target_state_2, l2(init_inputs_2|init_states_diff_2)) + self.assertEqual(results_target_state_2, l2(init_inputs_2 | init_states_2)) + self.assertEqual(results_target_state_2, l2(init_inputs_2 | init_states_diff_2)) m.saveModel() l = Modely(workspace=result_path, visualizer=None) l.loadModel() l.neuralizeModel() self.assertEqual(results_target_2, l(init_inputs_2)) - self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_2)) - self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_diff_2)) + self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_2)) + self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_diff_2)) m.exportPythonModel() l = Modely(workspace=result_path, visualizer=None) l.importPythonModel() self.assertEqual(results_target_2, l(init_inputs_2)) - self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_2)) - self.assertEqual(results_target_state_2, l(init_inputs_2|init_states_diff_2)) - - m.exportONNX(outputs_order=['Dout', 'Cout', 'Bout', 'Aout']) - init_inputs_2_ox = {'Din3': np.array([[[[1.0]]]]).astype(np.float32), - 'Cin2': np.array([[[[1.0]]]]).astype(np.float32), - 'Ain1': np.array([[[[1.0]]]]).astype(np.float32), - 'Bin3': np.array([[[[1.0]]]]).astype(np.float32)} - init_states_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32), - 'Cin1': np.array([[[0.0]]]).astype(np.float32), - 'Din2': np.array([[[20.0]]]).astype(np.float32), - 'Bin2': np.array([[[0.0]]]).astype(np.float32), - 'Ain2': np.array([[[0.0]]]).astype(np.float32), - 'Bin1': np.array([[[34.0]]]).astype(np.float32)} - init_states_diff_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32), - 'Cin1': np.array([[[1.0]]]).astype(np.float32), - 'Din2': np.array([[[23.0]]]).astype(np.float32), - 'Bin2': np.array([[[1.0]]]).astype(np.float32), - 'Ain2': np.array([[[1.0]]]).astype(np.float32), - 'Bin1': np.array([[[12.0]]]).astype(np.float32)} - results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_2_ox) - self.assertEqual([[[[[-2.0]]]], [[[[-1.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results) - results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_diff_2_ox) - self.assertEqual([[[[[-6.0]]]], [[[[-2.0]]]], [[[[-40.0]]]], [[[[6.0]]]]], results) - + self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_2)) + self.assertEqual(results_target_state_2, l(init_inputs_2 | init_states_diff_2)) + + m.exportONNX(outputs_order=["Dout", "Cout", "Bout", "Aout"]) + init_inputs_2_ox = { + "Din3": np.array([[[[1.0]]]]).astype(np.float32), + "Cin2": np.array([[[[1.0]]]]).astype(np.float32), + "Ain1": np.array([[[[1.0]]]]).astype(np.float32), + "Bin3": np.array([[[[1.0]]]]).astype(np.float32), + } + init_states_2_ox = { + "Din1": np.array([[[12.0]]]).astype(np.float32), + "Cin1": np.array([[[0.0]]]).astype(np.float32), + "Din2": np.array([[[20.0]]]).astype(np.float32), + "Bin2": np.array([[[0.0]]]).astype(np.float32), + "Ain2": np.array([[[0.0]]]).astype(np.float32), + "Bin1": np.array([[[34.0]]]).astype(np.float32), + } + init_states_diff_2_ox = { + "Din1": np.array([[[12.0]]]).astype(np.float32), + "Cin1": np.array([[[1.0]]]).astype(np.float32), + "Din2": np.array([[[23.0]]]).astype(np.float32), + "Bin2": np.array([[[1.0]]]).astype(np.float32), + "Ain2": np.array([[[1.0]]]).astype(np.float32), + "Bin1": np.array([[[12.0]]]).astype(np.float32), + } + results = Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs_2_ox | init_states_2_ox + ) + self.assertEqual( + [[[[[-2.0]]]], [[[[-1.0]]]], [[[[-20.0]]]], [[[[3.0]]]]], results + ) + results = Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs_2_ox | init_states_diff_2_ox + ) + self.assertEqual( + [[[[[-6.0]]]], [[[[-2.0]]]], [[[[-40.0]]]], [[[[6.0]]]]], results + ) ## Testing loading of model1 # Target with states = 0.0 - results_target_m2 = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0], - 'Cout': [(1.0 + 1.0) * -1.0]} - results_target_state_m2 = {'Dout': [(-2.0 + 1.0 + 1.0) * 2.0], - 'Cout': [(1.0 + 1.0) * -1.0]} - results_target_2_m2 = {'Dout': [(-1.0 - 1.0 + 1.0) * 2.0], - 'Cout': [(0.0 + 1.0) * -1.0]} - results_target_state_2_m2 = {'Dout': [(-2.0 - 2.0 + 1.0) * 2.0], - 'Cout': [(1.0 + 1.0) * -1.0]} - m.saveTorchModel(models='model2') + results_target_m2 = { + "Dout": [(-2.0 + 1.0 + 1.0) * 2.0], + "Cout": [(1.0 + 1.0) * -1.0], + } + results_target_state_m2 = { + "Dout": [(-2.0 + 1.0 + 1.0) * 2.0], + "Cout": [(1.0 + 1.0) * -1.0], + } + results_target_2_m2 = { + "Dout": [(-1.0 - 1.0 + 1.0) * 2.0], + "Cout": [(0.0 + 1.0) * -1.0], + } + results_target_state_2_m2 = { + "Dout": [(-2.0 - 2.0 + 1.0) * 2.0], + "Cout": [(1.0 + 1.0) * -1.0], + } + m.saveTorchModel(models="model2") l = Modely(workspace=result_path, visualizer=None) - l.addModel('model2', [modelC, modelD]) + l.addModel("model2", [modelC, modelD]) l.neuralizeModel() - l.parameters['PD'] = [[22.0]] - l.loadTorchModel(name='net_model2') + l.parameters["PD"] = [[22.0]] + l.loadTorchModel(name="net_model2") self.assertEqual(results_target_m2, l(init_inputs)) self.assertEqual(results_target_state_m2, l(init_inputs | init_states)) self.assertEqual(results_target_state_m2, l(init_inputs | init_states_diff)) l2 = Modely(workspace=result_path, visualizer=None) - l2.addModel('model2', [modelC, modelD]) - l2.addConnect(Cout,Din2) - l2.addClosedLoop(Dout,Cin1) + l2.addModel("model2", [modelC, modelD]) + l2.addConnect(Cout, Din2) + l2.addClosedLoop(Dout, Cin1) l2.neuralizeModel() - l2.parameters['PD'] = [[22.0]] + l2.parameters["PD"] = [[22.0]] with self.assertRaises(FileNotFoundError): - l2.loadTorchModel(name='net_m22',model_folder='test') - l2.loadTorchModel(name='net_model2') + l2.loadTorchModel(name="net_m22", model_folder="test") + l2.loadTorchModel(name="net_model2") self.assertEqual(results_target_2_m2, l2(init_inputs_2)) self.assertEqual(results_target_state_2_m2, l2(init_inputs_2 | init_states_2)) - self.assertEqual(results_target_state_2_m2, l2(init_inputs_2 | init_states_diff_2)) + self.assertEqual( + results_target_state_2_m2, l2(init_inputs_2 | init_states_diff_2) + ) - m.saveModel(models='model2') + m.saveModel(models="model2") l = Modely(workspace=result_path, visualizer=None) with self.assertRaises(FileNotFoundError): - l.loadModel(name='_model2') - l.loadModel(name='net_model2') + l.loadModel(name="_model2") + l.loadModel(name="net_model2") l.neuralizeModel() self.assertEqual(results_target_2_m2, l(init_inputs_2)) self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_2)) - self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2)) + self.assertEqual( + results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2) + ) - m.exportPythonModel(models='model2') + m.exportPythonModel(models="model2") l = Modely(workspace=result_path, visualizer=None) with self.assertRaises(FileNotFoundError): - l.importPythonModel(name='net_model22') - l.importPythonModel(name='net_model2') + l.importPythonModel(name="net_model22") + l.importPythonModel(name="net_model2") self.assertEqual(results_target_2_m2, l(init_inputs_2)) self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_2)) - self.assertEqual(results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2)) - - m.exportONNX(models='model2', outputs_order=['Cout', 'Dout']) - init_inputs_2_ox = {'Din3': np.array([[[[1.0]]]]).astype(np.float32), - 'Cin2': np.array([[[[1.0]]]]).astype(np.float32)} - init_states_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32), - 'Cin1': np.array([[[0.0]]]).astype(np.float32), - 'Din2': np.array([[[20.0]]]).astype(np.float32)} - init_states_diff_2_ox = {'Din1': np.array([[[12.0]]]).astype(np.float32), - 'Cin1': np.array([[[1.0]]]).astype(np.float32), - 'Din2': np.array([[[23.0]]]).astype(np.float32)} + self.assertEqual( + results_target_state_2_m2, l(init_inputs_2 | init_states_diff_2) + ) + + m.exportONNX(models="model2", outputs_order=["Cout", "Dout"]) + init_inputs_2_ox = { + "Din3": np.array([[[[1.0]]]]).astype(np.float32), + "Cin2": np.array([[[[1.0]]]]).astype(np.float32), + } + init_states_2_ox = { + "Din1": np.array([[[12.0]]]).astype(np.float32), + "Cin1": np.array([[[0.0]]]).astype(np.float32), + "Din2": np.array([[[20.0]]]).astype(np.float32), + } + init_states_diff_2_ox = { + "Din1": np.array([[[12.0]]]).astype(np.float32), + "Cin1": np.array([[[1.0]]]).astype(np.float32), + "Din2": np.array([[[23.0]]]).astype(np.float32), + } with self.assertRaises(FileNotFoundError): - Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_2_ox, name='net_models2') - results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_2_ox, name='net_model2') + Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs_2_ox | init_states_2_ox, name="net_models2" + ) + results = Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs_2_ox | init_states_2_ox, name="net_model2" + ) self.assertEqual([[[[[-1.0]]]], [[[[-2.0]]]]], results) - results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs_2_ox | init_states_diff_2_ox, name='net_model2') + results = Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs_2_ox | init_states_diff_2_ox, name="net_model2" + ) self.assertEqual([[[[[-2.0]]]], [[[[-6.0]]]]], results) - #log.setAllLevel(logging.CRITICAL) + # log.setAllLevel(logging.CRITICAL) if os.path.exists(m.getWorkspace()): shutil.rmtree(m.getWorkspace()) def test_partial_model_export_intern_extern_connection(self): - result_path = 'results' + result_path = "results" NeuObj.clearNames() - #Network A and B -> Model1 - Ain1 = Input('Ain1') - Bin1 = Input('Bin1') + # Network A and B -> Model1 + Ain1 = Input("Ain1") + Bin1 = Input("Bin1") - pA = Parameter('PA', sw=1, values=[[3.0]]) - pB = Parameter('PB', sw=1, values=[[-5.0]]) + pA = Parameter("PA", sw=1, values=[[3.0]]) + pB = Parameter("PB", sw=1, values=[[-5.0]]) - Aout = (Ain1.last() * pA).sw(1, name='AoutD').s(-1,method='trapezoidal',int_name='int_AoutD',der_name='der_AoutD') - Bout = (Bin1.last() * pB).tw(1, name='BoutD').s(1,method='trapezoidal',int_name='int_BoutD',der_name='der_BoutD') + Aout = ( + (Ain1.last() * pA) + .sw(1, name="AoutD") + .s(-1, method="trapezoidal", int_name="int_AoutD", der_name="der_AoutD") + ) + Bout = ( + (Bin1.last() * pB) + .tw(1, name="BoutD") + .s(1, method="trapezoidal", int_name="int_BoutD", der_name="der_BoutD") + ) - modelA = Output('Aout',Aout) - modelB = Output('Bout',Bout) + modelA = Output("Aout", Aout) + modelB = Output("Bout", Bout) - Cin1 = Input('Cin1') - Din1 = Input('Din1') + Cin1 = Input("Cin1") + Din1 = Input("Din1") - pC = Parameter('PC', tw=1, values=[[-1.0]]) - pD = Parameter('PD', sw=1, values=[[2.0]]) + pC = Parameter("PC", tw=1, values=[[-1.0]]) + pD = Parameter("PD", sw=1, values=[[2.0]]) - Cout = (Cin1.tw(1) * pC).delay(1, name='CoutD') # TODO Change convetion - Dout = (Din1.last() * pD).z(1, name='DoutD') + Cout = (Cin1.tw(1) * pC).delay(1, name="CoutD") # TODO Change convetion + Dout = (Din1.last() * pD).z(1, name="DoutD") - modelC = Output('Cout',Cout) - modelD = Output('Dout',Dout) + modelC = Output("Cout", Cout) + modelD = Output("Dout", Dout) m = Modely(workspace=result_path, visualizer=None) - m.addModel('model1', [modelA, modelB]) - m.addModel('model2', [modelC, modelD]) - m.addConnect(Cout,Bin1) - m.addConnect(Aout,Din1) - m.addClosedLoop(Dout,Ain1) - m.addClosedLoop(Bout,Cin1) - - init_states = {'Ain1': [1,1], 'Bin1':[2,2], 'Cin1':[3,3], 'Din1': [4,4]} - init_states_diff = {'Ain1': [1,1], 'Bin1':[12,20], 'Cin1':[3,3], 'Din1': [12,20]} + m.addModel("model1", [modelA, modelB]) + m.addModel("model2", [modelC, modelD]) + m.addConnect(Cout, Bin1) + m.addConnect(Aout, Din1) + m.addClosedLoop(Dout, Ain1) + m.addClosedLoop(Bout, Cin1) + + init_states = {"Ain1": [1, 1], "Bin1": [2, 2], "Cin1": [3, 3], "Din1": [4, 4]} + init_states_diff = { + "Ain1": [1, 1], + "Bin1": [12, 20], + "Cin1": [3, 3], + "Din1": [12, 20], + } # Target with states = 0.0 - results_target = {'Dout': [0.0, 1.5 * 2.0], - 'Cout': [0.0, 3.0 * -1.0], - 'Bout': [0.0, (-3.0 * -5.0)*2.0], - 'Aout': [(1.0 * 3.0)*0.5, (1.0 * 3.0)+(1.0 * 3.0)*0.5]} + results_target = { + "Dout": [0.0, 1.5 * 2.0], + "Cout": [0.0, 3.0 * -1.0], + "Bout": [0.0, (-3.0 * -5.0) * 2.0], + "Aout": [(1.0 * 3.0) * 0.5, (1.0 * 3.0) + (1.0 * 3.0) * 0.5], + } m.neuralizeModel() self.assertEqual(results_target, m(init_states)) @@ -951,14 +1318,14 @@ def test_partial_model_export_intern_extern_connection(self): # Test loading of all models m.saveTorchModel() l = Modely(workspace=result_path, visualizer=None) - l.addModel('model1', [modelA, modelB]) - l.addModel('model2', [modelC, modelD]) - l.addConnect(Cout,Bin1) - l.addConnect(Aout,Din1) - l.addClosedLoop(Dout,Ain1) - l.addClosedLoop(Bout,Cin1) + l.addModel("model1", [modelA, modelB]) + l.addModel("model2", [modelC, modelD]) + l.addConnect(Cout, Bin1) + l.addConnect(Aout, Din1) + l.addClosedLoop(Dout, Ain1) + l.addClosedLoop(Bout, Cin1) l.neuralizeModel() - l.parameters['PA'] = [[22.0]] + l.parameters["PA"] = [[22.0]] l.loadTorchModel() self.assertEqual(results_target, l(init_states)) l.resetStates() @@ -980,47 +1347,53 @@ def test_partial_model_export_intern_extern_connection(self): self.assertEqual(results_target, l(init_states_diff)) with self.assertRaises(TypeError): - m.exportONNX(outputs_order=['Dout', 'Cout', 'Bout', 'Aout']) + m.exportONNX(outputs_order=["Dout", "Cout", "Bout", "Aout"]) - results_target_m1 = {'Aout': [1.5, 4.5], 'Bout': [-20.0, 20.0]} + results_target_m1 = {"Aout": [1.5, 4.5], "Bout": [-20.0, 20.0]} # Test loading of Model1 - m.saveTorchModel(models='model1') + m.saveTorchModel(models="model1") l = Modely(workspace=result_path, visualizer=None) - l.addModel('model1', [modelA, modelB]) + l.addModel("model1", [modelA, modelB]) l.neuralizeModel() - l.parameters['PA'] = [[22.0]] - l.loadTorchModel(name='net_model1') + l.parameters["PA"] = [[22.0]] + l.loadTorchModel(name="net_model1") self.assertEqual(results_target_m1, l(init_states)) l.resetStates() self.assertEqual(results_target_m1, l(init_states)) - m.saveModel(models='model1') + m.saveModel(models="model1") l = Modely(workspace=result_path, visualizer=None) - l.loadModel(name='net_model1') + l.loadModel(name="net_model1") l.neuralizeModel() self.assertEqual(results_target_m1, l(init_states)) l.resetStates() self.assertEqual(results_target_m1, l(init_states)) - m.exportPythonModel(models='model1') + m.exportPythonModel(models="model1") l = Modely(workspace=result_path, visualizer=None) - l.importPythonModel(name='net_model1') + l.importPythonModel(name="net_model1") self.assertEqual(results_target_m1, l(init_states)) l.resetStates() self.assertEqual(results_target_m1, l(init_states)) - m.exportONNX(outputs_order=['Bout', 'Aout'],models='model1') - - init_inputs = {'Ain1': np.array([[[[1.0]]],[[[1.0]]]]).astype(np.float32), - 'Bin1': np.array([[[[2.0]]],[[[2.0]]]]).astype(np.float32)} - init_states = {'AoutD': np.array([[[0.0]]]).astype(np.float32), - 'int_AoutD': np.array([[[0.]]]).astype(np.float32), - 'der_AoutD': np.array([[[0.],[0.]]]).astype(np.float32), - 'BoutD': np.array([[[0.0]]]).astype(np.float32), - 'int_BoutD': np.array([[[0.],[0.]]]).astype(np.float32), - 'der_BoutD': np.array([[[0.]]]).astype(np.float32)} - results = Modely(workspace=result_path, visualizer=None).onnxInference(init_inputs|init_states, name = 'net_model1' ) + m.exportONNX(outputs_order=["Bout", "Aout"], models="model1") + + init_inputs = { + "Ain1": np.array([[[[1.0]]], [[[1.0]]]]).astype(np.float32), + "Bin1": np.array([[[[2.0]]], [[[2.0]]]]).astype(np.float32), + } + init_states = { + "AoutD": np.array([[[0.0]]]).astype(np.float32), + "int_AoutD": np.array([[[0.0]]]).astype(np.float32), + "der_AoutD": np.array([[[0.0], [0.0]]]).astype(np.float32), + "BoutD": np.array([[[0.0]]]).astype(np.float32), + "int_BoutD": np.array([[[0.0], [0.0]]]).astype(np.float32), + "der_BoutD": np.array([[[0.0]]]).astype(np.float32), + } + results = Modely(workspace=result_path, visualizer=None).onnxInference( + init_inputs | init_states, name="net_model1" + ) self.assertEqual([[[-20.0]]], results[0][0]) self.assertEqual([[[20.0]]], results[0][1]) self.assertEqual([[[1.5]]], results[1][0]) @@ -1031,32 +1404,45 @@ def test_partial_model_export_intern_extern_connection(self): def test_export_report_recurrent(self): NeuObj.clearNames() - result_path = 'results' + result_path = "results" test = Modely(visualizer=None, seed=42, workspace=result_path) - x = Input('x') - y = Input('y') - z = Input('z') - target = Input('target') - a = Parameter('a', dimensions=1, sw=1, values=[[1]]) - b = Parameter('b', dimensions=1, sw=1, values=[[1]]) - c = Parameter('c', dimensions=1, sw=1, values=[[1]]) + x = Input("x") + y = Input("y") + z = Input("z") + target = Input("target") + a = Parameter("a", dimensions=1, sw=1, values=[[1]]) + b = Parameter("b", dimensions=1, sw=1, values=[[1]]) + c = Parameter("c", dimensions=1, sw=1, values=[[1]]) fir_x = Fir(W=a)(x.last()) fir_y = Fir(W=b)(y.last()) fir_z = Fir(W=c)(z.last()) - data_x, data_y, data_z = np.random.rand(20), np.random.rand(20), np.random.rand(20) - dataset = {'x': data_x, 'y': data_y, 'z': data_z, 'target': 3 * data_x + 3 * data_y + 3 * data_z} + data_x, data_y, data_z = ( + np.random.rand(20), + np.random.rand(20), + np.random.rand(20), + ) + dataset = { + "x": data_x, + "y": data_y, + "z": data_z, + "target": 3 * data_x + 3 * data_y + 3 * data_z, + } fir_x.connect(y) sum_rel = fir_x + fir_y + fir_z sum_rel.closedLoop(z) - out = Output('out', sum_rel) - test.addModel('model', out) - test.addMinimize('error', target.last(), out) + out = Output("out", sum_rel) + test.addModel("model", out) + test.addMinimize("error", target.last(), out) test.neuralizeModel(0.5) - test.loadData(name='test_dataset', source=dataset) + test.loadData(name="test_dataset", source=dataset) ## Train - test.trainAndAnalyze(optimizer='SGD', training_params={'num_of_epochs': 2, 'lr': 0.0001, 'train_batch_size': 1}, - splits=[100, 0, 0], prediction_samples=10) # Train the traced model + test.trainAndAnalyze( + optimizer="SGD", + training_params={"num_of_epochs": 2, "lr": 0.0001, "train_batch_size": 1}, + splits=[100, 0, 0], + prediction_samples=10, + ) # Train the traced model test.exportReport() if os.path.exists(test.getWorkspace()): - shutil.rmtree(test.getWorkspace()) \ No newline at end of file + shutil.rmtree(test.getWorkspace()) diff --git a/tests/test_input_dimensions.py b/tests/test_input_dimensions.py index 687443ca..44a8d081 100644 --- a/tests/test_input_dimensions.py +++ b/tests/test_input_dimensions.py @@ -20,61 +20,61 @@ # And finally the dimensions for each relation # relation_samples -class ModelyNetworkBuildingTest(unittest.TestCase): +class ModelyNetworkBuildingTest(unittest.TestCase): def test_network_building_very_simple(self): NeuObj.clearNames() - input1 = Input('in1') + input1 = Input("in1") rel1 = Fir(input1.last()) - fun = Output('out', rel1) + fun = Output("out", rel1) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0,test.input_tw_backward['in1']) - #self.assertEqual(0,test.input_tw_forward['in1']) - self.assertEqual(1,test.json['Inputs']['in1']['ns'][0]) - self.assertEqual(0,test.json['Inputs']['in1']['ns'][1]) - self.assertEqual(1,test.json['Inputs']['in1']['ntot']) + # self.assertEqual(0,test.input_tw_backward['in1']) + # self.assertEqual(0,test.input_tw_forward['in1']) + self.assertEqual(1, test.json["Inputs"]["in1"]["ns"][0]) + self.assertEqual(0, test.json["Inputs"]["in1"]["ns"][1]) + self.assertEqual(1, test.json["Inputs"]["in1"]["ntot"]) - self.assertEqual(1,test.json['Info']['ns'][0]) - self.assertEqual(0,test.json['Info']['ns'][1]) - self.assertEqual(1,test.json['Info']['ntot']) # 5 samples + self.assertEqual(1, test.json["Info"]["ns"][0]) + self.assertEqual(0, test.json["Info"]["ns"][1]) + self.assertEqual(1, test.json["Info"]["ntot"]) # 5 samples def test_network_building_simple(self): NeuObj.clearNames() - input1 = Input('in1') + input1 = Input("in1") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input1.tw(0.01)) - fun = Output('out',rel1+rel2) + fun = Output("out", rel1 + rel2) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0.05,test.input_tw_backward['in1']) - #self.assertEqual(0,test.input_tw_forward['in1']) - self.assertEqual(5,test.json['Inputs']['in1']['ns'][0]) - self.assertEqual(0,test.json['Inputs']['in1']['ns'][1]) - self.assertEqual(5,test.json['Inputs']['in1']['ntot']) + # self.assertEqual(0.05,test.input_tw_backward['in1']) + # self.assertEqual(0,test.input_tw_forward['in1']) + self.assertEqual(5, test.json["Inputs"]["in1"]["ns"][0]) + self.assertEqual(0, test.json["Inputs"]["in1"]["ns"][1]) + self.assertEqual(5, test.json["Inputs"]["in1"]["ntot"]) - self.assertEqual(5,test.json['Info']['ns'][0]) - self.assertEqual(0,test.json['Info']['ns'][1]) - self.assertEqual(5,test.json['Info']['ntot']) # 5 samples + self.assertEqual(5, test.json["Info"]["ns"][0]) + self.assertEqual(0, test.json["Info"]["ns"][1]) + self.assertEqual(5, test.json["Info"]["ntot"]) # 5 samples def test_network_building_tw(self): NeuObj.clearNames() - input1 = Input('in1') - input2 = Input('in2') + input1 = Input("in1") + input2 = Input("in2") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input1.tw(0.01)) rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.02,0.02])) - fun = Output('out',rel1+rel2+rel3+rel4) + rel4 = Fir(input2.tw([-0.02, 0.02])) + fun = Output("out", rel1 + rel2 + rel3 + rel4) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) # self.assertEqual({'in1': 0.05, 'in2': 0.05}, test.input_tw_backward) @@ -82,109 +82,122 @@ def test_network_building_tw(self): # self.assertEqual({'in1': 5, 'in2': 5},test.input_ns_backward) # self.assertEqual({'in1': 0, 'in2': 2},test.input_ns_forward) # self.assertEqual({'in1': 5, 'in2': 7},test.input_n_samples) - self.assertEqual([5,0] ,test.json['Inputs']['in1']['ns']) - self.assertEqual([5,2],test.json['Inputs']['in2']['ns']) - self.assertEqual(5,test.json['Inputs']['in1']['ntot']) - self.assertEqual(7,test.json['Inputs']['in2']['ntot']) + self.assertEqual([5, 0], test.json["Inputs"]["in1"]["ns"]) + self.assertEqual([5, 2], test.json["Inputs"]["in2"]["ns"]) + self.assertEqual(5, test.json["Inputs"]["in1"]["ntot"]) + self.assertEqual(7, test.json["Inputs"]["in2"]["ntot"]) - self.assertEqual(5,test.json['Info']['ns'][0]) - self.assertEqual(2,test.json['Info']['ns'][1]) - self.assertEqual(7,test.json['Info']['ntot']) # 5 samples + 2 samples of the horizon + self.assertEqual(5, test.json["Info"]["ns"][0]) + self.assertEqual(2, test.json["Info"]["ns"][1]) + self.assertEqual( + 7, test.json["Info"]["ntot"] + ) # 5 samples + 2 samples of the horizon def test_network_building_tw2(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.02,0.02])) - rel5 = Fir(input2.tw([-0.03,0.03])) + rel4 = Fir(input2.tw([-0.02, 0.02])) + rel5 = Fir(input2.tw([-0.03, 0.03])) rel6 = Fir(input2.tw([-0.03, 0])) rel7 = Fir(input2.tw(0.03)) - fun = Output('out',rel3+rel4+rel5+rel6+rel7) + fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0.05,test.input_tw_backward['in2']) - #self.assertEqual(0.03,test.input_tw_forward['in2']) - self.assertEqual(5,test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(3,test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(8,test.json['Inputs']['in2']['ntot']) # 5 samples + 3 samples of the horizon + # self.assertEqual(0.05,test.input_tw_backward['in2']) + # self.assertEqual(0.03,test.input_tw_forward['in2']) + self.assertEqual(5, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(3, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual( + 8, test.json["Inputs"]["in2"]["ntot"] + ) # 5 samples + 3 samples of the horizon - self.assertEqual(5,test.json['Info']['ns'][0]) - self.assertEqual(3,test.json['Info']['ns'][1]) - self.assertEqual(8,test.json['Info']['ntot']) # 5 samples + self.assertEqual(5, test.json["Info"]["ns"][0]) + self.assertEqual(3, test.json["Info"]["ns"][1]) + self.assertEqual(8, test.json["Info"]["ntot"]) # 5 samples def test_network_building_tw3(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.01,0.03])) - rel5 = Fir(input2.tw([-0.04,0.01])) - fun = Output('out',rel3+rel4+rel5) + rel4 = Fir(input2.tw([-0.01, 0.03])) + rel5 = Fir(input2.tw([-0.04, 0.01])) + fun = Output("out", rel3 + rel4 + rel5) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0.05, test.input_tw_backward['in2']) - #self.assertEqual(0.03, test.input_tw_forward['in2']) - self.assertEqual(5, test.json['Inputs']['in2']['ns'][0],) - self.assertEqual(3, test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(8, test.json['Inputs']['in2']['ntot']) # 5 samples + 3 samples of the horizon - - self.assertEqual(5, test.json['Info']['ns'][0]) - self.assertEqual(3, test.json['Info']['ns'][1]) - self.assertEqual(8, test.json['Info']['ntot']) # 5 samples + # self.assertEqual(0.05, test.input_tw_backward['in2']) + # self.assertEqual(0.03, test.input_tw_forward['in2']) + self.assertEqual( + 5, + test.json["Inputs"]["in2"]["ns"][0], + ) + self.assertEqual(3, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual( + 8, test.json["Inputs"]["in2"]["ntot"] + ) # 5 samples + 3 samples of the horizon + + self.assertEqual(5, test.json["Info"]["ns"][0]) + self.assertEqual(3, test.json["Info"]["ns"][1]) + self.assertEqual(8, test.json["Info"]["ntot"]) # 5 samples def test_network_building_tw_with_offest(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.04,0.02])) + rel4 = Fir(input2.tw([-0.04, 0.02])) rel5 = Fir(input2.tw([-0.04, 0.02], offset=-0.04)) rel6 = Fir(input2.tw([-0.04, 0.02], offset=-0.01)) rel7 = Fir(input2.tw([-0.04, 0.02], offset=0.01)) - fun = Output('out',rel3+rel4+rel5+rel6+rel7) + fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0.05, test.input_tw_backward['in2']) - #self.assertEqual(0.02, test.input_tw_forward['in2']) - self.assertEqual(5, test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(2, test.json['Inputs']['in2']['ns'][1] ) - self.assertEqual(7, test.json['Inputs']['in2']['ntot']) # 5 samples + 2 samples of the horizon + # self.assertEqual(0.05, test.input_tw_backward['in2']) + # self.assertEqual(0.02, test.input_tw_forward['in2']) + self.assertEqual(5, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(2, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual( + 7, test.json["Inputs"]["in2"]["ntot"] + ) # 5 samples + 2 samples of the horizon - self.assertEqual(5, test.json['Info']['ns'][0]) - self.assertEqual(2, test.json['Info']['ns'][1]) - self.assertEqual(7,test.json['Info']['ntot']) # 5 samples + self.assertEqual(5, test.json["Info"]["ns"][0]) + self.assertEqual(2, test.json["Info"]["ns"][1]) + self.assertEqual(7, test.json["Info"]["ntot"]) # 5 samples def test_network_building_tw_negative(self): NeuObj.clearNames() - input2 = Input('in2') - rel1 = Fir(input2.tw([-0.05,-0.01])) - rel2 = Fir(input2.tw([-0.06,-0.03])) - fun = Output('out',rel1+rel2) + input2 = Input("in2") + rel1 = Fir(input2.tw([-0.05, -0.01])) + rel2 = Fir(input2.tw([-0.06, -0.03])) + fun = Output("out", rel1 + rel2) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0.06,test.input_tw_backward['in2']) - #self.assertEqual( -0.01, test.input_tw_forward['in2']) - self.assertEqual(6, test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(-1, test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(5, test.json['Inputs']['in2']['ntot']) # 6 samples - 1 samples of the horizon + # self.assertEqual(0.06,test.input_tw_backward['in2']) + # self.assertEqual( -0.01, test.input_tw_forward['in2']) + self.assertEqual(6, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual( + 5, test.json["Inputs"]["in2"]["ntot"] + ) # 6 samples - 1 samples of the horizon - self.assertEqual(6, test.json['Info']['ns'][0]) - self.assertEqual(-1, test.json['Info']['ns'][1]) - self.assertEqual(5, test.json['Info']['ntot']) # 5 samples + self.assertEqual(6, test.json["Info"]["ns"][0]) + self.assertEqual(-1, test.json["Info"]["ns"][1]) + self.assertEqual(5, test.json["Info"]["ntot"]) # 5 samples def test_network_building_tw_negative_with_offset(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel1 = Fir(input2.tw([-0.05, -0.01], offset=-0.05)) rel2 = Fir(input2.tw([-0.02, -0.01], offset=-0.02)) rel3 = Fir(input2.tw([-0.06, -0.03], offset=-0.06)) @@ -195,55 +208,59 @@ def test_network_building_tw_negative_with_offset(self): input2.tw([-0.06, -0.03], offset=-0.07) with self.assertRaises(IndexError): input2.tw([-0.06, -0.01], offset=-0.01) - fun = Output('out', rel1 + rel2 + rel3 + rel4) + fun = Output("out", rel1 + rel2 + rel3 + rel4) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0.06,test.input_tw_backward['in2']) - #self.assertEqual( -0.01, test.input_tw_forward['in2']) - self.assertEqual(6, test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(-1, test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(5, test.json['Inputs']['in2']['ntot']) # 6 samples - 1 samples of the horizon + # self.assertEqual(0.06,test.input_tw_backward['in2']) + # self.assertEqual( -0.01, test.input_tw_forward['in2']) + self.assertEqual(6, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual( + 5, test.json["Inputs"]["in2"]["ntot"] + ) # 6 samples - 1 samples of the horizon - self.assertEqual(6, test.json['Info']['ns'][0]) - self.assertEqual(-1, test.json['Info']['ns'][1]) - self.assertEqual(5, test.json['Info']['ntot']) # 5 samples + self.assertEqual(6, test.json["Info"]["ns"][0]) + self.assertEqual(-1, test.json["Info"]["ns"][1]) + self.assertEqual(5, test.json["Info"]["ntot"]) # 5 samples def test_network_building_tw_positive(self): NeuObj.clearNames() - input1 = Input('in1') - rel = Fir(input1.tw([0.03,0.04])) - fun = Output('out1', rel) + input1 = Input("in1") + rel = Fir(input1.tw([0.03, 0.04])) + fun = Output("out1", rel) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - input2 = Input('in2') - rel1 = Fir(input2.tw([0.01,0.04])) - rel2 = Fir(input2.tw([0.03,0.07])) - fun = Output('out2',rel1+rel2) + input2 = Input("in2") + rel1 = Fir(input2.tw([0.01, 0.04])) + rel2 = Fir(input2.tw([0.03, 0.07])) + fun = Output("out2", rel1 + rel2) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(-0.01, test.input_tw_backward['in2']) - #self.assertEqual(0.07, test.input_tw_forward['in2']) - self.assertEqual(-1, test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(7, test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(6, test.json['Inputs']['in2']['ntot']) # -1 samples + 6 samples of the horizon + # self.assertEqual(-0.01, test.input_tw_backward['in2']) + # self.assertEqual(0.07, test.input_tw_forward['in2']) + self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(7, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual( + 6, test.json["Inputs"]["in2"]["ntot"] + ) # -1 samples + 6 samples of the horizon - self.assertEqual(-1, test.json['Info']['ns'][0]) - self.assertEqual(7, test.json['Info']['ns'][1]) - self.assertEqual(6, test.json['Info']['ntot']) # 5 samples + self.assertEqual(-1, test.json["Info"]["ns"][0]) + self.assertEqual(7, test.json["Info"]["ns"][1]) + self.assertEqual(6, test.json["Info"]["ntot"]) # 5 samples def test_network_building_tw_positive_with_offset(self): NeuObj.clearNames() - input2 = Input('in2') - rel1 = Fir(input2.tw([0.01,0.04],offset=0.02)) - rel2 = Fir(input2.tw([0.03,0.07],offset=0.04)) + input2 = Input("in2") + rel1 = Fir(input2.tw([0.01, 0.04], offset=0.02)) + rel2 = Fir(input2.tw([0.03, 0.07], offset=0.04)) with self.assertRaises(ValueError): input2.tw([0.03, 0.02]) with self.assertRaises(IndexError): @@ -251,116 +268,120 @@ def test_network_building_tw_positive_with_offset(self): with self.assertRaises(IndexError): input2.tw([0.03, 0.07], offset=0) - fun = Output('out', rel1 + rel2) + fun = Output("out", rel1 + rel2) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(-0.01,test.input_tw_backward['in2']) - #self.assertEqual( 0.07, test.input_tw_forward['in2']) - self.assertEqual(-1, test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(7, test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(6, test.json['Inputs']['in2']['ntot']) # 6 samples - 1 samples of the horizon + # self.assertEqual(-0.01,test.input_tw_backward['in2']) + # self.assertEqual( 0.07, test.input_tw_forward['in2']) + self.assertEqual(-1, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(7, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual( + 6, test.json["Inputs"]["in2"]["ntot"] + ) # 6 samples - 1 samples of the horizon - self.assertEqual(-1, test.json['Info']['ns'][0]) - self.assertEqual(7, test.json['Info']['ns'][1]) - self.assertEqual(6, test.json['Info']['ntot']) # 5 samples + self.assertEqual(-1, test.json["Info"]["ns"][0]) + self.assertEqual(7, test.json["Info"]["ns"][1]) + self.assertEqual(6, test.json["Info"]["ntot"]) # 5 samples def test_network_building_sw(self): NeuObj.clearNames() - input1 = Input('in1') + input1 = Input("in1") rel3 = Fir(input1.sw(2)) - rel4 = Fir(input1.sw([-2,2])) - rel5 = Fir(input1.sw([-3,3])) + rel4 = Fir(input1.sw([-2, 2])) + rel5 = Fir(input1.sw([-3, 3])) rel6 = Fir(input1.sw([-3, 0])) rel7 = Fir(input1.sw(3)) - fun = Output('out',rel3+rel4+rel5+rel6+rel7) + fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0,test.input_tw_backward['in1']) - #self.assertEqual(0,test.input_tw_forward['in1']) - self.assertEqual(3,test.json['Inputs']['in1']['ns'][0]) - self.assertEqual(3,test.json['Inputs']['in1']['ns'][1]) - self.assertEqual(6,test.json['Inputs']['in1']['ntot']) # 6 samples - 1 samples of the horizon + # self.assertEqual(0,test.input_tw_backward['in1']) + # self.assertEqual(0,test.input_tw_forward['in1']) + self.assertEqual(3, test.json["Inputs"]["in1"]["ns"][0]) + self.assertEqual(3, test.json["Inputs"]["in1"]["ns"][1]) + self.assertEqual( + 6, test.json["Inputs"]["in1"]["ntot"] + ) # 6 samples - 1 samples of the horizon - self.assertEqual(3,test.json['Info']['ns'][0]) - self.assertEqual(3,test.json['Info']['ns'][1]) - self.assertEqual(6,test.json['Info']['ntot']) # 5 samples + self.assertEqual(3, test.json["Info"]["ns"][0]) + self.assertEqual(3, test.json["Info"]["ns"][1]) + self.assertEqual(6, test.json["Info"]["ntot"]) # 5 samples def test_network_building_sw_with_offset(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.sw(5)) - rel4 = Fir(input2.sw([-4,2])) + rel4 = Fir(input2.sw([-4, 2])) rel5 = Fir(input2.sw([-4, 2], offset=0)) rel6 = Fir(input2.sw([-4, 2], offset=1)) rel7 = Fir(input2.sw([-2, 2], offset=1)) rel8 = Fir(input2.sw([-4, 2], offset=-3)) - fun = Output('out',rel3+rel4+rel5+rel6+rel7+rel8) + fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7 + rel8) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0, test.input_tw_backward['in2']) - #self.assertEqual(0, test.input_tw_forward['in2']) - self.assertEqual(5, test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(2, test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(7, test.json['Inputs']['in2']['ntot']) + # self.assertEqual(0, test.input_tw_backward['in2']) + # self.assertEqual(0, test.input_tw_forward['in2']) + self.assertEqual(5, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(2, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual(7, test.json["Inputs"]["in2"]["ntot"]) - self.assertEqual(5, test.json['Info']['ns'][0]) - self.assertEqual(2, test.json['Info']['ns'][1]) - self.assertEqual(7, test.json['Info']['ntot']) + self.assertEqual(5, test.json["Info"]["ns"][0]) + self.assertEqual(2, test.json["Info"]["ns"][1]) + self.assertEqual(7, test.json["Info"]["ntot"]) def test_network_building_sw_and_tw(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") with self.assertRaises(TypeError): - input2.sw(5)+input2.tw(0.05) + input2.sw(5) + input2.tw(0.05) - rel1 = Fir(input2.sw([-4,2]))+Fir(input2.tw([-0.01,0])) - fun = Output('out',rel1) + rel1 = Fir(input2.sw([-4, 2])) + Fir(input2.tw([-0.01, 0])) + fun = Output("out", rel1) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - #self.assertEqual(0.01,test.input_tw_backward['in2']) - #self.assertEqual(0,test.input_tw_forward['in2']) - self.assertEqual(4,test.json['Inputs']['in2']['ns'][0]) - self.assertEqual(2,test.json['Inputs']['in2']['ns'][1]) - self.assertEqual(6,test.json['Inputs']['in2']['ntot']) + # self.assertEqual(0.01,test.input_tw_backward['in2']) + # self.assertEqual(0,test.input_tw_forward['in2']) + self.assertEqual(4, test.json["Inputs"]["in2"]["ns"][0]) + self.assertEqual(2, test.json["Inputs"]["in2"]["ns"][1]) + self.assertEqual(6, test.json["Inputs"]["in2"]["ntot"]) - self.assertEqual(4,test.json['Info']['ns'][0]) - self.assertEqual(2,test.json['Info']['ns'][1]) - self.assertEqual(6,test.json['Info']['ntot']) + self.assertEqual(4, test.json["Info"]["ns"][0]) + self.assertEqual(2, test.json["Info"]["ns"][1]) + self.assertEqual(6, test.json["Info"]["ntot"]) def test_example_parametric_different_dim_input(self): NeuObj.clearNames() test = Modely(visualizer=None, seed=42) - x = Input('x') - y = Input('y') - z = Input('z') + x = Input("x") + y = Input("y") + z = Input("z") ## create the relations def myFun(K1, p1, p2): return K1 * p1 * p2 - K_x = Parameter('k_x', dimensions=1, tw=1) - K_y = Parameter('k_y', dimensions=1, tw=1) - w = Parameter('w', dimensions=1, tw=1) - t = Parameter('t', dimensions=1, tw=1) - c_v = Constant('c_v', tw=1, values=[[1], [2]]) + K_x = Parameter("k_x", dimensions=1, tw=1) + K_y = Parameter("k_y", dimensions=1, tw=1) + w = Parameter("w", dimensions=1, tw=1) + t = Parameter("t", dimensions=1, tw=1) + c_v = Constant("c_v", tw=1, values=[[1], [2]]) c = 5 - w_5 = Parameter('w_5', dimensions=1, tw=5) - t_5 = Parameter('t_5', dimensions=1, tw=5) + w_5 = Parameter("w_5", dimensions=1, tw=5) + t_5 = Parameter("t_5", dimensions=1, tw=5) c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]] - c_5_2 = Constant('c_5_2', tw=5, values=c_5) - parfun_x = ParamFun(myFun, parameters_and_constants=[K_x,c_v]) + c_5_2 = Constant("c_5_2", tw=5, values=c_5) + parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v]) parfun_y = ParamFun(myFun, parameters_and_constants=[K_y]) parfun_z = ParamFun(myFun) fir_w = Fir(W=w_5)(x.tw(5)) @@ -373,34 +394,37 @@ def fuzzyfun(x): fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1)) - out = Output('out', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))) - out2 = Output('out2', Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) - out3 = Output('out3', Add(fir_w, fir_t)) - out4 = Output('out4', Linear(output_dimension=1)(fuzzy)) - out5 = Output('out5', Fir(time_part) + Fir(sample_select)) - out6 = Output('out6', LocalModel(output_function=Fir())(x.tw(1), fuzzy)) + out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))) + out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) + out3 = Output("out3", Add(fir_w, fir_t)) + out4 = Output("out4", Linear(output_dimension=1)(fuzzy)) + out5 = Output("out5", Fir(time_part) + Fir(sample_select)) + out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy)) with self.assertRaises(TypeError): parfun_z(x.tw(5), t_5, c_5) - out7 = Output('out7', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + Fir(parfun_z(x.tw(5), t_5, c_5_2))) + out7 = Output( + "out7", + Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + + Fir(parfun_z(x.tw(5), t_5, c_5_2)), + ) # parfun = ParamFun(myFun, map_over_batch=True) # p = Constant('co', values=[[2]]) # with self.assertRaises(TypeError): # Output('out-12', parfun(p, x.sw(4))) - - test.addModel('modelA', out) - test.addModel('modelB', [out2, out3, out4]) - test.addModel('modelC', [out4, out5, out6]) - test.addModel('modelD', [out7]) - test.addMinimize('error1', x.last(), out) - test.addMinimize('error2', y.last(), out3, loss_function='rmse') - test.addMinimize('error3', z.last(), out6, loss_function='rmse') + test.addModel("modelA", out) + test.addModel("modelB", [out2, out3, out4]) + test.addModel("modelC", [out4, out5, out6]) + test.addModel("modelD", [out7]) + test.addMinimize("error1", x.last(), out) + test.addMinimize("error2", y.last(), out3, loss_function="rmse") + test.addMinimize("error3", z.last(), out6, loss_function="rmse") test.neuralizeModel(0.5) - self.assertEqual([10,0],test.json['Inputs']['x']['ns']) - self.assertEqual([10,0],test.json['Inputs']['y']['ns']) - self.assertEqual([1,0],test.json['Inputs']['z']['ns']) + self.assertEqual([10, 0], test.json["Inputs"]["x"]["ns"]) + self.assertEqual([10, 0], test.json["Inputs"]["y"]["ns"]) + self.assertEqual([1, 0], test.json["Inputs"]["z"]["ns"]) # # self.assertEqual(4,test.json['Info']['ns'][0]) # self.assertEqual(2,test.json['Info']['ns'][1]) @@ -409,30 +433,51 @@ def fuzzyfun(x): def test_batch_size_and_step(self): NeuObj.clearNames() test = Modely(visualizer=None, seed=42, log_internal=True) - x = Input('x') - y = Input('y') + x = Input("x") + y = Input("y") rel_out = Fir(x.last()) + Fir(y.last()) rel_out.closedLoop(y) - out = Output('out', rel_out) + out = Output("out", rel_out) - test.addModel('modelA', out) - test.addMinimize('error1', out, x.next()) + test.addModel("modelA", out) + test.addMinimize("error1", out, x.next()) test.neuralizeModel() data_x = np.random.rand(101, 1) data_y = np.random.rand(101, 1) - dataset = {'x': data_x, 'y': data_y} - test.loadData(name='dataset', source=dataset) + dataset = {"x": data_x, "y": data_y} + test.loadData(name="dataset", source=dataset) ## 100 // (step+batch) = 2 - test.trainModel(train_dataset='dataset', num_of_epochs=1, train_batch_size=10, step=30, prediction_samples=20, shuffle_data=False) + test.trainModel( + train_dataset="dataset", + num_of_epochs=1, + train_batch_size=10, + step=30, + prediction_samples=20, + shuffle_data=False, + ) self.assertEqual(2 * 21, len(test.internals.keys())) ## Clip the step to the maximum number of samples (100 - prediction_samples - batch) = 70 - test.trainModel(train_dataset='dataset', num_of_epochs=1, train_batch_size=10, step=200, prediction_samples=20, shuffle_data=True) + test.trainModel( + train_dataset="dataset", + num_of_epochs=1, + train_batch_size=10, + step=200, + prediction_samples=20, + shuffle_data=True, + ) self.assertEqual(1 * 21, len(test.internals.keys())) - ## Clip the step to 0 - test.trainModel(train_dataset='dataset', num_of_epochs=1, train_batch_size=10, step=-4, prediction_samples=20, shuffle_data=True) + ## Clip the step to 0 + test.trainModel( + train_dataset="dataset", + num_of_epochs=1, + train_batch_size=10, + step=-4, + prediction_samples=20, + shuffle_data=True, + ) self.assertEqual(8 * 21, len(test.internals.keys())) diff --git a/tests/test_json.py b/tests/test_json.py index 689107ee..7e609ef0 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -17,685 +17,1004 @@ # the dimensions that are propagated through the relations # and the structure of the json itself -def myFun(K1,K2,p1,p2): + +def myFun(K1, K2, p1, p2): import torch - return p1*K1+p2*torch.sin(K2) -def myFun_out5(K1,p1): + return p1 * K1 + p2 * torch.sin(K2) + + +def myFun_out5(K1, p1): import torch - return torch.stack([K1,K1,K1,K1,K1],dim=2).squeeze(-1)*p1 + + return torch.stack([K1, K1, K1, K1, K1], dim=2).squeeze(-1) * p1 + def myFunPar(x, p1): import torch + if len(p1.shape) == 0: out = torch.tensor([[[1]]]).repeat((x.shape[0], 1, 1)) else: - out = torch.tensor([[p1.shape]]).repeat((x.shape[0],1,1)) + out = torch.tensor([[p1.shape]]).repeat((x.shape[0], 1, 1)) return out + NeuObj.count = 0 + class ModelyJsonTest(unittest.TestCase): def TestAlmostEqual(self, data1, data2, precision=4): - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) if type(data1) == type(data2) == list: - self.assertEqual(len(data1),len(data2)) + self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): self.TestAlmostEqual(pred, label, precision=precision) else: self.assertAlmostEqual(data1, data2, places=precision) def test_input(self): - input = Input('in1') - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1}}, 'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {}},input.json) - - #Discrete input removed - #input = Input('in', values=[2,3,4]) - #self.assertEqual({'Inputs': {'in': {'dim': 1, 'discrete': [2,3,4], 'tw': [0,0], 'sw': [0, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {}},input.json) + input = Input("in1") + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in1": {"dim": 1}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": {}, + }, + input.json, + ) + + # Discrete input removed + # input = Input('in', values=[2,3,4]) + # self.assertEqual({'Inputs': {'in': {'dim': 1, 'discrete': [2,3,4], 'tw': [0,0], 'sw': [0, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {}},input.json) def test_aritmetic(self): Stream.resetCount() NeuObj.clearNames() - input = Input('in1') + input = Input("in1") inlast = input.last() - out = inlast+inlast - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'sw': [-1, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Add2': ['Add', ['SamplePart1', 'SamplePart1']], - 'SamplePart1': ['SamplePart', ['in1'], -1, [-1, 0]]}},out.json) + out = inlast + inlast + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in1": {"dim": 1, "sw": [-1, 0]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Add2": ["Add", ["SamplePart1", "SamplePart1"]], + "SamplePart1": ["SamplePart", ["in1"], -1, [-1, 0]], + }, + }, + out.json, + ) out = input.tw(1) + input.tw(1) - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'tw': [-1,0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Add7': ['Add', ['TimePart4', 'TimePart6']], - 'TimePart4': ['TimePart', ['in1'], -1, [-1, 0]], - 'TimePart6': ['TimePart', ['in1'], -1, [-1, 0]]}},out.json) + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in1": {"dim": 1, "tw": [-1, 0]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Add7": ["Add", ["TimePart4", "TimePart6"]], + "TimePart4": ["TimePart", ["in1"], -1, [-1, 0]], + "TimePart6": ["TimePart", ["in1"], -1, [-1, 0]], + }, + }, + out.json, + ) out = input.tw(1) * input.tw(1) - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'tw': [-1,0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Mul12': ['Mul', ['TimePart9', 'TimePart11']], - 'TimePart9': ['TimePart', ['in1'], -1, [-1, 0]], - 'TimePart11': ['TimePart', ['in1'], -1, [-1, 0]]}},out.json) + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in1": {"dim": 1, "tw": [-1, 0]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Mul12": ["Mul", ["TimePart9", "TimePart11"]], + "TimePart9": ["TimePart", ["in1"], -1, [-1, 0]], + "TimePart11": ["TimePart", ["in1"], -1, [-1, 0]], + }, + }, + out.json, + ) out = input.tw(1) - input.tw(1) - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in1': {'dim': 1, 'tw': [-1,0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Sub17': ['Sub', ['TimePart14', 'TimePart16']], - 'TimePart14': ['TimePart', ['in1'], -1, [-1, 0]], - 'TimePart16': ['TimePart', ['in1'], -1, [-1, 0]]}},out.json) - input = Input('in2', dimensions = 5) + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in1": {"dim": 1, "tw": [-1, 0]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Sub17": ["Sub", ["TimePart14", "TimePart16"]], + "TimePart14": ["TimePart", ["in1"], -1, [-1, 0]], + "TimePart16": ["TimePart", ["in1"], -1, [-1, 0]], + }, + }, + out.json, + ) + input = Input("in2", dimensions=5) inlast = input.last() out = inlast + inlast - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'sw': [-1, 0]}},'Functions' : {}, 'Parameters' : {}, 'Outputs': {}, 'Relations': {'Add20': ['Add', ['SamplePart19', 'SamplePart19']], - 'SamplePart19': ['SamplePart', ['in2'], -1, [-1, 0]]}},out.json) + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in2": {"dim": 5, "sw": [-1, 0]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Add20": ["Add", ["SamplePart19", "SamplePart19"]], + "SamplePart19": ["SamplePart", ["in2"], -1, [-1, 0]], + }, + }, + out.json, + ) out = input.tw(1) + input.tw(1) - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'tw': [-1, 0]}}, 'Functions': {}, 'Parameters': {},'Outputs': {}, 'Relations': {'Add25': ['Add', ['TimePart22', 'TimePart24']], - 'TimePart22': ['TimePart', ['in2'], -1, [-1, 0]], - 'TimePart24': ['TimePart', ['in2'], -1, [-1, 0]]}}, out.json) - out = input.tw([2,5]) + input.tw([3,6]) - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'tw': [2, 6]}}, 'Functions': {}, 'Parameters': {},'Outputs': {}, 'Relations': {'Add30': ['Add', ['TimePart27', 'TimePart29']], - 'TimePart27': ['TimePart', ['in2'], -1, [2, 5]], - 'TimePart29': ['TimePart', ['in2'], -1, [3, 6]]}}, out.json) - out = input.tw([-5,-2]) + input.tw([-6,-3]) - self.assertEqual({'Info':{},'Constants': {},'Inputs': {'in2': {'dim': 5, 'tw': [-6, -2]}}, 'Functions': {}, 'Parameters': {},'Outputs': {}, 'Relations': {'Add35': ['Add', ['TimePart32', 'TimePart34']], - 'TimePart32': ['TimePart', ['in2'], -1, [-5, -2]], - 'TimePart34': ['TimePart', ['in2'], -1, [-6, -3]]}}, out.json) + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in2": {"dim": 5, "tw": [-1, 0]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Add25": ["Add", ["TimePart22", "TimePart24"]], + "TimePart22": ["TimePart", ["in2"], -1, [-1, 0]], + "TimePart24": ["TimePart", ["in2"], -1, [-1, 0]], + }, + }, + out.json, + ) + out = input.tw([2, 5]) + input.tw([3, 6]) + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in2": {"dim": 5, "tw": [2, 6]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Add30": ["Add", ["TimePart27", "TimePart29"]], + "TimePart27": ["TimePart", ["in2"], -1, [2, 5]], + "TimePart29": ["TimePart", ["in2"], -1, [3, 6]], + }, + }, + out.json, + ) + out = input.tw([-5, -2]) + input.tw([-6, -3]) + self.assertEqual( + { + "Info": {}, + "Constants": {}, + "Inputs": {"in2": {"dim": 5, "tw": [-6, -2]}}, + "Functions": {}, + "Parameters": {}, + "Outputs": {}, + "Relations": { + "Add35": ["Add", ["TimePart32", "TimePart34"]], + "TimePart32": ["TimePart", ["in2"], -1, [-5, -2]], + "TimePart34": ["TimePart", ["in2"], -1, [-6, -3]], + }, + }, + out.json, + ) def test_scalar_input_dimensions(self): NeuObj.clearNames() - input = Input('in1').last() - out = input+input - self.assertEqual({'dim': 1,'sw': 1}, out.dim) + input = Input("in1").last() + out = input + input + self.assertEqual({"dim": 1, "sw": 1}, out.dim) out = Fir(input) - self.assertEqual({'dim': 1,'sw': 1}, out.dim) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) out = Fir(7)(input) - self.assertEqual({'dim': 7,'sw': 1}, out.dim) - out = Fuzzify(5, [-1,1])(input) - self.assertEqual({'dim': 5,'sw': 1}, out.dim) + self.assertEqual({"dim": 7, "sw": 1}, out.dim) + out = Fuzzify(5, [-1, 1])(input) + self.assertEqual({"dim": 5, "sw": 1}, out.dim) out = ParamFun(myFun)(input) - self.assertEqual({'dim': 1,'sw': 1}, out.dim) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) out = ParamFun(myFun_out5)(input) - self.assertEqual({'dim': 5, 'sw': 1}, out.dim) + self.assertEqual({"dim": 5, "sw": 1}, out.dim) with self.assertRaises(ValueError): out = Fir(Fir(7)(input)) # with self.assertRaises(IndexError): - out = Part(input,0,4) + out = Part(input, 0, 4) inpart = ParamFun(myFun_out5)(input) - out = Part(inpart,0,4) - self.assertEqual({'dim': 4, 'sw': 1}, out.dim) - out = Part(inpart,0,1) - self.assertEqual({'dim': 1, 'sw': 1}, out.dim) - out = Part(inpart,1,3) - self.assertEqual({'dim': 2, 'sw': 1}, out.dim) - out = Select(inpart,0) - self.assertEqual({'dim': 1, 'sw': 1}, out.dim) + out = Part(inpart, 0, 4) + self.assertEqual({"dim": 4, "sw": 1}, out.dim) + out = Part(inpart, 0, 1) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) + out = Part(inpart, 1, 3) + self.assertEqual({"dim": 2, "sw": 1}, out.dim) + out = Select(inpart, 0) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) with self.assertRaises(IndexError): - out = Select(inpart,5) + out = Select(inpart, 5) with self.assertRaises(IndexError): - out = Select(inpart,-1) + out = Select(inpart, -1) with self.assertRaises(KeyError): - out = TimePart(inpart,-1,0) + out = TimePart(inpart, -1, 0) def test_scalar_input_tw_dimensions(self): NeuObj.clearNames() - input = Input('in1') + input = Input("in1") out = input.tw(1) + input.tw(1) - self.assertEqual({'dim': 1, 'tw': 1}, out.dim) + self.assertEqual({"dim": 1, "tw": 1}, out.dim) out = Fir(input.tw(1)) - self.assertEqual({'dim': 1, 'sw': 1}, out.dim) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) out = Fir(5)(input.tw(1)) - self.assertEqual({'dim': 5, 'sw': 1}, out.dim) - out = Fuzzify(5, [0,5])(input.tw(2)) - self.assertEqual({'dim': 5, 'tw': 2}, out.dim) - out = Fuzzify(5,range=[-1,5])(input.tw(2)) - self.assertEqual({'dim': 5, 'tw': 2}, out.dim) - out = Fuzzify(centers=[-1,5])(input.tw(2)) - self.assertEqual({'dim': 2, 'tw': 2}, out.dim) + self.assertEqual({"dim": 5, "sw": 1}, out.dim) + out = Fuzzify(5, [0, 5])(input.tw(2)) + self.assertEqual({"dim": 5, "tw": 2}, out.dim) + out = Fuzzify(5, range=[-1, 5])(input.tw(2)) + self.assertEqual({"dim": 5, "tw": 2}, out.dim) + out = Fuzzify(centers=[-1, 5])(input.tw(2)) + self.assertEqual({"dim": 2, "tw": 2}, out.dim) out = ParamFun(myFun)(input.tw(1)) - self.assertEqual({'dim': 1, 'tw' : 1}, out.dim) + self.assertEqual({"dim": 1, "tw": 1}, out.dim) out = ParamFun(myFun_out5)(input.tw(2)) - self.assertEqual({'dim': 5, 'tw': 2}, out.dim) - out = ParamFun(myFun_out5)(input.tw(2),input.tw(1)) - self.assertEqual({'dim': 5, 'tw': 2}, out.dim) + self.assertEqual({"dim": 5, "tw": 2}, out.dim) + out = ParamFun(myFun_out5)(input.tw(2), input.tw(1)) + self.assertEqual({"dim": 5, "tw": 2}, out.dim) inpart = ParamFun(myFun_out5)(input.tw(2)) - out = Part(inpart,0,4) - self.assertEqual({'dim': 4,'tw': 2}, out.dim) - out = Part(inpart,0,1) - self.assertEqual({'dim': 1,'tw': 2}, out.dim) - out = Part(inpart,1,3) - self.assertEqual({'dim': 2,'tw': 2}, out.dim) - out = Select(inpart,0) - self.assertEqual({'dim': 1,'tw': 2}, out.dim) + out = Part(inpart, 0, 4) + self.assertEqual({"dim": 4, "tw": 2}, out.dim) + out = Part(inpart, 0, 1) + self.assertEqual({"dim": 1, "tw": 2}, out.dim) + out = Part(inpart, 1, 3) + self.assertEqual({"dim": 2, "tw": 2}, out.dim) + out = Select(inpart, 0) + self.assertEqual({"dim": 1, "tw": 2}, out.dim) with self.assertRaises(IndexError): - out = Select(inpart,5) + out = Select(inpart, 5) with self.assertRaises(IndexError): - out = Select(inpart,-1) - out = TimePart(inpart, 0,1) - self.assertEqual({'dim': 5, 'tw': 1}, out.dim) - #out = TimeSelect(inpart,0) - #self.assertEqual({'dim': 5}, out.dim) - #with self.assertRaises(ValueError): + out = Select(inpart, -1) + out = TimePart(inpart, 0, 1) + self.assertEqual({"dim": 5, "tw": 1}, out.dim) + # out = TimeSelect(inpart,0) + # self.assertEqual({'dim': 5}, out.dim) + # with self.assertRaises(ValueError): # out = TimeSelect(inpart,-3) - twinput = input.tw([-2,4]) + twinput = input.tw([-2, 4]) out = TimePart(twinput, 0, 1) - self.assertEqual({'dim': 1, 'tw': 1}, out.dim) + self.assertEqual({"dim": 1, "tw": 1}, out.dim) def test_scalar_input_tw2_dimensions(self): NeuObj.clearNames() - input = Input('in1') - out = input.tw([-1,1])+input.tw([-2,0]) - self.assertEqual({'dim': 1, 'tw': 2}, out.dim) - out = input.tw(1)+input.tw([-1,0]) - self.assertEqual({'dim': 1, 'tw': 1}, out.dim) + input = Input("in1") + out = input.tw([-1, 1]) + input.tw([-2, 0]) + self.assertEqual({"dim": 1, "tw": 2}, out.dim) + out = input.tw(1) + input.tw([-1, 0]) + self.assertEqual({"dim": 1, "tw": 1}, out.dim) out = Fir(input.tw(1) + input.tw([-1, 0])) - self.assertEqual({'dim': 1, 'sw': 1}, out.dim) - out = input.tw([-1,0])+input.tw([-4,-3])+input.tw(1) - self.assertEqual({'dim': 1,'tw': 1}, out.dim) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) + out = input.tw([-1, 0]) + input.tw([-4, -3]) + input.tw(1) + self.assertEqual({"dim": 1, "tw": 1}, out.dim) with self.assertRaises(ValueError): - out = input.tw([-2,0])-input.tw([-1,0]) + out = input.tw([-2, 0]) - input.tw([-1, 0]) with self.assertRaises(ValueError): - out = input.tw([-2,0])+input.tw([-1,0]) + out = input.tw([-2, 0]) + input.tw([-1, 0]) def test_scalar_input_sw_dimensions(self): NeuObj.clearNames() - input = Input('in1') - out = input.sw([-1,1])+input.sw([-2,0]) - self.assertEqual({'dim': 1, 'sw': 2}, out.dim) - out = input.sw(1)+input.sw([-1,0]) - self.assertEqual({'dim': 1, 'sw': 1}, out.dim) + input = Input("in1") + out = input.sw([-1, 1]) + input.sw([-2, 0]) + self.assertEqual({"dim": 1, "sw": 2}, out.dim) + out = input.sw(1) + input.sw([-1, 0]) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) out = Fir(input.sw(1) + input.sw([-1, 0])) - self.assertEqual({'dim': 1, 'sw': 1}, out.dim) - out = input.sw([-1,0])+input.sw([-4,-3])+input.sw(1) - self.assertEqual({'dim': 1,'sw': 1}, out.dim) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) + out = input.sw([-1, 0]) + input.sw([-4, -3]) + input.sw(1) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) with self.assertRaises(ValueError): - out = input.sw([-2,0])-input.sw([-1,0]) + out = input.sw([-2, 0]) - input.sw([-1, 0]) with self.assertRaises(ValueError): - out = input.sw([-2,0])+input.sw([-1,0]) + out = input.sw([-2, 0]) + input.sw([-1, 0]) with self.assertRaises(TypeError): out = input.sw(1) + input.tw([-1, 0]) with self.assertRaises(TypeError): out = input.sw(1.2) with self.assertRaises(TypeError): - out = input.sw([-1.2,0.05]) + out = input.sw([-1.2, 0.05]) def test_vector_input_dimensions(self): NeuObj.clearNames() - input = Input('in1', dimensions = 5) - self.assertEqual({'dim': 5}, input.dim) - self.assertEqual({'dim': 5, 'tw' : 2}, input.tw(2).dim) + input = Input("in1", dimensions=5) + self.assertEqual({"dim": 5}, input.dim) + self.assertEqual({"dim": 5, "tw": 2}, input.tw(2).dim) out = input.tw(1) + input.tw(1) - self.assertEqual({'dim': 5, 'tw': 1}, out.dim) + self.assertEqual({"dim": 5, "tw": 1}, out.dim) out = Relu(input.tw(1)) - self.assertEqual({'dim': 5, 'tw': 1}, out.dim) + self.assertEqual({"dim": 5, "tw": 1}, out.dim) with self.assertRaises(TypeError): Fir(7)(input) with self.assertRaises(TypeError): - Fuzzify(7,[1,7])(input) + Fuzzify(7, [1, 7])(input) out = ParamFun(myFun)(input.tw(1)) - self.assertEqual({'dim': 5, 'tw' : 1}, out.dim) + self.assertEqual({"dim": 5, "tw": 1}, out.dim) out = ParamFun(myFun)(input.tw(2)) - self.assertEqual({'dim': 5, 'tw': 2}, out.dim) - out = ParamFun(myFun)(input.tw(2),input.tw(1)) - self.assertEqual({'dim': 5, 'tw': 2}, out.dim) + self.assertEqual({"dim": 5, "tw": 2}, out.dim) + out = ParamFun(myFun)(input.tw(2), input.tw(1)) + self.assertEqual({"dim": 5, "tw": 2}, out.dim) def test_parameter_and_linear(self): NeuObj.clearNames() - input = Input('in1').last() - W15 = Parameter('W15', dimensions=(1, 5)) - b15 = Parameter('b15', dimensions=5) - input4 = Input('in4', dimensions=4).last() - W45 = Parameter('W45', dimensions=(4, 5)) - b45 = Parameter('b45', dimensions=5) + input = Input("in1").last() + W15 = Parameter("W15", dimensions=(1, 5)) + b15 = Parameter("b15", dimensions=5) + input4 = Input("in4", dimensions=4).last() + W45 = Parameter("W45", dimensions=(4, 5)) + b45 = Parameter("b45", dimensions=5) out = Linear(input) + Linear(input4) out3 = Linear(3)(input) + Linear(3)(input4) - outW = Linear(W = W15)(input) + Linear(W = W45)(input4) - outWb = Linear(W = W15,b = b15)(input) + Linear(W = W45, b = b45)(input4) - self.assertEqual({'dim': 1, 'sw': 1}, out.dim) - self.assertEqual({'dim': 3, 'sw': 1}, out3.dim) - self.assertEqual({'dim': 5, 'sw': 1}, outW.dim) - self.assertEqual({'dim': 5, 'sw': 1}, outWb.dim) + outW = Linear(W=W15)(input) + Linear(W=W45)(input4) + outWb = Linear(W=W15, b=b15)(input) + Linear(W=W45, b=b45)(input4) + self.assertEqual({"dim": 1, "sw": 1}, out.dim) + self.assertEqual({"dim": 3, "sw": 1}, out3.dim) + self.assertEqual({"dim": 5, "sw": 1}, outW.dim) + self.assertEqual({"dim": 5, "sw": 1}, outWb.dim) NeuObj.clearNames() - input2 = Input('in1').sw([-1,1]) - W15 = Parameter('W15', dimensions=(1, 5)) - b15 = Parameter('b15', dimensions=5) - input42 = Input('in4', dimensions=4).sw([-1,1]) - W45 = Parameter('W45', dimensions=(4, 5)) - b45 = Parameter('b45', dimensions=5) + input2 = Input("in1").sw([-1, 1]) + W15 = Parameter("W15", dimensions=(1, 5)) + b15 = Parameter("b15", dimensions=5) + input42 = Input("in4", dimensions=4).sw([-1, 1]) + W45 = Parameter("W45", dimensions=(4, 5)) + b45 = Parameter("b45", dimensions=5) out = Linear(input2) + Linear(input42) out3 = Linear(3)(input2) + Linear(3)(input42) - outW = Linear(W = W15)(input2) + Linear(W = W45)(input42) - outWb = Linear(W = W15,b = b15)(input2) + Linear(W = W45, b = b45)(input42) - self.assertEqual({'dim': 1, 'sw': 2}, out.dim) - self.assertEqual({'dim': 3, 'sw': 2}, out3.dim) - self.assertEqual({'dim': 5, 'sw': 2}, outW.dim) - self.assertEqual({'dim': 5, 'sw': 2}, outWb.dim) + outW = Linear(W=W15)(input2) + Linear(W=W45)(input42) + outWb = Linear(W=W15, b=b15)(input2) + Linear(W=W45, b=b45)(input42) + self.assertEqual({"dim": 1, "sw": 2}, out.dim) + self.assertEqual({"dim": 3, "sw": 2}, out3.dim) + self.assertEqual({"dim": 5, "sw": 2}, outW.dim) + self.assertEqual({"dim": 5, "sw": 2}, outWb.dim) with self.assertRaises(ValueError): Linear(input) + Linear(input42) with self.assertRaises(ValueError): Linear(3)(input2) + Linear(3)(input4) with self.assertRaises(ValueError): - Linear(W = W15)(input) + Linear(W = W45)(input42) + Linear(W=W15)(input) + Linear(W=W45)(input42) with self.assertRaises(ValueError): - Linear(W = W15,b = b15)(input2) + Linear(W = W45, b = b45)(input4) + Linear(W=W15, b=b15)(input2) + Linear(W=W45, b=b45)(input4) def test_input_paramfun_param_const(self): NeuObj.clearNames() - input2 = Input('in2') - def fun_test(x,y,z,k): - return x*y*z*k + input2 = Input("in2") + + def fun_test(x, y, z, k): + return x * y * z * k NeuObj.clearNames() out = ParamFun(fun_test)(input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0k': {'dim': 1},'FParamFun0y': {'dim': 1},'FParamFun0z': {'dim': 1}}, out.json['Parameters']) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual( + { + "FParamFun0k": {"dim": 1}, + "FParamFun0y": {"dim": 1}, + "FParamFun0z": {"dim": 1}, + }, + out.json["Parameters"], + ) NeuObj.clearNames() - out = ParamFun(fun_test)(input2.tw(0.01),input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0k': {'dim': 1}, 'FParamFun0z': {'dim': 1}}, out.json['Parameters']) + out = ParamFun(fun_test)(input2.tw(0.01), input2.tw(0.01)) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual( + {"FParamFun0k": {"dim": 1}, "FParamFun0z": {"dim": 1}}, + out.json["Parameters"], + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants=['t'])(input2.tw(0.01),input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0z': {'dim': 1}, 't': {'dim': 1}}, out.json['Parameters']) + out = ParamFun(fun_test, parameters_and_constants=["t"])( + input2.tw(0.01), input2.tw(0.01) + ) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual( + {"FParamFun0z": {"dim": 1}, "t": {"dim": 1}}, out.json["Parameters"] + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants=['t','r'])(input2.tw(0.01),input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'r': {'dim': 1}, 't': {'dim': 1}}, out.json['Parameters']) + out = ParamFun(fun_test, parameters_and_constants=["t", "r"])( + input2.tw(0.01), input2.tw(0.01) + ) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual({"r": {"dim": 1}, "t": {"dim": 1}}, out.json["Parameters"]) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants={'k':'t'})(input2.tw(0.01),input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0z': {'dim': 1}, 't': {'dim': 1}}, out.json['Parameters']) + out = ParamFun(fun_test, parameters_and_constants={"k": "t"})( + input2.tw(0.01), input2.tw(0.01) + ) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual( + {"FParamFun0z": {"dim": 1}, "t": {"dim": 1}}, out.json["Parameters"] + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants={'k':(1,2)})(input2.tw(0.01),input2.tw(0.01)) - self.assertEqual({'dim': 2, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0k': {'dim': [1,2]}, 'FParamFun0z': {'dim': 1}}, out.json['Parameters']) + out = ParamFun(fun_test, parameters_and_constants={"k": (1, 2)})( + input2.tw(0.01), input2.tw(0.01) + ) + self.assertEqual({"dim": 2, "tw": 0.01}, out.dim) + self.assertEqual( + {"FParamFun0k": {"dim": [1, 2]}, "FParamFun0z": {"dim": 1}}, + out.json["Parameters"], + ) with self.assertRaises(ValueError): - ParamFun(fun_test,parameters_and_constants={'k':(1,2),'y':'r'})(input2.tw(0.01),input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants={"k": (1, 2), "y": "r"})( + input2.tw(0.01), input2.tw(0.01) + ) with self.assertRaises(ValueError): - ParamFun(fun_test,parameters_and_constants=[1.0,(1,2),'gg'])(input2.tw(0.01),input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants=[1.0, (1, 2), "gg"])( + input2.tw(0.01), input2.tw(0.01) + ) with self.assertRaises(ValueError): - ParamFun(fun_test,parameters_and_constants=[(1,2),'pp','c',[[1.0]]])(input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants=[(1, 2), "pp", "c", [[1.0]]])( + input2.tw(0.01) + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants={'k':(1,2)})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01)) - self.assertEqual({'dim': 2, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0k': {'dim': [1,2]}}, out.json['Parameters']) + out = ParamFun(fun_test, parameters_and_constants={"k": (1, 2)})( + input2.tw(0.01), input2.tw(0.01), input2.tw(0.01) + ) + self.assertEqual({"dim": 2, "tw": 0.01}, out.dim) + self.assertEqual({"FParamFun0k": {"dim": [1, 2]}}, out.json["Parameters"]) with self.assertRaises(ValueError): - ParamFun(fun_test,parameters_and_constants={'z':(1,2)})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants={"z": (1, 2)})( + input2.tw(0.01), input2.tw(0.01), input2.tw(0.01) + ) with self.assertRaises(ValueError): - ParamFun(fun_test,parameters_and_constants={'z':'g'})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants={"z": "g"})( + input2.tw(0.01), input2.tw(0.01), input2.tw(0.01) + ) with self.assertRaises(ValueError): - ParamFun(fun_test,parameters_and_constants={'z':'o'})(input2.tw(0.01),input2.tw(0.01),input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants={"z": "o"})( + input2.tw(0.01), input2.tw(0.01), input2.tw(0.01) + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants=['pp','tt'])(input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0y': {'dim': 1}, 'pp': {'dim': 1},'tt': {'dim': 1}}, out.json['Parameters']) + out = ParamFun(fun_test, parameters_and_constants=["pp", "tt"])(input2.tw(0.01)) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual( + {"FParamFun0y": {"dim": 1}, "pp": {"dim": 1}, "tt": {"dim": 1}}, + out.json["Parameters"], + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants={'y':'pp','k':'el'})(input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'FParamFun0z': {'dim': 1}, 'pp': {'dim': 1},'el': {'dim': 1}}, out.json['Parameters']) + out = ParamFun(fun_test, parameters_and_constants={"y": "pp", "k": "el"})( + input2.tw(0.01) + ) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual( + {"FParamFun0z": {"dim": 1}, "pp": {"dim": 1}, "el": {"dim": 1}}, + out.json["Parameters"], + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants=['pp','oo',Constant('el',values=2.0)])(input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'oo': {'dim': 1}, 'pp': {'dim': 1}}, out.json['Parameters']) - self.assertEqual({'el': {'dim': 1,'values':[2.0]}}, out.json['Constants']) + out = ParamFun( + fun_test, parameters_and_constants=["pp", "oo", Constant("el", values=2.0)] + )(input2.tw(0.01)) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual({"oo": {"dim": 1}, "pp": {"dim": 1}}, out.json["Parameters"]) + self.assertEqual({"el": {"dim": 1, "values": [2.0]}}, out.json["Constants"]) with self.assertRaises(NameError): - ParamFun(fun_test,parameters_and_constants=['pp','oo','el'])(input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants=["pp", "oo", "el"])( + input2.tw(0.01) + ) with self.assertRaises(NameError): - ParamFun(fun_test,parameters_and_constants=['pp','oo','el'])(input2.tw(0.01)) + ParamFun(fun_test, parameters_and_constants=["pp", "oo", "el"])( + input2.tw(0.01) + ) NeuObj.clearNames() - out = ParamFun(fun_test,parameters_and_constants=['pp',Constant('oo',values=[[2.0]]),Constant('ll',sw=1,values=[[7.0]])])(input2.tw(0.01)) - self.assertEqual({'dim': 1, 'tw': 0.01}, out.dim) - self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters']) - self.assertEqual({'oo': {'dim': [1,1],'values':[[2.0]]}, 'll': {'dim': 1,'sw': 1,'values':[[7.0]]}}, out.json['Constants']) + out = ParamFun( + fun_test, + parameters_and_constants=[ + "pp", + Constant("oo", values=[[2.0]]), + Constant("ll", sw=1, values=[[7.0]]), + ], + )(input2.tw(0.01)) + self.assertEqual({"dim": 1, "tw": 0.01}, out.dim) + self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"]) + self.assertEqual( + { + "oo": {"dim": [1, 1], "values": [[2.0]]}, + "ll": {"dim": 1, "sw": 1, "values": [[7.0]]}, + }, + out.json["Constants"], + ) NeuObj.clearNames() - pp = Parameter('pp') - ll = Constant('ll', values=[[1,2,3],[1,2,3]]) - oo = Constant('oo', values=[1,2,3]) - out = ParamFun(fun_test,parameters_and_constants=[pp,ll,oo])(input2.tw(0.01)) - self.assertEqual({'dim': 3, 'sw': 2}, out.dim) - self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters']) - self.assertEqual({'oo': {'dim': 3, 'values': [1,2,3]}, 'll': {'dim': [2,3], 'values':[[1,2,3],[1,2,3]]}}, out.json['Constants']) - - out = ParamFun(fun_test,parameters_and_constants={'z':pp,'y':ll,'k':oo})(input2.tw(0.01)) - self.assertEqual({'dim': 3, 'sw': 2}, out.dim) - self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters']) - self.assertEqual(['ll', 'pp', 'oo'],out.json['Functions']['FParamFun4']['params_and_consts']) - self.assertEqual({'oo': {'dim': 3, 'values': [1,2,3]}, 'll': {'dim': [2,3], 'values':[[1,2,3],[1,2,3]]}}, out.json['Constants']) + pp = Parameter("pp") + ll = Constant("ll", values=[[1, 2, 3], [1, 2, 3]]) + oo = Constant("oo", values=[1, 2, 3]) + out = ParamFun(fun_test, parameters_and_constants=[pp, ll, oo])(input2.tw(0.01)) + self.assertEqual({"dim": 3, "sw": 2}, out.dim) + self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"]) + self.assertEqual( + { + "oo": {"dim": 3, "values": [1, 2, 3]}, + "ll": {"dim": [2, 3], "values": [[1, 2, 3], [1, 2, 3]]}, + }, + out.json["Constants"], + ) + + out = ParamFun(fun_test, parameters_and_constants={"z": pp, "y": ll, "k": oo})( + input2.tw(0.01) + ) + self.assertEqual({"dim": 3, "sw": 2}, out.dim) + self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"]) + self.assertEqual( + ["ll", "pp", "oo"], out.json["Functions"]["FParamFun4"]["params_and_consts"] + ) + self.assertEqual( + { + "oo": {"dim": 3, "values": [1, 2, 3]}, + "ll": {"dim": [2, 3], "values": [[1, 2, 3], [1, 2, 3]]}, + }, + out.json["Constants"], + ) NeuObj.clearNames() Stream.resetCount() - pp = Parameter('pp') - ll = Constant('ll', values=[1,2,3]) - oo = Constant('oo', tw=0.01, values=[[1]]) - out = ParamFun(fun_test)(input2.tw(0.01),ll,oo,pp) - self.assertEqual({'dim': 3, 'tw': 0.01}, out.dim) - self.assertEqual({'pp': {'dim': 1}}, out.json['Parameters']) - self.assertEqual({'oo': {'dim': 1, 'tw':0.01, 'values': [[1]]}, 'll': {'dim': 3, 'values': [1,2,3]}}, out.json['Constants']) - self.assertEqual(['TimePart1', 'll', 'oo', 'pp'], out.json['Relations']['ParamFun2'][1]) + pp = Parameter("pp") + ll = Constant("ll", values=[1, 2, 3]) + oo = Constant("oo", tw=0.01, values=[[1]]) + out = ParamFun(fun_test)(input2.tw(0.01), ll, oo, pp) + self.assertEqual({"dim": 3, "tw": 0.01}, out.dim) + self.assertEqual({"pp": {"dim": 1}}, out.json["Parameters"]) + self.assertEqual( + { + "oo": {"dim": 1, "tw": 0.01, "values": [[1]]}, + "ll": {"dim": 3, "values": [1, 2, 3]}, + }, + out.json["Constants"], + ) + self.assertEqual( + ["TimePart1", "ll", "oo", "pp"], out.json["Relations"]["ParamFun2"][1] + ) def test_check_multiple_streams_compatibility_paramfun(self): NeuObj.clearNames() log.setAllLevel(logging.WARNING) - x = Input('x') - F = Input('F') + x = Input("x") + F = Input("F") def myFun(p1, p2, k1, k2): import torch + return k1 * torch.sin(p1) + k2 * torch.cos(p2) - K1 = Parameter('k1', dimensions=1, sw=1, values=[[2.0]]) - K2 = Parameter('k2', dimensions=1, sw=1, values=[[3.0]]) - parfun = ParamFun(myFun, parameters_and_constants=[K1,K2]) + K1 = Parameter("k1", dimensions=1, sw=1, values=[[2.0]]) + K2 = Parameter("k2", dimensions=1, sw=1, values=[[3.0]]) + parfun = ParamFun(myFun, parameters_and_constants=[K1, K2]) rel1 = parfun(x.last(), F.last()) - rel2 = parfun(Tanh(F.sw(2)+F.sw([-2,-0])+F.sw([-3,-1])+F.sw([-4,-2])), Tanh(F.sw([0,2]))) - rel3 = parfun(Tanh(F.sw([-2,1]))) - rel4 = parfun(Tanh(F.sw([-2,1])), K1) + rel2 = parfun( + Tanh(F.sw(2) + F.sw([-2, -0]) + F.sw([-3, -1]) + F.sw([-4, -2])), + Tanh(F.sw([0, 2])), + ) + rel3 = parfun(Tanh(F.sw([-2, 1]))) + rel4 = parfun(Tanh(F.sw([-2, 1])), K1) rel5 = parfun(K1, Tanh(F.sw(1))) with self.assertRaises(TypeError): parfun(Fir(3)(parfun(x.tw(0.4), x.tw(0.4)))) - out1 = Output('out1', rel1) - out2 = Output('out2', rel2) - out3 = Output('out3', rel3) - out4 = Output('out4', rel4) - out5 = Output('out5', rel5) + out1 = Output("out1", rel1) + out2 = Output("out2", rel2) + out3 = Output("out3", rel3) + out4 = Output("out4", rel4) + out5 = Output("out5", rel5) # m = MPLVisualizer(5) # m.showFunctions(list(example.json['Functions'].keys()), xlim=[[-5, 5], [-1, 1]]) exampleA = Modely(visualizer=None, seed=2) with self.assertRaises(TypeError): - exampleA.addModel('model', [out1, out2, out3]) - exampleA.addModel('model_A', [out1, out2]) + exampleA.addModel("model", [out1, out2, out3]) + exampleA.addModel("model_A", [out1, out2]) with self.assertRaises(TypeError): - exampleA.addModel('model_B', [out3]) - exampleA.addModel('model_A2', [out1, out2, out4, out5]) + exampleA.addModel("model_B", [out3]) + exampleA.addModel("model_A2", [out1, out2, out4, out5]) exampleA.neuralizeModel(0.25) exampleB = Modely(visualizer=None, seed=2) - exampleB.addModel('model_B', [out3]) + exampleB.addModel("model_B", [out3]) exampleB.neuralizeModel(1) - resultsA = exampleA({'x': [1, 3, 3]}) - self.TestAlmostEqual([4.682941913604736, 3.2822399139404297, 3.2822399139404297], resultsA['out1']) - self.TestAlmostEqual([[3.0, 3.0],[3.0 , 3.0],[3.0 , 3.0]], resultsA['out2']) - self.TestAlmostEqual([[-1.2484405040740967, -1.2484405040740967, -1.2484405040740967], - [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967], - [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967]], resultsA['out4']) - self.TestAlmostEqual([4.818594932556152, 4.818594932556152, 4.818594932556152], resultsA['out5']) - - resultsB = exampleB({'F': [1, 3, 4]}) - self.TestAlmostEqual([[3.831000328063965, 4.128425598144531, 4.133065223693848]], resultsB['out3']) + resultsA = exampleA({"x": [1, 3, 3]}) + self.TestAlmostEqual( + [4.682941913604736, 3.2822399139404297, 3.2822399139404297], + resultsA["out1"], + ) + self.TestAlmostEqual([[3.0, 3.0], [3.0, 3.0], [3.0, 3.0]], resultsA["out2"]) + self.TestAlmostEqual( + [ + [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967], + [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967], + [-1.2484405040740967, -1.2484405040740967, -1.2484405040740967], + ], + resultsA["out4"], + ) + self.TestAlmostEqual( + [4.818594932556152, 4.818594932556152, 4.818594932556152], resultsA["out5"] + ) + + resultsB = exampleB({"F": [1, 3, 4]}) + self.TestAlmostEqual( + [[3.831000328063965, 4.128425598144531, 4.133065223693848]], + resultsB["out3"], + ) log.setAllLevel(logging.CRITICAL) def test_check_multiple_streams_compatibility_linear(self): NeuObj.clearNames() log.setAllLevel(logging.WARNING) - x = Input('x',dimensions=3) - f = Input('f') + x = Input("x", dimensions=3) + f = Input("f") lin = Linear() l1out = lin(x.last()) + Fir(lin(x.tw(2.0))) + Fir(lin(x.sw(3))) l2out = lin(f.last()) + Fir(lin(f.tw(2.0))) + Fir(lin(f.sw(3))) - out1 = Output('out1', l1out) - out2 = Output('out2', l2out) + out1 = Output("out1", l1out) + out2 = Output("out2", l2out) exampleA = Modely(visualizer=None, seed=2) with self.assertRaises(TypeError): - exampleA.addModel('model', [out1, out2]) - exampleA.addModel('model_A', [out1]) + exampleA.addModel("model", [out1, out2]) + exampleA.addModel("model_A", [out1]) with self.assertRaises(TypeError): - exampleA.addModel('model_B', [out2]) + exampleA.addModel("model_B", [out2]) exampleA.neuralizeModel(1) exampleB = Modely(visualizer=None, seed=2) - exampleB.addModel('model_B', [out2]) + exampleB.addModel("model_B", [out2]) exampleB.neuralizeModel(1) - resultsA = exampleA({'x': [[1, 3, 3], [1, 2, 1], [2, 3, 4]]}) - self.TestAlmostEqual([12.507442474365234], resultsA['out1']) + resultsA = exampleA({"x": [[1, 3, 3], [1, 2, 1], [2, 3, 4]]}) + self.TestAlmostEqual([12.507442474365234], resultsA["out1"]) - resultsB = exampleB({'f': [1, 3, 3, 1, 2, 1]}) - self.TestAlmostEqual([6.585615158081055, 4.480303764343262, 4.106618881225586, 3.18161678314209], resultsB['out2']) + resultsB = exampleB({"f": [1, 3, 3, 1, 2, 1]}) + self.TestAlmostEqual( + [6.585615158081055, 4.480303764343262, 4.106618881225586, 3.18161678314209], + resultsB["out2"], + ) log.setAllLevel(logging.CRITICAL) def test_check_multiple_streams_compatibility_fir(self): NeuObj.clearNames() log.setAllLevel(logging.WARNING) - x = Input('x') + x = Input("x") fir = Fir() with self.assertRaises(TypeError): fir(x.last()) + fir(x.tw(2.0)) + fir(x.sw(3)) - out1 = Output('out1', fir(x.last())) - out2 = Output('out2', fir(x.tw(2.0))) + out1 = Output("out1", fir(x.last())) + out2 = Output("out2", fir(x.tw(2.0))) exampleA = Modely(visualizer=None, seed=2) with self.assertRaises(TypeError): - exampleA.addModel('model', [out1, out2]) - exampleA.addModel('model_A', [out1]) + exampleA.addModel("model", [out1, out2]) + exampleA.addModel("model_A", [out1]) with self.assertRaises(TypeError): - exampleA.addModel('model_B', [out2]) + exampleA.addModel("model_B", [out2]) exampleA.neuralizeModel(1) exampleB = Modely(visualizer=None, seed=2) - exampleB.addModel('model_B', [out2]) + exampleB.addModel("model_B", [out2]) exampleB.neuralizeModel(1) - resultsA = exampleA({'x': [1, 3]}) - self.TestAlmostEqual([0.6146950721740723, 1.8440852165222168], resultsA['out1']) + resultsA = exampleA({"x": [1, 3]}) + self.TestAlmostEqual([0.6146950721740723, 1.8440852165222168], resultsA["out1"]) - resultsB = exampleB({'x': [1, 4, 5]}) - self.TestAlmostEqual([2.138746500015259, 4.363844871520996], resultsB['out2']) + resultsB = exampleB({"x": [1, 4, 5]}) + self.TestAlmostEqual([2.138746500015259, 4.363844871520996], resultsB["out2"]) log.setAllLevel(logging.CRITICAL) def test_constant_and_parameter(self): NeuObj.clearNames() - c1 = Constant('c1', values=5.0) # {'dim': 1} -> shape (1,) - c11 = Constant('c11', values=5.0) # {'dim': 1} -> shape (1,) - c2 = Constant('c2', values=[5.0, 2.0, 1.0]) # {'dim': 3} -> shape (3,) - c3 = Constant('c3', values=[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]]) # {'dim': (2,3)} -> shape (2,3) - c4 = Constant('c4', sw=2, values=[[2, 3, 4], [1, 2, 3]]) # {'sw':2, 'dim': 3} -> shape (2,3) - c5 = Constant('c5', tw=4, values=[[2, 3, 4], [1, 2, 3]]) # {'tw':4, 'dim': 3} -> shape (2,3) - c6 = Constant('c6', sw=2, values=[[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], [[5.0, 2.0, 1.0], [3.0, 4.0,5.0]]]) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3) - self.assertEqual({'dim': 1}, c1.dim) - self.assertEqual({'dim': 1}, c11.dim) - self.assertEqual({'dim': 3}, c2.dim) - self.assertEqual({'dim': [2,3]}, c3.dim) - self.assertEqual({'dim': 3, 'sw': 2}, c4.dim) - self.assertEqual({'dim': 3, 'tw': 4}, c5.dim) - self.assertEqual({'dim': [2,3], 'sw':2}, c6.dim) - - p1 = Parameter('p1', values=5.0) # {'dim': 1} -> shape (1,) - p11 = Parameter('p11', values=[5.0]) # {'dim': 1} -> shape (1,) - p111 = Parameter('p111', values=[[5.0]]) # {'dim': 1} -> shape (1,) - p2 = Parameter('p2', values=[5.0, 2.0, 1.0]) # {'dim': 3} - p22 = Parameter('p22', sw=1, values=[[2, 3, 4]]) # {'dim': 3, 'sw': 1} -> shape (1,3) - p3 = Parameter('p3', values=[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0],[3.0, 4.0, 5.0],[3.0, 4.0, 5.0],[3.0, 4.0, 5.0]]) # {'dim': [5,3]} - p4 = Parameter('p4', sw=2, values=[[2, 3, 4], [1, 2, 3]]) # {'sw':2, 'dim': 3} - p5 = Parameter('p5', tw=4, values=[[2, 3, 4], [1, 2, 3]]) # {'tw':4, 'dim': 3} - p6 = Parameter('p6', sw=2, values=[[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]]]) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3) - self.assertEqual({'dim': 1}, p1.dim) - self.assertEqual({'dim': 1}, p11.dim) - self.assertEqual({'dim': [1,1]}, p111.dim) - self.assertEqual({'dim': 3}, p2.dim) - self.assertEqual({'dim': 3, 'sw':1}, p22.dim) - self.assertEqual({'dim': [5,3]}, p3.dim) - self.assertEqual({'dim': 3, 'sw': 2}, p4.dim) - self.assertEqual({'dim': 3, 'tw': 4}, p5.dim) - self.assertEqual({'dim': [2,3], 'sw':2}, p6.dim) - - x = Input('x',dimensions=5) - out1 = Output('out1', ParamFun(myFunPar, parameters_and_constants=[p1])(x.last())) - out11 = Output('out11', ParamFun(myFunPar, parameters_and_constants=[p11])(x.last())) - out111 = Output('out111', ParamFun(myFunPar, parameters_and_constants=[p111])(x.last())) - out2 = Output('out2', ParamFun(myFunPar, parameters_and_constants=[p2])(x.last())) - out22 = Output('out22', ParamFun(myFunPar, parameters_and_constants=[p22])(x.last())) - out3 = Output('out3', ParamFun(myFunPar, parameters_and_constants=[p3])(x.last())) - out4 = Output('out4', ParamFun(myFunPar, parameters_and_constants=[p4])(x.last())) - out5 = Output('out5', ParamFun(myFunPar, parameters_and_constants=[p5])(x.last())) - out6 = Output('out6', ParamFun(myFunPar, parameters_and_constants=[p6])(x.last())) + c1 = Constant("c1", values=5.0) # {'dim': 1} -> shape (1,) + c11 = Constant("c11", values=5.0) # {'dim': 1} -> shape (1,) + c2 = Constant("c2", values=[5.0, 2.0, 1.0]) # {'dim': 3} -> shape (3,) + c3 = Constant( + "c3", values=[[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]] + ) # {'dim': (2,3)} -> shape (2,3) + c4 = Constant( + "c4", sw=2, values=[[2, 3, 4], [1, 2, 3]] + ) # {'sw':2, 'dim': 3} -> shape (2,3) + c5 = Constant( + "c5", tw=4, values=[[2, 3, 4], [1, 2, 3]] + ) # {'tw':4, 'dim': 3} -> shape (2,3) + c6 = Constant( + "c6", + sw=2, + values=[ + [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], + [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], + ], + ) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3) + self.assertEqual({"dim": 1}, c1.dim) + self.assertEqual({"dim": 1}, c11.dim) + self.assertEqual({"dim": 3}, c2.dim) + self.assertEqual({"dim": [2, 3]}, c3.dim) + self.assertEqual({"dim": 3, "sw": 2}, c4.dim) + self.assertEqual({"dim": 3, "tw": 4}, c5.dim) + self.assertEqual({"dim": [2, 3], "sw": 2}, c6.dim) + + p1 = Parameter("p1", values=5.0) # {'dim': 1} -> shape (1,) + p11 = Parameter("p11", values=[5.0]) # {'dim': 1} -> shape (1,) + p111 = Parameter("p111", values=[[5.0]]) # {'dim': 1} -> shape (1,) + p2 = Parameter("p2", values=[5.0, 2.0, 1.0]) # {'dim': 3} + p22 = Parameter( + "p22", sw=1, values=[[2, 3, 4]] + ) # {'dim': 3, 'sw': 1} -> shape (1,3) + p3 = Parameter( + "p3", + values=[ + [5.0, 2.0, 1.0], + [3.0, 4.0, 5.0], + [3.0, 4.0, 5.0], + [3.0, 4.0, 5.0], + [3.0, 4.0, 5.0], + ], + ) # {'dim': [5,3]} + p4 = Parameter("p4", sw=2, values=[[2, 3, 4], [1, 2, 3]]) # {'sw':2, 'dim': 3} + p5 = Parameter("p5", tw=4, values=[[2, 3, 4], [1, 2, 3]]) # {'tw':4, 'dim': 3} + p6 = Parameter( + "p6", + sw=2, + values=[ + [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], + [[5.0, 2.0, 1.0], [3.0, 4.0, 5.0]], + ], + ) # {'sw':2, 'dim': (2,3)} -> shape (2,2,3) + self.assertEqual({"dim": 1}, p1.dim) + self.assertEqual({"dim": 1}, p11.dim) + self.assertEqual({"dim": [1, 1]}, p111.dim) + self.assertEqual({"dim": 3}, p2.dim) + self.assertEqual({"dim": 3, "sw": 1}, p22.dim) + self.assertEqual({"dim": [5, 3]}, p3.dim) + self.assertEqual({"dim": 3, "sw": 2}, p4.dim) + self.assertEqual({"dim": 3, "tw": 4}, p5.dim) + self.assertEqual({"dim": [2, 3], "sw": 2}, p6.dim) + + x = Input("x", dimensions=5) + out1 = Output( + "out1", ParamFun(myFunPar, parameters_and_constants=[p1])(x.last()) + ) + out11 = Output( + "out11", ParamFun(myFunPar, parameters_and_constants=[p11])(x.last()) + ) + out111 = Output( + "out111", ParamFun(myFunPar, parameters_and_constants=[p111])(x.last()) + ) + out2 = Output( + "out2", ParamFun(myFunPar, parameters_and_constants=[p2])(x.last()) + ) + out22 = Output( + "out22", ParamFun(myFunPar, parameters_and_constants=[p22])(x.last()) + ) + out3 = Output( + "out3", ParamFun(myFunPar, parameters_and_constants=[p3])(x.last()) + ) + out4 = Output( + "out4", ParamFun(myFunPar, parameters_and_constants=[p4])(x.last()) + ) + out5 = Output( + "out5", ParamFun(myFunPar, parameters_and_constants=[p5])(x.last()) + ) + out6 = Output( + "out6", ParamFun(myFunPar, parameters_and_constants=[p6])(x.last()) + ) nn = Modely(visualizer=None) - nn.addModel('model', [out1,out11,out111,out2,out22,out3,out4,out5,out6]) + nn.addModel("model", [out1, out11, out111, out2, out22, out3, out4, out5, out6]) nn.neuralizeModel(2.0) - results = nn({'x':[[1,2,3,4,5]]}) - self.assertEqual(results['out1'][0], p1.dim['dim']) - self.assertEqual(results['out11'][0], p11.dim['dim']) - self.assertEqual(results['out111'][0][0], p111.dim['dim']) - self.assertEqual(results['out2'][0], p2.dim['dim']) - self.assertEqual(results['out22'][0][0][1], p22.dim['dim']) - self.assertEqual(results['out22'][0][0][0], p22.dim['sw']) - self.assertEqual(results['out3'][0][0], p3.dim['dim']) - self.assertEqual(results['out3'][0][0][1], p4.dim['dim']) - self.assertEqual(results['out4'][0][0][0], p4.dim['sw']) - self.assertEqual(results['out5'][0][0][1], p5.dim['dim']) - self.assertEqual(results['out5'][0][0][0], p5.dim['tw']/2.0) - self.assertEqual(results['out6'][0][0][1:3], p6.dim['dim']) - self.assertEqual(results['out6'][0][0][0], p6.dim['sw']) - - NeuObj.clearNames(['x','out1','out11','out2','out22','out3','out4','out5','out6']) - x = Input('x') + results = nn({"x": [[1, 2, 3, 4, 5]]}) + self.assertEqual(results["out1"][0], p1.dim["dim"]) + self.assertEqual(results["out11"][0], p11.dim["dim"]) + self.assertEqual(results["out111"][0][0], p111.dim["dim"]) + self.assertEqual(results["out2"][0], p2.dim["dim"]) + self.assertEqual(results["out22"][0][0][1], p22.dim["dim"]) + self.assertEqual(results["out22"][0][0][0], p22.dim["sw"]) + self.assertEqual(results["out3"][0][0], p3.dim["dim"]) + self.assertEqual(results["out3"][0][0][1], p4.dim["dim"]) + self.assertEqual(results["out4"][0][0][0], p4.dim["sw"]) + self.assertEqual(results["out5"][0][0][1], p5.dim["dim"]) + self.assertEqual(results["out5"][0][0][0], p5.dim["tw"] / 2.0) + self.assertEqual(results["out6"][0][0][1:3], p6.dim["dim"]) + self.assertEqual(results["out6"][0][0][0], p6.dim["sw"]) + + NeuObj.clearNames( + ["x", "out1", "out11", "out2", "out22", "out3", "out4", "out5", "out6"] + ) + x = Input("x") with self.assertRaises(TypeError): - Output('out1', Linear(W=p1, b=p1)(x.last())) + Output("out1", Linear(W=p1, b=p1)(x.last())) with self.assertRaises(TypeError): - Output('out1', Linear(W=p11, b=p11)(x.last())) - out1 = Output('out1', Linear(W=p111, b=p1)(x.last())) - out11 = Output('out11', Linear(W=p111, b=p11)(x.last())) + Output("out1", Linear(W=p11, b=p11)(x.last())) + out1 = Output("out1", Linear(W=p111, b=p1)(x.last())) + out11 = Output("out11", Linear(W=p111, b=p11)(x.last())) with self.assertRaises(TypeError): - Output('out111', Linear(W=p111, b=p111)(x.last())) + Output("out111", Linear(W=p111, b=p111)(x.last())) - x5 = Input('x5',dimensions=5) + x5 = Input("x5", dimensions=5) with self.assertRaises(TypeError): - Output('out2', Linear(W=p1, b=p1)(x5.last())) + Output("out2", Linear(W=p1, b=p1)(x5.last())) with self.assertRaises(TypeError): - Output('out2', Linear(output_dimension=3, W=p1, b=p1)(x5.last())) - out2 = Output('out2', Linear(W=p3, b=p2)(x5.last())) - out3 = Output('out3', Linear(output_dimension=3, W=p3, b=p2)(x5.last())) + Output("out2", Linear(output_dimension=3, W=p1, b=p1)(x5.last())) + out2 = Output("out2", Linear(W=p3, b=p2)(x5.last())) + out3 = Output("out3", Linear(output_dimension=3, W=p3, b=p2)(x5.last())) with self.assertRaises(TypeError): - Output('out3', Linear(output_dimension=3, W=p3, b=p22)(x5.last())) + Output("out3", Linear(output_dimension=3, W=p3, b=p22)(x5.last())) with self.assertRaises(TypeError): - Output('out6', Linear(W=p6)(x.sw(2))) + Output("out6", Linear(W=p6)(x.sw(2))) - x2 = Input('x2', dimensions=2) + x2 = Input("x2", dimensions=2) with self.assertRaises(TypeError): - Output('out4', Linear(output_dimension=3, W=p4, b=p2)(x2.last())) + Output("out4", Linear(output_dimension=3, W=p4, b=p2)(x2.last())) - out4 = Output('out4', Fir(W=p4, b=p2)(x.sw(2))) - out41 = Output('out41', Fir(output_dimension=3, W=p4, b=p2)(x.sw(2))) + out4 = Output("out4", Fir(W=p4, b=p2)(x.sw(2))) + out41 = Output("out41", Fir(output_dimension=3, W=p4, b=p2)(x.sw(2))) with self.assertRaises(TypeError): - Output('out41', Fir(output_dimension=3, W=p5, b=p2)(x.sw(2))) + Output("out41", Fir(output_dimension=3, W=p5, b=p2)(x.sw(2))) with self.assertRaises(ValueError): - Output('out41', Fir(output_dimension=3, W=p4, b=p2)(x.sw(4))) + Output("out41", Fir(output_dimension=3, W=p4, b=p2)(x.sw(4))) with self.assertRaises(TypeError): - Output('out41', Fir(output_dimension=3, W=p4, b=p4)(x.sw(2))) - out5 = Output('out5', Fir(output_dimension=3, W=p5, b=p2)(x.tw(4))) - out51 = Output('out51', Fir(W=p5, b=p2)(x.tw(4))) + Output("out41", Fir(output_dimension=3, W=p4, b=p4)(x.sw(2))) + out5 = Output("out5", Fir(output_dimension=3, W=p5, b=p2)(x.tw(4))) + out51 = Output("out51", Fir(W=p5, b=p2)(x.tw(4))) with self.assertRaises(ValueError): - Output('out6', Fir(output_dimension=3, W=p6)(x.sw(2))) + Output("out6", Fir(output_dimension=3, W=p6)(x.sw(2))) with self.assertRaises(TypeError): - Output('out6', Fir(W=p6)(x.sw(2))) + Output("out6", Fir(W=p6)(x.sw(2))) nn = Modely(visualizer=None) - nn.addModel('model', [out1, out11, out111, out2, out22, out3, out4, out41, out5, out51, out6]) + nn.addModel( + "model", + [out1, out11, out111, out2, out22, out3, out4, out41, out5, out51, out6], + ) nn.neuralizeModel(2.0) - results = nn({'x': [1, 2, 3, 4, 5, 6, 7, 8, 9], 'x5': [[1, 2, 3, 4, 5]]}) - self.assertEqual((1,),np.array(results['out1']).shape) - self.assertEqual((1,),np.array(results['out11']).shape) - self.assertEqual((1, 1, 3),np.array(results['out2']).shape) - self.assertEqual((1, 1, 3), np.array(results['out3']).shape) - self.assertEqual((1, 1, 3), np.array(results['out4']).shape) - self.assertEqual((1, 1, 3), np.array(results['out41']).shape) - self.assertEqual((1, 1, 3), np.array(results['out5']).shape) - self.assertEqual((1, 1, 3), np.array(results['out51']).shape) + results = nn({"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "x5": [[1, 2, 3, 4, 5]]}) + self.assertEqual((1,), np.array(results["out1"]).shape) + self.assertEqual((1,), np.array(results["out11"]).shape) + self.assertEqual((1, 1, 3), np.array(results["out2"]).shape) + self.assertEqual((1, 1, 3), np.array(results["out3"]).shape) + self.assertEqual((1, 1, 3), np.array(results["out4"]).shape) + self.assertEqual((1, 1, 3), np.array(results["out41"]).shape) + self.assertEqual((1, 1, 3), np.array(results["out5"]).shape) + self.assertEqual((1, 1, 3), np.array(results["out51"]).shape) def test_multi_model_json_and_subjson(self): Stream.resetCount() NeuObj.clearNames() - x = Input('x') - y = Input('y') + x = Input("x") + y = Input("y") + + c1 = Constant("c1", values=5.0) + c3 = Constant("c3", values=5.0) - c1 = Constant('c1', values=5.0) - c3 = Constant('c3', values=5.0) + rel2 = Linear(W=Parameter("W2", values=[[2.0]]), b=False)(y.last()) + rel4 = Linear(W=Parameter("W4", values=[[4.0]]), b=False)(y.last()) - rel2 = Linear(W=Parameter('W2', values=[[2.0]]), b=False)(y.last()) - rel4 = Linear(W=Parameter('W4', values=[[4.0]]), b=False)(y.last()) - def fun2(x, a): return x * a - + def fun4(x, b): return x + b - out1 = Output('out1', c1 + Linear(W=Parameter('W1', values=[[1.0]]), b=False)(x.last())) - out2 = Output('out2', rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last())) - out3 = Output('out3', c3 + Linear(W=Parameter('W3', values=[[3.0]]), b=False)(x.last())) - out4 = Output('out4', rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last())) + out1 = Output( + "out1", c1 + Linear(W=Parameter("W1", values=[[1.0]]), b=False)(x.last()) + ) + out2 = Output( + "out2", rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last()) + ) + out3 = Output( + "out3", c3 + Linear(W=Parameter("W3", values=[[3.0]]), b=False)(x.last()) + ) + out4 = Output( + "out4", rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last()) + ) nn = Modely(visualizer=None) - nn.addModel('model_A', [out1, out2]) - nn.addModel('model_B', [out3, out4]) - nn.addClosedLoop(rel2,y) - - subjson_A = subjson_from_model(nn.json, 'model_A') - subjson_B = subjson_from_model(nn.json, 'model_B') - self.assertEqual(subjson_A['Constants']['c1'],nn.json['Constants']['c1']) - self.assertEqual(subjson_B['Constants']['c3'],nn.json['Constants']['c3']) - self.assertEqual(subjson_A['Functions']['FParamFun11'], nn.json['Functions']['FParamFun11']) - self.assertEqual(subjson_B['Functions']['FParamFun16'], nn.json['Functions']['FParamFun16']) - self.assertEqual(subjson_A['Inputs']['x'], nn.json['Inputs']['x']) - self.assertEqual(subjson_B['Inputs']['x'], nn.json['Inputs']['x']) - self.assertEqual(subjson_A['Models'], 'model_A') - self.assertEqual(subjson_B['Models'], 'model_B') - self.assertEqual(sorted(list(subjson_A['Relations'].keys())), sorted(nn.json['Models']['model_A']['Relations'])) - self.assertEqual(sorted(list(subjson_B['Relations'].keys())), sorted(nn.json['Models']['model_B']['Relations'])) - self.assertEqual(sorted(list(subjson_A['Parameters'].keys())), sorted(['W2', 'W1'])) - self.assertEqual(sorted(list(subjson_B['Parameters'].keys())), sorted(['W3', 'W4'])) - self.assertEqual(sorted(list(subjson_A['Outputs'].keys())), sorted(['out1', 'out2'])) - self.assertEqual(sorted(list(subjson_B['Outputs'].keys())), sorted(['out3', 'out4'])) - yval = copy.deepcopy(nn.json['Inputs']['y']) - del yval['closedLoop'] - del yval['local'] - self.assertEqual(subjson_A['Inputs']['y'], nn.json['Inputs']['y']) - self.assertEqual(subjson_B['Inputs']['y'], yval) + nn.addModel("model_A", [out1, out2]) + nn.addModel("model_B", [out3, out4]) + nn.addClosedLoop(rel2, y) + + subjson_A = subjson_from_model(nn.json, "model_A") + subjson_B = subjson_from_model(nn.json, "model_B") + self.assertEqual(subjson_A["Constants"]["c1"], nn.json["Constants"]["c1"]) + self.assertEqual(subjson_B["Constants"]["c3"], nn.json["Constants"]["c3"]) + self.assertEqual( + subjson_A["Functions"]["FParamFun11"], nn.json["Functions"]["FParamFun11"] + ) + self.assertEqual( + subjson_B["Functions"]["FParamFun16"], nn.json["Functions"]["FParamFun16"] + ) + self.assertEqual(subjson_A["Inputs"]["x"], nn.json["Inputs"]["x"]) + self.assertEqual(subjson_B["Inputs"]["x"], nn.json["Inputs"]["x"]) + self.assertEqual(subjson_A["Models"], "model_A") + self.assertEqual(subjson_B["Models"], "model_B") + self.assertEqual( + sorted(list(subjson_A["Relations"].keys())), + sorted(nn.json["Models"]["model_A"]["Relations"]), + ) + self.assertEqual( + sorted(list(subjson_B["Relations"].keys())), + sorted(nn.json["Models"]["model_B"]["Relations"]), + ) + self.assertEqual( + sorted(list(subjson_A["Parameters"].keys())), sorted(["W2", "W1"]) + ) + self.assertEqual( + sorted(list(subjson_B["Parameters"].keys())), sorted(["W3", "W4"]) + ) + self.assertEqual( + sorted(list(subjson_A["Outputs"].keys())), sorted(["out1", "out2"]) + ) + self.assertEqual( + sorted(list(subjson_B["Outputs"].keys())), sorted(["out3", "out4"]) + ) + yval = copy.deepcopy(nn.json["Inputs"]["y"]) + del yval["closedLoop"] + del yval["local"] + self.assertEqual(subjson_A["Inputs"]["y"], nn.json["Inputs"]["y"]) + self.assertEqual(subjson_B["Inputs"]["y"], yval) aa = Modely(visualizer=None) - aa.addModel('model_A', [out1, out2]) + aa.addModel("model_A", [out1, out2]) aa.addClosedLoop(rel2, y) self.assertEqual(subjson_A, aa.json) bb = Modely(visualizer=None) - bb.addModel('model_B', [out3, out4]) + bb.addModel("model_B", [out3, out4]) self.assertEqual(subjson_B, bb.json) def test_add_remove_models(self): Stream.resetCount() NeuObj.clearNames() - x = Input('x') - y = Input('y') + x = Input("x") + y = Input("y") - c1 = Constant('c1', values=5.0) - c3 = Constant('c3', values=5.0) + c1 = Constant("c1", values=5.0) + c3 = Constant("c3", values=5.0) - rel2 = Linear(W=Parameter('W2', values=[[2.0]]), b=False)(y.last()) - rel4 = Linear(W=Parameter('W4', values=[[4.0]]), b=False)(y.last()) + rel2 = Linear(W=Parameter("W2", values=[[2.0]]), b=False)(y.last()) + rel4 = Linear(W=Parameter("W4", values=[[4.0]]), b=False)(y.last()) def fun2(x, a): return x * a @@ -703,39 +1022,47 @@ def fun2(x, a): def fun4(x, b): return x + b - out1 = Output('out1', c1 + Linear(W=Parameter('W1', values=[[1.0]]), b=False)(x.last())) - out2 = Output('out2', rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last())) - out3 = Output('out3', c3 + Linear(W=Parameter('W3', values=[[3.0]]), b=False)(x.last())) - out4 = Output('out4', rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last())) + out1 = Output( + "out1", c1 + Linear(W=Parameter("W1", values=[[1.0]]), b=False)(x.last()) + ) + out2 = Output( + "out2", rel2 + ParamFun(fun2, parameters_and_constants=[c1])(y.last()) + ) + out3 = Output( + "out3", c3 + Linear(W=Parameter("W3", values=[[3.0]]), b=False)(x.last()) + ) + out4 = Output( + "out4", rel4 + ParamFun(fun4, parameters_and_constants=[c3])(y.last()) + ) nn = Modely(visualizer=None) - nn.addModel('model_A', [out1, out2]) + nn.addModel("model_A", [out1, out2]) model_A_json_1 = nn.json - nn.addModel('model_B', [out3, out4]) - nn.removeModel('model_B') + nn.addModel("model_B", [out3, out4]) + nn.removeModel("model_B") model_A_json_2 = nn.json self.assertEqual(model_A_json_1, model_A_json_2) def test_add_remove_minimize(self): clearNames() - input1 = Input('in1').last() - input2 = Input('in2').last() - input3 = Input('in3').last() - output1 = Output('out1', input1) - output2 = Output('out2', input1) - output3 = Output('out3', input1) + input1 = Input("in1").last() + input2 = Input("in2").last() + input3 = Input("in3").last() + output1 = Output("out1", input1) + output2 = Output("out2", input1) + output3 = Output("out3", input1) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1, output2, output3]) - test.addMinimize('error1', input1, output1) + test.addModel("model", [output1, output2, output3]) + test.addMinimize("error1", input1, output1) test_json_1 = test.json - test.addMinimize('error2', input2, output2) + test.addMinimize("error2", input2, output2) test_json_2 = test.json - test.addMinimize('error3', input3, output3) + test.addMinimize("error3", input3, output3) test_json_3 = test.json - test.removeMinimize('error3') + test.removeMinimize("error3") self.assertEqual(test_json_2, test.json) - test.addMinimize('error3', input3, output3) + test.addMinimize("error3", input3, output3) self.assertEqual(test_json_3, test.json) - test.removeMinimize(['error3','error2']) - self.assertEqual(test_json_1, test.json) \ No newline at end of file + test.removeMinimize(["error3", "error2"]) + self.assertEqual(test_json_1, test.json) diff --git a/tests/test_losses.py b/tests/test_losses.py index c7860722..2217805c 100644 --- a/tests/test_losses.py +++ b/tests/test_losses.py @@ -15,11 +15,17 @@ # Test the looses comparison between closed loop and states # Test the looses comparison between connect and states -data_folder = os.path.join(os.path.dirname(__file__), '_data/') +data_folder = os.path.join(os.path.dirname(__file__), "_data/") + class ModelyTrainingTest(unittest.TestCase): def TestAlmostEqual(self, data1, data2, precision=4): - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) if type(data1) == type(data2) == list: self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): @@ -29,162 +35,452 @@ def TestAlmostEqual(self, data1, data2, precision=4): def test_losses_compare(self): NeuObj.clearNames() - input1 = Input('in1') - target1 = Input('out1') - target2 = Input('out2') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out', Fir(W=a)(input1.last())) + input1 = Input("in1") + target1 = Input("out1") + target2 = Input("out2") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out", Fir(W=a)(input1.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model', output1) - test.addMinimize('error1', target1.last(), output1) - test.addMinimize('error2', target2.last(), output1) + test.addModel("model", output1) + test.addMinimize("error1", target1.last(), output1) + test.addMinimize("error2", target2.last(), output1) test.neuralizeModel() - dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [5,5,5,5,5,5,5,5,5,5]} - test.loadData(name='dataset', source=dataset) - test.trainAndAnalyze(optimizer='SGD', num_of_epochs=5, lr=0.5, splits=[70,20,10]) - self.TestAlmostEqual( [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], test.prediction['dataset_train']['error1']['A']) - self.TestAlmostEqual([[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]] ,test.prediction['dataset_train']['error1']['B']) - self.TestAlmostEqual( [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]], test.prediction['dataset_train']['error2']['A']) - self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error1']['train']) - self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val']) - self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error2']['train']) - self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val']) - self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['total']['mean_error'], - (test._training['error1']['val'][-1] + test._training['error2']['val'][-1]) / 2.0) - self.TestAlmostEqual(test.performance['dataset_test']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_test']['error2']['mse'], test._training['error2']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_test']['total']['mean_error'], (test._training['error1']['val'][-1]+test._training['error2']['val'][-1])/2.0) + dataset = { + "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5], + } + test.loadData(name="dataset", source=dataset) + test.trainAndAnalyze( + optimizer="SGD", num_of_epochs=5, lr=0.5, splits=[70, 20, 10] + ) + self.TestAlmostEqual( + [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], + test.prediction["dataset_train"]["error1"]["A"], + ) + self.TestAlmostEqual( + [[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]], + test.prediction["dataset_train"]["error1"]["B"], + ) + self.TestAlmostEqual( + [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]], + test.prediction["dataset_train"]["error2"]["A"], + ) + self.TestAlmostEqual( + [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error1"]["train"] + ) + self.TestAlmostEqual( + [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"] + ) + self.TestAlmostEqual( + [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error2"]["train"] + ) + self.TestAlmostEqual( + [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"] + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["total"]["mean_error"], + (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1]) + / 2.0, + ) + self.TestAlmostEqual( + test.performance["dataset_test"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_test"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_test"]["total"]["mean_error"], + (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1]) + / 2.0, + ) test.neuralizeModel(clear_model=True) - test.trainAndAnalyze(optimizer='SGD', splits=[60,20,20], num_of_epochs=5, lr=0.5, train_batch_size=2) - self.TestAlmostEqual( [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], test.prediction['dataset_train']['error1']['A']) - self.TestAlmostEqual([[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]] ,test.prediction['dataset_train']['error1']['B']) - self.TestAlmostEqual( [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]], test.prediction['dataset_train']['error2']['A']) - self.TestAlmostEqual([6.0, 11.0, 6.0, 11.0, 6.0], test._training['error1']['train']) - self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val']) - self.TestAlmostEqual([11.0, 6.0, 11.0, 6.0, 11.0], test._training['error2']['train']) - self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val']) - self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_test']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_test']['error2']['mse'], test._training['error2']['val'][-1]) + test.trainAndAnalyze( + optimizer="SGD", + splits=[60, 20, 20], + num_of_epochs=5, + lr=0.5, + train_batch_size=2, + ) + self.TestAlmostEqual( + [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], + test.prediction["dataset_train"]["error1"]["A"], + ) + self.TestAlmostEqual( + [[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]], + test.prediction["dataset_train"]["error1"]["B"], + ) + self.TestAlmostEqual( + [[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]], + test.prediction["dataset_train"]["error2"]["A"], + ) + self.TestAlmostEqual( + [6.0, 11.0, 6.0, 11.0, 6.0], test._training["error1"]["train"] + ) + self.TestAlmostEqual( + [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"] + ) + self.TestAlmostEqual( + [11.0, 6.0, 11.0, 6.0, 11.0], test._training["error2"]["train"] + ) + self.TestAlmostEqual( + [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"] + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_test"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_test"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) def test_losses_compare_closed_loop_state(self): NeuObj.clearNames() - input1 = Input('in1') - target1 = Input('out1') - target2 = Input('out2') - a = Parameter('a', sw=1, values=[[1]]) + input1 = Input("in1") + target1 = Input("out1") + target2 = Input("out2") + a = Parameter("a", sw=1, values=[[1]]) relation = Fir(W=a)(input1.last()) relation.closedLoop(input1) - output1 = Output('out', relation) + output1 = Output("out", relation) - test = Modely(visualizer=None,seed=42) - test.addModel('model', output1) - test.addMinimize('error1', target1.last(), output1) - test.addMinimize('error2', target2.last(), output1) + test = Modely(visualizer=None, seed=42) + test.addModel("model", output1) + test.addMinimize("error1", target1.last(), output1) + test.addMinimize("error2", target2.last(), output1) test.neuralizeModel() - dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [5,5,5,5,5,5,5,5,5,5]} - test.loadData(name='dataset', source=dataset) - test.trainAndAnalyze(optimizer='SGD', num_of_epochs=5, lr=0.5, shuffle_data=False, splits=[70,20,10]) - self.TestAlmostEqual([[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A']) - self.TestAlmostEqual([[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]] ,test.prediction['dataset_train']['error1']['B']) - self.TestAlmostEqual([[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A']) - self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error1']['train']) - self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val']) - self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error2']['train']) - self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val']) - self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['total']['mean_error'], - (test._training['error1']['val'][-1] + test._training['error2']['val'][-1]) / 2.0) - self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['total']['mean_error'], (test._training['error1']['val'][-1]+test._training['error2']['val'][-1])/2.0) + dataset = { + "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5], + } + test.loadData(name="dataset", source=dataset) + test.trainAndAnalyze( + optimizer="SGD", + num_of_epochs=5, + lr=0.5, + shuffle_data=False, + splits=[70, 20, 10], + ) + self.TestAlmostEqual( + [[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]], + test.prediction["dataset_train"]["error1"]["A"], + ) + self.TestAlmostEqual( + [[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]], + test.prediction["dataset_train"]["error1"]["B"], + ) + self.TestAlmostEqual( + [[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]], + test.prediction["dataset_train"]["error2"]["A"], + ) + self.TestAlmostEqual( + [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error1"]["train"] + ) + self.TestAlmostEqual( + [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"] + ) + self.TestAlmostEqual( + [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error2"]["train"] + ) + self.TestAlmostEqual( + [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"] + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["total"]["mean_error"], + (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1]) + / 2.0, + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["total"]["mean_error"], + (test._training["error1"]["val"][-1] + test._training["error2"]["val"][-1]) + / 2.0, + ) test.neuralizeModel(clear_model=True) - test.trainAndAnalyze(optimizer='SGD', splits=[60,20,20], num_of_epochs=5, lr=0.5, train_batch_size=2) - self.TestAlmostEqual([[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A']) - self.TestAlmostEqual([[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]] ,test.prediction['dataset_train']['error1']['B']) - self.TestAlmostEqual([[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A']) - self.TestAlmostEqual([6.0, 11.0, 6.0, 11.0, 6.0], test._training['error1']['train']) - self.TestAlmostEqual([16.0, 1.0, 16.0, 1.0, 16.0], test._training['error1']['val']) - self.TestAlmostEqual([11.0, 6.0, 11.0, 6.0, 11.0], test._training['error2']['train']) - self.TestAlmostEqual([1.0, 16.0, 1.0, 16.0, 1.0], test._training['error2']['val']) - self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_test']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_test']['error2']['mse'], test._training['error2']['val'][-1]) + test.trainAndAnalyze( + optimizer="SGD", + splits=[60, 20, 20], + num_of_epochs=5, + lr=0.5, + train_batch_size=2, + ) + self.TestAlmostEqual( + [[[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]]], + test.prediction["dataset_train"]["error1"]["A"], + ) + self.TestAlmostEqual( + [[[[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]], [[6.0]]]], + test.prediction["dataset_train"]["error1"]["B"], + ) + self.TestAlmostEqual( + [[[[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]], [[5.0]]]], + test.prediction["dataset_train"]["error2"]["A"], + ) + self.TestAlmostEqual( + [6.0, 11.0, 6.0, 11.0, 6.0], test._training["error1"]["train"] + ) + self.TestAlmostEqual( + [16.0, 1.0, 16.0, 1.0, 16.0], test._training["error1"]["val"] + ) + self.TestAlmostEqual( + [11.0, 6.0, 11.0, 6.0, 11.0], test._training["error2"]["train"] + ) + self.TestAlmostEqual( + [1.0, 16.0, 1.0, 16.0, 1.0], test._training["error2"]["val"] + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_test"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_test"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) test.neuralizeModel(clear_model=True) with self.assertRaises(ValueError): - test.trainAndAnalyze(optimizer='SGD', splits=[60,20,20], num_of_epochs=5, lr=0.5, train_batch_size=2, prediction_samples=4) + test.trainAndAnalyze( + optimizer="SGD", + splits=[60, 20, 20], + num_of_epochs=5, + lr=0.5, + train_batch_size=2, + prediction_samples=4, + ) test.neuralizeModel(clear_model=True) - test.trainAndAnalyze(optimizer='SGD', splits=[50, 50, 0], num_of_epochs=5, lr=0.001, train_batch_size=2, prediction_samples=3) - - self.TestAlmostEqual([[[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A']) - self.TestAlmostEqual([[[[1.1285]], [[1.1285]]], [[[1.2735]], [[1.2735]]], [[[1.4371]], [[1.4371]]], [[[1.6217]], [[1.6217]]]] ,test.prediction['dataset_train']['error1']['B']) - self.TestAlmostEqual([[[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A']) - self.TestAlmostEqual([1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training['error1']['train']) - self.TestAlmostEqual([0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training['error1']['val']) - self.TestAlmostEqual([16.0, 15.4923, 14.9602, 14.4059, 13.8328], test._training['error2']['train']) - self.TestAlmostEqual([15.4923, 14.9602, 14.4059, 13.8328, 13.2457], test._training['error2']['val']) - self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1]) + test.trainAndAnalyze( + optimizer="SGD", + splits=[50, 50, 0], + num_of_epochs=5, + lr=0.001, + train_batch_size=2, + prediction_samples=3, + ) + + self.TestAlmostEqual( + [ + [[[2.0]], [[2.0]]], + [[[2.0]], [[2.0]]], + [[[2.0]], [[2.0]]], + [[[2.0]], [[2.0]]], + ], + test.prediction["dataset_train"]["error1"]["A"], + ) + self.TestAlmostEqual( + [ + [[[1.1285]], [[1.1285]]], + [[[1.2735]], [[1.2735]]], + [[[1.4371]], [[1.4371]]], + [[[1.6217]], [[1.6217]]], + ], + test.prediction["dataset_train"]["error1"]["B"], + ) + self.TestAlmostEqual( + [ + [[[5.0]], [[5.0]]], + [[[5.0]], [[5.0]]], + [[[5.0]], [[5.0]]], + [[[5.0]], [[5.0]]], + ], + test.prediction["dataset_train"]["error2"]["A"], + ) + self.TestAlmostEqual( + [1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training["error1"]["train"] + ) + self.TestAlmostEqual( + [0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training["error1"]["val"] + ) + self.TestAlmostEqual( + [16.0, 15.4923, 14.9602, 14.4059, 13.8328], + test._training["error2"]["train"], + ) + self.TestAlmostEqual( + [15.4923, 14.9602, 14.4059, 13.8328, 13.2457], + test._training["error2"]["val"], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) def test_losses_compare_closed_loop(self): NeuObj.clearNames() - input1 = Input('in1') - target1 = Input('out1') - target2 = Input('out2') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out', Fir(W=a)(input1.last())) - - test = Modely(visualizer=None,seed=42) - test.addModel('model', output1) - test.addMinimize('error1', target1.last(), output1) - test.addMinimize('error2', target2.last(), output1) + input1 = Input("in1") + target1 = Input("out1") + target2 = Input("out2") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out", Fir(W=a)(input1.last())) + + test = Modely(visualizer=None, seed=42) + test.addModel("model", output1) + test.addMinimize("error1", target1.last(), output1) + test.addMinimize("error2", target2.last(), output1) test.neuralizeModel() - dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [5,5,5,5,5,5,5,5,5,5]} - test.loadData(name='dataset', source=dataset) - test.trainAndAnalyze(optimizer='SGD', splits=[50, 50, 0], num_of_epochs=5, lr=0.001, train_batch_size=2, prediction_samples=3, closed_loop={'in1': 'out'}) + dataset = { + "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [5, 5, 5, 5, 5, 5, 5, 5, 5, 5], + } + test.loadData(name="dataset", source=dataset) + test.trainAndAnalyze( + optimizer="SGD", + splits=[50, 50, 0], + num_of_epochs=5, + lr=0.001, + train_batch_size=2, + prediction_samples=3, + closed_loop={"in1": "out"}, + ) - self.TestAlmostEqual([[[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]], [[[2.0]], [[2.0]]]], test.prediction['dataset_train']['error1']['A']) - self.TestAlmostEqual([[[[1.1285]], [[1.1285]]], [[[1.2735]], [[1.2735]]], [[[1.4371]], [[1.4371]]], [[[1.6217]], [[1.6217]]]] ,test.prediction['dataset_train']['error1']['B']) - self.TestAlmostEqual([[[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]], [[[5.0]], [[5.0]]]], test.prediction['dataset_train']['error2']['A']) - self.TestAlmostEqual([1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training['error1']['train']) - self.TestAlmostEqual([0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training['error1']['val']) - self.TestAlmostEqual([16.0, 15.4923, 14.9602, 14.4059, 13.8328], test._training['error2']['train']) - self.TestAlmostEqual([15.4923, 14.9602, 14.4059, 13.8328, 13.2457], test._training['error2']['val']) - self.TestAlmostEqual(test.performance['dataset_val']['error1']['mse'], test._training['error1']['val'][-1]) - self.TestAlmostEqual(test.performance['dataset_val']['error2']['mse'], test._training['error2']['val'][-1]) + self.TestAlmostEqual( + [ + [[[2.0]], [[2.0]]], + [[[2.0]], [[2.0]]], + [[[2.0]], [[2.0]]], + [[[2.0]], [[2.0]]], + ], + test.prediction["dataset_train"]["error1"]["A"], + ) + self.TestAlmostEqual( + [ + [[[1.1285]], [[1.1285]]], + [[[1.2735]], [[1.2735]]], + [[[1.4371]], [[1.4371]]], + [[[1.6217]], [[1.6217]]], + ], + test.prediction["dataset_train"]["error1"]["B"], + ) + self.TestAlmostEqual( + [ + [[[5.0]], [[5.0]]], + [[[5.0]], [[5.0]]], + [[[5.0]], [[5.0]]], + [[[5.0]], [[5.0]]], + ], + test.prediction["dataset_train"]["error2"]["A"], + ) + self.TestAlmostEqual( + [1.0, 0.8768, 0.75615, 0.64057, 0.533], test._training["error1"]["train"] + ) + self.TestAlmostEqual( + [0.8768, 0.75615, 0.64057, 0.533, 0.4368], test._training["error1"]["val"] + ) + self.TestAlmostEqual( + [16.0, 15.4923, 14.9602, 14.4059, 13.8328], + test._training["error2"]["train"], + ) + self.TestAlmostEqual( + [15.4923, 14.9602, 14.4059, 13.8328, 13.2457], + test._training["error2"]["val"], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error1"]["mse"], + test._training["error1"]["val"][-1], + ) + self.TestAlmostEqual( + test.performance["dataset_val"]["error2"]["mse"], + test._training["error2"]["val"][-1], + ) def test_categorical_crossentropy(self): NeuObj.clearNames() - input1 = Input('in1') - target1 = Input('out1') - target2 = Input('out2', dimensions=5) - - k = Parameter('k', values=[[0.1,0.1,0.1,0.1,0.6]]) + input1 = Input("in1") + target1 = Input("out1") + target2 = Input("out2", dimensions=5) + + k = Parameter("k", values=[[0.1, 0.1, 0.1, 0.1, 0.6]]) linear = Linear(output_dimension=5, W=k, b=False)(input1.last()) - output = Output('out', linear) + output = Output("out", linear) test = Modely(visualizer=None, seed=42, log_internal=True) - test.addModel('model', output) - test.addMinimize('error1', output, target1.last(), loss_function='cross_entropy') - test.addMinimize('error2', output, target2.last(), loss_function='cross_entropy') + test.addModel("model", output) + test.addMinimize( + "error1", output, target1.last(), loss_function="cross_entropy" + ) + test.addMinimize( + "error2", output, target2.last(), loss_function="cross_entropy" + ) test.neuralizeModel() - - dataset = {'in1': [1], 'out1': [4], 'out2':[[0.0,0.0,0.0,0.0,1.0]]} - test.loadData(name='dataset', source=dataset) - test.trainAndAnalyze(optimizer='SGD', train_dataset='dataset', train_batch_size=1, num_of_epochs=1, lr=0.0) + + dataset = {"in1": [1], "out1": [4], "out2": [[0.0, 0.0, 0.0, 0.0, 1.0]]} + test.loadData(name="dataset", source=dataset) + test.trainAndAnalyze( + optimizer="SGD", + train_dataset="dataset", + train_batch_size=1, + num_of_epochs=1, + lr=0.0, + ) loss = torch.nn.CrossEntropyLoss() - self.assertAlmostEqual(1.2314292192459106, loss(torch.tensor(test.prediction['dataset']['error1']['A']).squeeze(), torch.tensor(test.prediction['dataset']['error1']['B'], dtype=torch.long).squeeze()).item()) - self.assertAlmostEqual(1.2314292192459106, loss(torch.tensor(test.prediction['dataset']['error2']['A']).squeeze(), torch.tensor(test.prediction['dataset']['error2']['B'], dtype=torch.float32).squeeze()).item()) + self.assertAlmostEqual( + 1.2314292192459106, + loss( + torch.tensor(test.prediction["dataset"]["error1"]["A"]).squeeze(), + torch.tensor( + test.prediction["dataset"]["error1"]["B"], dtype=torch.long + ).squeeze(), + ).item(), + ) + self.assertAlmostEqual( + 1.2314292192459106, + loss( + torch.tensor(test.prediction["dataset"]["error2"]["A"]).squeeze(), + torch.tensor( + test.prediction["dataset"]["error2"]["B"], dtype=torch.float32 + ).squeeze(), + ).item(), + ) diff --git a/tests/test_model_predict.py b/tests/test_model_predict.py index 5ebf8f05..15906b02 100644 --- a/tests/test_model_predict.py +++ b/tests/test_model_predict.py @@ -17,25 +17,35 @@ # The second dimension indicates the output time dimension for each sample. # The third is the size of the signal + def myfun(x, P): - return x*P + return x * P + -def myfun2(a, b ,c): +def myfun2(a, b, c): import torch + return torch.sin(a + b) * c + def myfun3(a, b, p1, p2): import torch - at = torch.transpose(a[:, :, 0:2],1,2) + + at = torch.transpose(a[:, :, 0:2], 1, 2) bt = torch.transpose(b, 1, 2) - return torch.matmul(p1,at+bt)+p2.t() + return torch.matmul(p1, at + bt) + p2.t() + class ModelyPredictTest(unittest.TestCase): - def TestAlmostEqual(self, data1, data2, precision=4): - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) if type(data1) == type(data2) == list: - self.assertEqual(len(data1),len(data2)) + self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): self.TestAlmostEqual(pred, label, precision=precision) else: @@ -43,681 +53,942 @@ def TestAlmostEqual(self, data1, data2, precision=4): def test_single_in(self): NeuObj.clearNames() - in1 = Input('in1') - in2 = Input('in2') + in1 = Input("in1") + in2 = Input("in2") out_fun = Fir(in1.tw(0.1)) + Fir(in2.last()) - out = Output('out', out_fun) + out = Output("out", out_fun) test = Modely(visualizer=None, seed=1) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.01) - results = test({'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]],'in2': [[5]]}) - self.assertEqual(1, len(results['out'])) - self.TestAlmostEqual([33.74938201904297], results['out']) - results = test({'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]], 'in2': [[5], [7]]}) - self.assertEqual(2, len(results['out'])) - self.TestAlmostEqual([33.74938201904297, 40.309326171875], results['out']) - results = test({'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12]], 'in2': [[5], [7], [9]]}) - self.assertEqual(3, len(results['out'])) - self.TestAlmostEqual([33.74938201904297, 40.309326171875, 46.86927032470703], results['out']) + results = test( + {"in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]], "in2": [[5]]} + ) + self.assertEqual(1, len(results["out"])) + self.TestAlmostEqual([33.74938201904297], results["out"]) + results = test( + { + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]], + "in2": [[5], [7]], + } + ) + self.assertEqual(2, len(results["out"])) + self.TestAlmostEqual([33.74938201904297, 40.309326171875], results["out"]) + results = test( + { + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12]], + "in2": [[5], [7], [9]], + } + ) + self.assertEqual(3, len(results["out"])) + self.TestAlmostEqual( + [33.74938201904297, 40.309326171875, 46.86927032470703], results["out"] + ) def test_activation(self): NeuObj.clearNames() - in1 = Input('in1') + in1 = Input("in1") out_fun = ELU(in1.last()) + Relu(in1.last()) + Tanh(in1.last()) - out = Output('out', out_fun) + out = Output("out", out_fun) test = Modely(visualizer=None, seed=1) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel() - results = test({'in1': [-1, -0.5, 0, 0.2, 2, 10]}) - self.assertEqual(6, len(results['out'])) - self.TestAlmostEqual([-1.3937146663665771,-0.8555865287780762,0,0.5973753333091736,4.964027404785156,21.0], results['out']) + results = test({"in1": [-1, -0.5, 0, 0.2, 2, 10]}) + self.assertEqual(6, len(results["out"])) + self.TestAlmostEqual( + [ + -1.3937146663665771, + -0.8555865287780762, + 0, + 0.5973753333091736, + 4.964027404785156, + 21.0, + ], + results["out"], + ) def test_single_in_window(self): NeuObj.clearNames() # Here there is more sample for each time step but the dimensions of the input is 1 - in1 = Input('in1') + in1 = Input("in1") # Finestre nel tempo - out1 = Output('x.tw(1)', in1.tw(1)) - out2 = Output('x.tw([-1,0])', in1.tw([-1, 0])) - out3 = Output('x.tw([-3,0])', in1.tw([-3, 0])) - out4 = Output('x.tw([1,3])', in1.tw([1, 3])) - out5 = Output('x.tw([-1,3])', in1.tw([-1, 3])) - out6 = Output('x.tw([0,1])', in1.tw([0, 1])) - out7 = Output('x.tw([-3,-2])', in1.tw([-3, -2])) + out1 = Output("x.tw(1)", in1.tw(1)) + out2 = Output("x.tw([-1,0])", in1.tw([-1, 0])) + out3 = Output("x.tw([-3,0])", in1.tw([-3, 0])) + out4 = Output("x.tw([1,3])", in1.tw([1, 3])) + out5 = Output("x.tw([-1,3])", in1.tw([-1, 3])) + out6 = Output("x.tw([0,1])", in1.tw([0, 1])) + out7 = Output("x.tw([-3,-2])", in1.tw([-3, -2])) ## TODO: adjust the z function - # Finesatre nei samples - out8 = Output('x.z(-1)', in1.z(-1)) - out9 = Output('x.z(0)', in1.z(0)) - out10 = Output('x.z(2)', in1.z(2)) - out11 = Output('x.sw([-1,0])', in1.sw([-1, 0])) - out12 = Output('x.sw([1,2])', in1.sw([1, 2])) - out13 = Output('x.sw([-3,1])', in1.sw([-3, 1])) - out14 = Output('x.sw([-3,-2])', in1.sw([-3, -2])) - out15 = Output('x.sw([0,1])', in1.sw([0, 1])) + # Finesatre nei samples + out8 = Output("x.z(-1)", in1.z(-1)) + out9 = Output("x.z(0)", in1.z(0)) + out10 = Output("x.z(2)", in1.z(2)) + out11 = Output("x.sw([-1,0])", in1.sw([-1, 0])) + out12 = Output("x.sw([1,2])", in1.sw([1, 2])) + out13 = Output("x.sw([-3,1])", in1.sw([-3, 1])) + out14 = Output("x.sw([-3,-2])", in1.sw([-3, -2])) + out15 = Output("x.sw([0,1])", in1.sw([0, 1])) test = Modely(visualizer=None, seed=1) - #test.addModel('out',[out0,out1,out2,out3,out4,out5,out6,out7,out11,out12,out13,out14,out15]) - test.addModel('out',[out1, out2, out3, out4, out5, out6, out7, out8, out9, out10, out11, out12, out13, out14, out15]) + # test.addModel('out',[out0,out1,out2,out3,out4,out5,out6,out7,out11,out12,out13,out14,out15]) + test.addModel( + "out", + [ + out1, + out2, + out3, + out4, + out5, + out6, + out7, + out8, + out9, + out10, + out11, + out12, + out13, + out14, + out15, + ], + ) test.neuralizeModel(1) # Time -2,-1,0,1,2,3 # zero represent the last passed instant - results = test({'in1': [[-2],[-1],[0],[1],[7],[3]]}) + results = test({"in1": [[-2], [-1], [0], [1], [7], [3]]}) # Time window - self.assertEqual((1,), np.array(results['x.tw(1)']).shape) - self.TestAlmostEqual([0], results['x.tw(1)']) - self.assertEqual((1,), np.array(results['x.tw([-1,0])']).shape) - self.TestAlmostEqual([0], results['x.tw([-1,0])']) - self.assertEqual((1, 3), np.array(results['x.tw([-3,0])']).shape) - self.TestAlmostEqual([[-2, -1, 0]], results['x.tw([-3,0])']) - self.assertEqual((1, 2), np.array(results['x.tw([1,3])']).shape) - self.TestAlmostEqual([[7, 3]], results['x.tw([1,3])']) - self.assertEqual((1, 4), np.array(results['x.tw([-1,3])']).shape) - self.TestAlmostEqual([[0, 1, 7, 3]], results['x.tw([-1,3])']) - self.assertEqual((1,), np.array(results['x.tw([0,1])']).shape) - self.TestAlmostEqual([1], results['x.tw([0,1])']) - self.assertEqual((1,), np.array(results['x.tw([-3,-2])']).shape) - self.TestAlmostEqual([-2],results['x.tw([-3,-2])']) + self.assertEqual((1,), np.array(results["x.tw(1)"]).shape) + self.TestAlmostEqual([0], results["x.tw(1)"]) + self.assertEqual((1,), np.array(results["x.tw([-1,0])"]).shape) + self.TestAlmostEqual([0], results["x.tw([-1,0])"]) + self.assertEqual((1, 3), np.array(results["x.tw([-3,0])"]).shape) + self.TestAlmostEqual([[-2, -1, 0]], results["x.tw([-3,0])"]) + self.assertEqual((1, 2), np.array(results["x.tw([1,3])"]).shape) + self.TestAlmostEqual([[7, 3]], results["x.tw([1,3])"]) + self.assertEqual((1, 4), np.array(results["x.tw([-1,3])"]).shape) + self.TestAlmostEqual([[0, 1, 7, 3]], results["x.tw([-1,3])"]) + self.assertEqual((1,), np.array(results["x.tw([0,1])"]).shape) + self.TestAlmostEqual([1], results["x.tw([0,1])"]) + self.assertEqual((1,), np.array(results["x.tw([-3,-2])"]).shape) + self.TestAlmostEqual([-2], results["x.tw([-3,-2])"]) # Sample window - self.assertEqual((1,), np.array(results['x.z(-1)']).shape) - self.TestAlmostEqual([1], results['x.z(-1)']) - self.assertEqual((1,), np.array(results['x.z(0)']).shape) - self.TestAlmostEqual([0], results['x.z(0)']) - self.assertEqual((1,), np.array(results['x.z(2)']).shape) - self.TestAlmostEqual([-2], results['x.z(2)']) - self.assertEqual((1,), np.array(results['x.sw([-1,0])']).shape) - self.TestAlmostEqual([0], results['x.sw([-1,0])']) - self.assertEqual((1,), np.array(results['x.sw([1,2])']).shape) - self.TestAlmostEqual([7], results['x.sw([1,2])']) - self.assertEqual((1,4), np.array(results['x.sw([-3,1])']).shape) - self.TestAlmostEqual([[-2,-1,0,1]], results['x.sw([-3,1])']) - self.assertEqual((1,), np.array(results['x.sw([-3,-2])']).shape) - self.TestAlmostEqual([-2], results['x.sw([-3,-2])']) - self.assertEqual((1,), np.array(results['x.sw([0,1])']).shape) - self.TestAlmostEqual([1],results['x.sw([0,1])']) - + self.assertEqual((1,), np.array(results["x.z(-1)"]).shape) + self.TestAlmostEqual([1], results["x.z(-1)"]) + self.assertEqual((1,), np.array(results["x.z(0)"]).shape) + self.TestAlmostEqual([0], results["x.z(0)"]) + self.assertEqual((1,), np.array(results["x.z(2)"]).shape) + self.TestAlmostEqual([-2], results["x.z(2)"]) + self.assertEqual((1,), np.array(results["x.sw([-1,0])"]).shape) + self.TestAlmostEqual([0], results["x.sw([-1,0])"]) + self.assertEqual((1,), np.array(results["x.sw([1,2])"]).shape) + self.TestAlmostEqual([7], results["x.sw([1,2])"]) + self.assertEqual((1, 4), np.array(results["x.sw([-3,1])"]).shape) + self.TestAlmostEqual([[-2, -1, 0, 1]], results["x.sw([-3,1])"]) + self.assertEqual((1,), np.array(results["x.sw([-3,-2])"]).shape) + self.TestAlmostEqual([-2], results["x.sw([-3,-2])"]) + self.assertEqual((1,), np.array(results["x.sw([0,1])"]).shape) + self.TestAlmostEqual([1], results["x.sw([0,1])"]) + def test_single_in_window_offset(self): NeuObj.clearNames() # Here there is more sample for each time step but the dimensions of the input is 1 - in1 = Input('in1') + in1 = Input("in1") # Finestre nel tempo - out1 = Output('x.tw(1)', in1.tw(1,offset=-1)) - out2 = Output('x.tw([-1,0])', in1.tw([-1, 0],offset=-1)) - out3 = Output('x.tw([1,3])', in1.tw([1, 3],offset=1)) - out4 = Output('x.tw([-3,-2])', in1.tw([-3, -2],offset=-3)) + out1 = Output("x.tw(1)", in1.tw(1, offset=-1)) + out2 = Output("x.tw([-1,0])", in1.tw([-1, 0], offset=-1)) + out3 = Output("x.tw([1,3])", in1.tw([1, 3], offset=1)) + out4 = Output("x.tw([-3,-2])", in1.tw([-3, -2], offset=-3)) # Finesatre nei samples - out5 = Output('x.sw([-1,0])', in1.sw([-1, 0],offset=-1)) - out6 = Output('x.sw([-3,1])', in1.sw([-3, 1],offset=-3)) - out7 = Output('x.sw([0,1])', in1.sw([0, 1],offset=0)) - out8 = Output('x.sw([-3, 3])', in1.sw([-3, 3], offset=2)) - out9 = Output('x.sw([-3, 3])-2', in1.sw([-3, 3], offset=-1)) + out5 = Output("x.sw([-1,0])", in1.sw([-1, 0], offset=-1)) + out6 = Output("x.sw([-3,1])", in1.sw([-3, 1], offset=-3)) + out7 = Output("x.sw([0,1])", in1.sw([0, 1], offset=0)) + out8 = Output("x.sw([-3, 3])", in1.sw([-3, 3], offset=2)) + out9 = Output("x.sw([-3, 3])-2", in1.sw([-3, 3], offset=-1)) - test = Modely(visualizer = None, seed = 1) - test.addModel('out',[out1,out2,out3,out4,out5,out6,out7,out8,out9]) + test = Modely(visualizer=None, seed=1) + test.addModel("out", [out1, out2, out3, out4, out5, out6, out7, out8, out9]) test.neuralizeModel(1) # Time -2,-1,0,1,2,3 # zero represent the last passed instant - results = test({'in1': [[-2],[-1],[0],[1],[7],[3]]}) + results = test({"in1": [[-2], [-1], [0], [1], [7], [3]]}) # Time window - self.assertEqual((1,), np.array(results['x.tw(1)']).shape) - self.TestAlmostEqual([0], results['x.tw(1)']) - self.assertEqual((1,), np.array(results['x.tw([-1,0])']).shape) - self.TestAlmostEqual([0], results['x.tw([-1,0])']) - self.assertEqual((1,2), np.array(results['x.tw([1,3])']).shape) - self.TestAlmostEqual([[0, -4]], results['x.tw([1,3])']) - self.assertEqual((1,), np.array(results['x.tw([-3,-2])']).shape) - self.TestAlmostEqual([0],results['x.tw([-3,-2])']) + self.assertEqual((1,), np.array(results["x.tw(1)"]).shape) + self.TestAlmostEqual([0], results["x.tw(1)"]) + self.assertEqual((1,), np.array(results["x.tw([-1,0])"]).shape) + self.TestAlmostEqual([0], results["x.tw([-1,0])"]) + self.assertEqual((1, 2), np.array(results["x.tw([1,3])"]).shape) + self.TestAlmostEqual([[0, -4]], results["x.tw([1,3])"]) + self.assertEqual((1,), np.array(results["x.tw([-3,-2])"]).shape) + self.TestAlmostEqual([0], results["x.tw([-3,-2])"]) # # Sample window - self.assertEqual((1,), np.array(results['x.sw([-1,0])']).shape) - self.TestAlmostEqual([0], results['x.sw([-1,0])']) - self.assertEqual((1,4), np.array(results['x.sw([-3,1])']).shape) - self.TestAlmostEqual([[0,1,2,3]], results['x.sw([-3,1])']) - self.assertEqual((1,), np.array(results['x.sw([0,1])']).shape) - self.TestAlmostEqual([0],results['x.sw([0,1])']) - self.assertEqual((1,6), np.array(results['x.sw([-3, 3])']).shape) - self.TestAlmostEqual([[-5,-4,-3,-2,4,0]],results['x.sw([-3, 3])']) - self.assertEqual((1,6), np.array(results['x.sw([-3, 3])-2']).shape) - self.TestAlmostEqual([[-2,-1,0,1,7,3]],results['x.sw([-3, 3])-2']) - + self.assertEqual((1,), np.array(results["x.sw([-1,0])"]).shape) + self.TestAlmostEqual([0], results["x.sw([-1,0])"]) + self.assertEqual((1, 4), np.array(results["x.sw([-3,1])"]).shape) + self.TestAlmostEqual([[0, 1, 2, 3]], results["x.sw([-3,1])"]) + self.assertEqual((1,), np.array(results["x.sw([0,1])"]).shape) + self.TestAlmostEqual([0], results["x.sw([0,1])"]) + self.assertEqual((1, 6), np.array(results["x.sw([-3, 3])"]).shape) + self.TestAlmostEqual([[-5, -4, -3, -2, 4, 0]], results["x.sw([-3, 3])"]) + self.assertEqual((1, 6), np.array(results["x.sw([-3, 3])-2"]).shape) + self.TestAlmostEqual([[-2, -1, 0, 1, 7, 3]], results["x.sw([-3, 3])-2"]) + def test_multi_in_window_offset(self): NeuObj.clearNames() # Here there is more sample for each time step but the dimensions of the input is 1 - in1 = Input('in1',dimensions=3) + in1 = Input("in1", dimensions=3) # Finestre nel tempo - out1 = Output('x.tw(1)', in1.tw(1, offset=-1)) - out2 = Output('x.tw([-1,0])', in1.tw([-1, 0], offset=-1)) - out3 = Output('x.tw([1,3])', in1.tw([1, 3], offset=1)) - out4 = Output('x.tw([-3,-2])', in1.tw([-3, -2], offset=-3)) + out1 = Output("x.tw(1)", in1.tw(1, offset=-1)) + out2 = Output("x.tw([-1,0])", in1.tw([-1, 0], offset=-1)) + out3 = Output("x.tw([1,3])", in1.tw([1, 3], offset=1)) + out4 = Output("x.tw([-3,-2])", in1.tw([-3, -2], offset=-3)) # Finesatre nei samples - out5 = Output('x.sw([-1,0])', in1.sw([-1, 0], offset=-1)) - out6 = Output('x.sw([-3,1])', in1.sw([-3, 1], offset=-3)) - out7 = Output('x.sw([0,1])', in1.sw([0, 1], offset=0)) - out8 = Output('x.sw([-3, 3])', in1.sw([-3, 3], offset=2)) - out9 = Output('x.sw([-3, 3])-2', in1.sw([-3, 3], offset=-1)) + out5 = Output("x.sw([-1,0])", in1.sw([-1, 0], offset=-1)) + out6 = Output("x.sw([-3,1])", in1.sw([-3, 1], offset=-3)) + out7 = Output("x.sw([0,1])", in1.sw([0, 1], offset=0)) + out8 = Output("x.sw([-3, 3])", in1.sw([-3, 3], offset=2)) + out9 = Output("x.sw([-3, 3])-2", in1.sw([-3, 3], offset=-1)) - test = Modely(visualizer = None, seed = 1) - test.addModel('out',[out1, out2, out3, out4, out5, out6, out7, out8, out9]) + test = Modely(visualizer=None, seed=1) + test.addModel("out", [out1, out2, out3, out4, out5, out6, out7, out8, out9]) test.neuralizeModel(1) # Single input # Time -2, -1, 0, 1, 2, 3 # zero represent the last passed instant - results = test({'in1': [[-2,3,4],[-1,2,2],[0,0,0],[1,2,3],[2,7,3],[3,3,3]]}) + results = test( + { + "in1": [ + [-2, 3, 4], + [-1, 2, 2], + [0, 0, 0], + [1, 2, 3], + [2, 7, 3], + [3, 3, 3], + ] + } + ) # Time window - self.assertEqual((1,1,3), np.array(results['x.tw(1)']).shape) - self.TestAlmostEqual([[[0,0,0]]], results['x.tw(1)']) - self.assertEqual((1,1,3), np.array(results['x.tw([-1,0])']).shape) - self.TestAlmostEqual([[[0,0,0]]], results['x.tw([-1,0])']) - self.assertEqual((1,2,3), np.array(results['x.tw([1,3])']).shape) - self.TestAlmostEqual([[[0,0,0], [1,-4,0]]], results['x.tw([1,3])']) - self.assertEqual((1,1,3), np.array(results['x.tw([-3,-2])']).shape) - self.TestAlmostEqual([[[0,0,0]]], results['x.tw([-3,-2])']) + self.assertEqual((1, 1, 3), np.array(results["x.tw(1)"]).shape) + self.TestAlmostEqual([[[0, 0, 0]]], results["x.tw(1)"]) + self.assertEqual((1, 1, 3), np.array(results["x.tw([-1,0])"]).shape) + self.TestAlmostEqual([[[0, 0, 0]]], results["x.tw([-1,0])"]) + self.assertEqual((1, 2, 3), np.array(results["x.tw([1,3])"]).shape) + self.TestAlmostEqual([[[0, 0, 0], [1, -4, 0]]], results["x.tw([1,3])"]) + self.assertEqual((1, 1, 3), np.array(results["x.tw([-3,-2])"]).shape) + self.TestAlmostEqual([[[0, 0, 0]]], results["x.tw([-3,-2])"]) # # Sample window - self.assertEqual((1,1,3), np.array(results['x.sw([-1,0])']).shape) - self.TestAlmostEqual([[[0,0,0]]], results['x.sw([-1,0])']) - self.assertEqual((1,4,3), np.array(results['x.sw([-3,1])']).shape) - self.TestAlmostEqual([[[0,0,0], [1,-1,-2], [2,-3,-4], [3,-1,-1]]], results['x.sw([-3,1])']) - self.assertEqual((1,1,3), np.array(results['x.sw([0,1])']).shape) - self.TestAlmostEqual([[[0,0,0]]], results['x.sw([0,1])']) - self.assertEqual((1,6,3), np.array(results['x.sw([-3, 3])']).shape) - self.TestAlmostEqual([[[-5,0,1],[-4,-1,-1], [-3,-3,-3], [-2,-1,0], [-1,4,0], [0,0,0]]], results['x.sw([-3, 3])']) - self.assertEqual((1,6,3), np.array(results['x.sw([-3, 3])-2']).shape) - self.TestAlmostEqual([[[-2,3,4],[-1,2,2],[0,0,0],[1,2,3],[2,7,3],[3,3,3]]], results['x.sw([-3, 3])-2']) + self.assertEqual((1, 1, 3), np.array(results["x.sw([-1,0])"]).shape) + self.TestAlmostEqual([[[0, 0, 0]]], results["x.sw([-1,0])"]) + self.assertEqual((1, 4, 3), np.array(results["x.sw([-3,1])"]).shape) + self.TestAlmostEqual( + [[[0, 0, 0], [1, -1, -2], [2, -3, -4], [3, -1, -1]]], + results["x.sw([-3,1])"], + ) + self.assertEqual((1, 1, 3), np.array(results["x.sw([0,1])"]).shape) + self.TestAlmostEqual([[[0, 0, 0]]], results["x.sw([0,1])"]) + self.assertEqual((1, 6, 3), np.array(results["x.sw([-3, 3])"]).shape) + self.TestAlmostEqual( + [ + [ + [-5, 0, 1], + [-4, -1, -1], + [-3, -3, -3], + [-2, -1, 0], + [-1, 4, 0], + [0, 0, 0], + ] + ], + results["x.sw([-3, 3])"], + ) + self.assertEqual((1, 6, 3), np.array(results["x.sw([-3, 3])-2"]).shape) + self.TestAlmostEqual( + [[[-2, 3, 4], [-1, 2, 2], [0, 0, 0], [1, 2, 3], [2, 7, 3], [3, 3, 3]]], + results["x.sw([-3, 3])-2"], + ) # Multi input - results = test({'in1': [[-2, 3, 4], [-1, 2, 2], [0, 0, 0], [1, 2, 3], [2, 7, 3], [3, 3, 3], [2, 2, 2]]}) - self.assertEqual((2,6,3), np.array(results['x.sw([-3, 3])']).shape) - self.TestAlmostEqual([[[-5,0,1], [-4,-1,-1], [-3,-3,-3], [-2,-1,0], [-1,4,0], [0,0,0]], - [[-3,0,0], [-2,-2,-2], [-1,0,1], [0,5,1], [1,1,1], [0,0,0]]], results['x.sw([-3, 3])']) - self.assertEqual((2,6,3), np.array(results['x.sw([-3, 3])-2']).shape) - self.TestAlmostEqual([[[-2,3,4],[-1,2,2],[0,0,0],[1,2,3],[2,7,3],[3,3,3]], - [[-2,0,-1],[-1,-2,-3],[0,0,0],[1,5,0],[2,1,0],[1,0,-1]]], results['x.sw([-3, 3])-2']) - + results = test( + { + "in1": [ + [-2, 3, 4], + [-1, 2, 2], + [0, 0, 0], + [1, 2, 3], + [2, 7, 3], + [3, 3, 3], + [2, 2, 2], + ] + } + ) + self.assertEqual((2, 6, 3), np.array(results["x.sw([-3, 3])"]).shape) + self.TestAlmostEqual( + [ + [ + [-5, 0, 1], + [-4, -1, -1], + [-3, -3, -3], + [-2, -1, 0], + [-1, 4, 0], + [0, 0, 0], + ], + [[-3, 0, 0], [-2, -2, -2], [-1, 0, 1], [0, 5, 1], [1, 1, 1], [0, 0, 0]], + ], + results["x.sw([-3, 3])"], + ) + self.assertEqual((2, 6, 3), np.array(results["x.sw([-3, 3])-2"]).shape) + self.TestAlmostEqual( + [ + [[-2, 3, 4], [-1, 2, 2], [0, 0, 0], [1, 2, 3], [2, 7, 3], [3, 3, 3]], + [ + [-2, 0, -1], + [-1, -2, -3], + [0, 0, 0], + [1, 5, 0], + [2, 1, 0], + [1, 0, -1], + ], + ], + results["x.sw([-3, 3])-2"], + ) + def test_single_in_window_offset_aritmetic(self): NeuObj.clearNames() # Elementwise arithmetic, Activation, Trigonometric # the dimensions and time window remain unchanged, for the # binary operators must be equal - in1 = Input('in1') - in2 = Input('in2', dimensions=2) - out1 = Output('sum', in1.tw(1, offset=-1) + in1.tw([-1,0])) - out2 = Output('sub', in1.tw([1, 3], offset=1) - in1.tw([-3, -1], offset=-2)) - out3 = Output('mul', in1.tw([-2, 2]) * in1.tw([-3, 1], offset=-2)) + in1 = Input("in1") + in2 = Input("in2", dimensions=2) + out1 = Output("sum", in1.tw(1, offset=-1) + in1.tw([-1, 0])) + out2 = Output("sub", in1.tw([1, 3], offset=1) - in1.tw([-3, -1], offset=-2)) + out3 = Output("mul", in1.tw([-2, 2]) * in1.tw([-3, 1], offset=-2)) - out4 = Output('sum2', in2.tw(1, offset=-1) + in2.tw([-1,0])) - out5 = Output('sub2', in2.tw([1, 3], offset=1) - in2.tw([-3, -1], offset=-2)) - out6 = Output('mul2', in2.tw([-2, 2]) * in2.tw([-3, 1], offset=-2)) + out4 = Output("sum2", in2.tw(1, offset=-1) + in2.tw([-1, 0])) + out5 = Output("sub2", in2.tw([1, 3], offset=1) - in2.tw([-3, -1], offset=-2)) + out6 = Output("mul2", in2.tw([-2, 2]) * in2.tw([-3, 1], offset=-2)) test = Modely(visualizer=None) - test.addModel('out',[out1, out2, out3, out4, out5, out6]) + test.addModel("out", [out1, out2, out3, out4, out5, out6]) test.neuralizeModel(1) # Single input - #Time -2 -1 0 1 2 3 -2 -1 0 1 2 3 - results = test({'in1': [[1], [2], [8], [4], [-1], [6]], 'in2': [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3]]}) - - self.assertEqual((1,), np.array(results['sum']).shape) - self.TestAlmostEqual([8], results['sum']) - self.assertEqual((1,2), np.array(results['sub']).shape) # [-1,6]+1 - [1,2]-2 - self.TestAlmostEqual([[1,7]], results['sub']) - self.assertEqual((1,4), np.array(results['mul']).shape) #[2, 8, 4, -1]*[1, 2, 8, 4]-2 - self.TestAlmostEqual([[-2,0,24,-2]], results['mul'])#[2, 8, 4, -1]*[-1, 0, 6, 2] - - self.assertEqual((1,1,2), np.array(results['sum2']).shape) - self.TestAlmostEqual([[[0,5]]], results['sum2']) - self.assertEqual((1,2,2), np.array(results['sub2']).shape) #[[2,7],[3,3]]-[2,7] - [[-2,3],[-1,2]]-[-1,2] - self.TestAlmostEqual([[[1,-1],[1,-4]]], results['sub2']) # [[0,0],[1,-4]] - [[-1,1],[0,0]] - #[[-1, 2], [0, 5], [1, 2], [2, 7]] * [[-2, 3], [-1, 2], [0, 5], [1, 2]]-[-1, 2] + # Time -2 -1 0 1 2 3 -2 -1 0 1 2 3 + results = test( + { + "in1": [[1], [2], [8], [4], [-1], [6]], + "in2": [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3]], + } + ) + + self.assertEqual((1,), np.array(results["sum"]).shape) + self.TestAlmostEqual([8], results["sum"]) + self.assertEqual((1, 2), np.array(results["sub"]).shape) # [-1,6]+1 - [1,2]-2 + self.TestAlmostEqual([[1, 7]], results["sub"]) + self.assertEqual( + (1, 4), np.array(results["mul"]).shape + ) # [2, 8, 4, -1]*[1, 2, 8, 4]-2 + self.TestAlmostEqual( + [[-2, 0, 24, -2]], results["mul"] + ) # [2, 8, 4, -1]*[-1, 0, 6, 2] + + self.assertEqual((1, 1, 2), np.array(results["sum2"]).shape) + self.TestAlmostEqual([[[0, 5]]], results["sum2"]) + self.assertEqual( + (1, 2, 2), np.array(results["sub2"]).shape + ) # [[2,7],[3,3]]-[2,7] - [[-2,3],[-1,2]]-[-1,2] + self.TestAlmostEqual( + [[[1, -1], [1, -4]]], results["sub2"] + ) # [[0,0],[1,-4]] - [[-1,1],[0,0]] + # [[-1, 2], [0, 5], [1, 2], [2, 7]] * [[-2, 3], [-1, 2], [0, 5], [1, 2]]-[-1, 2] # [[-1, 2], [0, 5], [1, 2], [2, 7]] * [[-1, 1], [0, 0], [1, 3], [2, 0]] - self.assertEqual((1,4,2), np.array(results['mul2']).shape) - self.TestAlmostEqual([[[1, 2], [0, 0], [1, 6], [4, 0]]], results['mul2']) + self.assertEqual((1, 4, 2), np.array(results["mul2"]).shape) + self.TestAlmostEqual([[[1, 2], [0, 0], [1, 6], [4, 0]]], results["mul2"]) # Multi input # Time -2 -1 0 1 2 3 4 -2 -1 0 1 2 3 4 - results = test({'in1': [1, 2, 8, 4, -1, 6, 9], 'in2': [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3], [0, 0]]}) - self.assertEqual((2,), np.array(results['sum']).shape) - self.TestAlmostEqual([8,4], results['sum']) + results = test( + { + "in1": [1, 2, 8, 4, -1, 6, 9], + "in2": [[-2, 3], [-1, 2], [0, 5], [1, 2], [2, 7], [3, 3], [0, 0]], + } + ) + self.assertEqual((2,), np.array(results["sum"]).shape) + self.TestAlmostEqual([8, 4], results["sum"]) # [6,9]-6 - [2,8]-8 = [0,3] - [-6,0] - self.assertEqual((2,2), np.array(results['sub']).shape) # [-1,6]+1 - [1,2]-2 - self.TestAlmostEqual([[1,7],[6,3]], results['sub']) - #[8, 4, -1, 6] * [2, 8, 4, -1]-8 = [8, 4, -1, 6] * [-6, 0, -4, -9] - self.assertEqual((2,4), np.array(results['mul']).shape) - self.TestAlmostEqual([[-2,0,24,-2],[-48,0,4,-54]], results['mul']) - - self.assertEqual((2,1,2), np.array(results['sum2']).shape) - self.TestAlmostEqual([[[0,5]],[[1, 2]]], results['sum2']) - self.assertEqual((2,2,2), np.array(results['sub2']).shape) - #[[3, 3], [0, 0]]-[3,3] - [[-1, 2], [0, 5]]-[0, 5] = [[0, 0], [-3, -3]] - [[-1, -3], [0, 0]] - self.TestAlmostEqual([[[1,-1],[1,-4]],[[1,3],[-3,-3]]], results['sub2']) - #[[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, 2], [0, 5], [1, 2], [2, 7]]-[0, 5] - #[[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, -3], [0, 0], [1, -3], [2, 2]] - self.assertEqual((2,4,2), np.array(results['mul2']).shape) - self.TestAlmostEqual([[[1, 2], [0, 0], [1, 6], [4, 0]],[[0, -15], [0, 0], [2, -21], [6, 6]]], results['mul2']) + self.assertEqual((2, 2), np.array(results["sub"]).shape) # [-1,6]+1 - [1,2]-2 + self.TestAlmostEqual([[1, 7], [6, 3]], results["sub"]) + # [8, 4, -1, 6] * [2, 8, 4, -1]-8 = [8, 4, -1, 6] * [-6, 0, -4, -9] + self.assertEqual((2, 4), np.array(results["mul"]).shape) + self.TestAlmostEqual([[-2, 0, 24, -2], [-48, 0, 4, -54]], results["mul"]) + + self.assertEqual((2, 1, 2), np.array(results["sum2"]).shape) + self.TestAlmostEqual([[[0, 5]], [[1, 2]]], results["sum2"]) + self.assertEqual((2, 2, 2), np.array(results["sub2"]).shape) + # [[3, 3], [0, 0]]-[3,3] - [[-1, 2], [0, 5]]-[0, 5] = [[0, 0], [-3, -3]] - [[-1, -3], [0, 0]] + self.TestAlmostEqual([[[1, -1], [1, -4]], [[1, 3], [-3, -3]]], results["sub2"]) + # [[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, 2], [0, 5], [1, 2], [2, 7]]-[0, 5] + # [[0, 5], [1, 2], [2, 7], [3, 3]] * [[-1, -3], [0, 0], [1, -3], [2, 2]] + self.assertEqual((2, 4, 2), np.array(results["mul2"]).shape) + self.TestAlmostEqual( + [[[1, 2], [0, 0], [1, 6], [4, 0]], [[0, -15], [0, 0], [2, -21], [6, 6]]], + results["mul2"], + ) def test_single_in_window_offset_fir(self): NeuObj.clearNames() # The input must be scalar and the time dimension is compress to 1, # Vector input not allowed, it could be done that a number of fir filters equal to the size of the vector are constructed # Should weights be shared or not? - in1 = Input('in1') - out1 = Output('Fir3', Fir(3)(in1.last())) - out2 = Output('Fir5', Fir(5)(in1.tw(1)))# - out3 = Output('Fir2', Fir(2)(in1.tw([-1,0])))# - out4 = Output('Fir1', Fir(1)(in1.tw([-3,3])))# - out5 = Output('Fir7', Fir(7)(in1.tw(3,offset=-1)))# - out6 = Output('Fir4', Fir(4)(in1.tw([2,3],offset=2)))# - out7 = Output('Fir6', Fir(6)(in1.sw([-2,-1], offset=-2)))# - - test = Modely(visualizer = None, seed = 1) - test.addModel('out',[out1,out2,out3,out4,out5,out6,out7]) + in1 = Input("in1") + out1 = Output("Fir3", Fir(3)(in1.last())) + out2 = Output("Fir5", Fir(5)(in1.tw(1))) # + out3 = Output("Fir2", Fir(2)(in1.tw([-1, 0]))) # + out4 = Output("Fir1", Fir(1)(in1.tw([-3, 3]))) # + out5 = Output("Fir7", Fir(7)(in1.tw(3, offset=-1))) # + out6 = Output("Fir4", Fir(4)(in1.tw([2, 3], offset=2))) # + out7 = Output("Fir6", Fir(6)(in1.sw([-2, -1], offset=-2))) # + + test = Modely(visualizer=None, seed=1) + test.addModel("out", [out1, out2, out3, out4, out5, out6, out7]) test.neuralizeModel(1) # Single input # Time -2 -1 0 1 2 3 - results = test({'in1': [[1], [2], [7], [4], [5], [6]]}) - self.assertEqual((1,1,3), np.array(results['Fir3']).shape) - self.assertEqual((1,1,5), np.array(results['Fir5']).shape) - self.assertEqual((1,1,2), np.array(results['Fir2']).shape) - self.assertEqual((1,), np.array(results['Fir1']).shape) - self.assertEqual((1,1,7), np.array(results['Fir7']).shape) - self.assertEqual((1,1,4), np.array(results['Fir4']).shape) - self.assertEqual((1,1,6), np.array(results['Fir6']).shape) + results = test({"in1": [[1], [2], [7], [4], [5], [6]]}) + self.assertEqual((1, 1, 3), np.array(results["Fir3"]).shape) + self.assertEqual((1, 1, 5), np.array(results["Fir5"]).shape) + self.assertEqual((1, 1, 2), np.array(results["Fir2"]).shape) + self.assertEqual((1,), np.array(results["Fir1"]).shape) + self.assertEqual((1, 1, 7), np.array(results["Fir7"]).shape) + self.assertEqual((1, 1, 4), np.array(results["Fir4"]).shape) + self.assertEqual((1, 1, 6), np.array(results["Fir6"]).shape) # Multi input there are 3 temporal instant # Time -2 -1 0 1 2 3 4 5 - results = test({'in1': [[1], [2], [7], [4], [5], [6], [7], [8]]}) - self.assertEqual((3,1,3), np.array(results['Fir3']).shape) - self.assertEqual((3,1,5), np.array(results['Fir5']).shape) - self.assertEqual((3,1,2), np.array(results['Fir2']).shape) - self.assertEqual((3,), np.array(results['Fir1']).shape) - self.assertEqual((3,1,7), np.array(results['Fir7']).shape) - self.assertEqual((3,1,4), np.array(results['Fir4']).shape) - self.assertEqual((3,1,6), np.array(results['Fir6']).shape) - + results = test({"in1": [[1], [2], [7], [4], [5], [6], [7], [8]]}) + self.assertEqual((3, 1, 3), np.array(results["Fir3"]).shape) + self.assertEqual((3, 1, 5), np.array(results["Fir5"]).shape) + self.assertEqual((3, 1, 2), np.array(results["Fir2"]).shape) + self.assertEqual((3,), np.array(results["Fir1"]).shape) + self.assertEqual((3, 1, 7), np.array(results["Fir7"]).shape) + self.assertEqual((3, 1, 4), np.array(results["Fir4"]).shape) + self.assertEqual((3, 1, 6), np.array(results["Fir6"]).shape) + def test_fir_and_parameter(self): NeuObj.clearNames() - x = Input('x') - p1 = Parameter('p1', tw=3, values=[[1],[2],[3],[6],[2],[3]]) + x = Input("x") + p1 = Parameter("p1", tw=3, values=[[1], [2], [3], [6], [2], [3]]) with self.assertRaises(TypeError): Fir(W=p1)(x) with self.assertRaises(ValueError): Fir(W=p1)(x.tw([-3, 1])) - out1 = Output('out1', Fir(W=p1)(x.tw([-2, 1]))) + out1 = Output("out1", Fir(W=p1)(x.tw([-2, 1]))) - p2 = Parameter('p2', sw=1, values=[[-2]]) + p2 = Parameter("p2", sw=1, values=[[-2]]) with self.assertRaises(TypeError): Fir(W=p2)(x.tw([-2, 1])) - out2 = Output('out2', Fir(W=p2)(x.last())) + out2 = Output("out2", Fir(W=p2)(x.last())) - p3 = Parameter('p3', dimensions=2, sw=1, values=[[-2,1]]) + p3 = Parameter("p3", dimensions=2, sw=1, values=[[-2, 1]]) with self.assertRaises(TypeError): Fir(W=p3)(x.tw([-2, 1])) - out3 = Output('out3', Fir(W=p3)(x.last())) + out3 = Output("out3", Fir(W=p3)(x.last())) - p4 = Parameter('p4', dimensions=2, tw=2, values=[[-2,1],[2,0],[0,1],[4,0]]) + p4 = Parameter( + "p4", dimensions=2, tw=2, values=[[-2, 1], [2, 0], [0, 1], [4, 0]] + ) with self.assertRaises(TypeError): Fir(W=p4)(x.sw([-2, 0])) - out4 = Output('out4', Fir(W=p4)(x.tw([-2, 0]))) + out4 = Output("out4", Fir(W=p4)(x.tw([-2, 0]))) - p5 = Parameter('p6', sw=2, dimensions=2, values=[[-2,1],[2,0]]) + p5 = Parameter("p6", sw=2, dimensions=2, values=[[-2, 1], [2, 0]]) with self.assertRaises(TypeError): - Fir(W = p5)(x) + Fir(W=p5)(x) with self.assertRaises(TypeError): - Fir(W = p5)(x.tw([-2,1])) + Fir(W=p5)(x.tw([-2, 1])) with self.assertRaises(ValueError): - Fir(W = p5)(x.sw([-2,1])) - out5 = Output('out5', Fir(W=p5)(x.sw([-2, 0]))) + Fir(W=p5)(x.sw([-2, 1])) + out5 = Output("out5", Fir(W=p5)(x.sw([-2, 0]))) test = Modely(visualizer=None) - test.addModel('out',[out1, out2, out3, out4, out5]) + test.addModel("out", [out1, out2, out3, out4, out5]) test.neuralizeModel(0.5) # Time -3, -2, -1, 0, 1, 2, 3 input = [-2, -1, 0, 1, 2, 3, 12] - results = test({'x': input}) - - self.assertEqual((2,), np.array(results['out1']).shape) - self.TestAlmostEqual([15,56], results['out1']) - self.assertEqual((2,), np.array(results['out2']).shape) - self.TestAlmostEqual([-2, -4], results['out2']) - self.assertEqual((2, 1, 2), np.array(results['out3']).shape) - self.TestAlmostEqual([[[-2,1]], [[-4,2]]], results['out3']) - self.assertEqual((2, 1, 2), np.array(results['out4']).shape) - self.TestAlmostEqual([[[6.0, -2.0]], [[10.0, 0.0]]], results['out4']) - self.assertEqual((2, 1, 2), np.array(results['out5']).shape) - self.TestAlmostEqual([[[2.0, 0.0]], [[2.0, 1.0]]], results['out5']) - + results = test({"x": input}) + + self.assertEqual((2,), np.array(results["out1"]).shape) + self.TestAlmostEqual([15, 56], results["out1"]) + self.assertEqual((2,), np.array(results["out2"]).shape) + self.TestAlmostEqual([-2, -4], results["out2"]) + self.assertEqual((2, 1, 2), np.array(results["out3"]).shape) + self.TestAlmostEqual([[[-2, 1]], [[-4, 2]]], results["out3"]) + self.assertEqual((2, 1, 2), np.array(results["out4"]).shape) + self.TestAlmostEqual([[[6.0, -2.0]], [[10.0, 0.0]]], results["out4"]) + self.assertEqual((2, 1, 2), np.array(results["out5"]).shape) + self.TestAlmostEqual([[[2.0, 0.0]], [[2.0, 1.0]]], results["out5"]) + def test_single_in_window_offset_parametric_function(self): NeuObj.clearNames() # An input dimension is temporal and does not remain unchanged unless redefined on output # If there are multiple inputs the function returns an error if the dimensions are not defined - in1 = Input('in1') + in1 = Input("in1") parfun = ParamFun(myfun) - out = Output('out', parfun(in1.last())) - test = Modely(visualizer = None, seed = 1) - test.addModel('out',out) + out = Output("out", parfun(in1.last())) + test = Modely(visualizer=None, seed=1) + test.addModel("out", out) test.neuralizeModel(0.1) - #results = test({'in1': 1}) - #self.TestAlmostEqual(results['out'], [0.7576315999031067]) - results = test({'in1': [[1]]}) - self.assertEqual((1,), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'],[0.7576315999031067]) - results = test({'in1': [2]}) - self.TestAlmostEqual(results['out'],[1.5152631998062134]) - results = test({'in1': [1,2]}) - self.assertEqual((2,), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'],[0.7576315999031067,1.5152631998062134]) - - NeuObj.clearNames('out') - out = Output('out', ParamFun(myfun)(in1.tw(0.2))) - test = Modely(visualizer=None, seed = 1) - test.addModel('out',out) + # results = test({'in1': 1}) + # self.TestAlmostEqual(results['out'], [0.7576315999031067]) + results = test({"in1": [[1]]}) + self.assertEqual((1,), np.array(results["out"]).shape) + self.TestAlmostEqual(results["out"], [0.7576315999031067]) + results = test({"in1": [2]}) + self.TestAlmostEqual(results["out"], [1.5152631998062134]) + results = test({"in1": [1, 2]}) + self.assertEqual((2,), np.array(results["out"]).shape) + self.TestAlmostEqual(results["out"], [0.7576315999031067, 1.5152631998062134]) + + NeuObj.clearNames("out") + out = Output("out", ParamFun(myfun)(in1.tw(0.2))) + test = Modely(visualizer=None, seed=1) + test.addModel("out", out) test.neuralizeModel(0.1) with self.assertRaises(StopIteration): - results = test({'in1': [1]}) + results = test({"in1": [1]}) with self.assertRaises(StopIteration): - results = test({'in1': [2]}) - results = test({'in1': [1,2]}) - self.assertEqual((1,2), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134]]) - test({'in1': [[1, 2]]}, num_of_samples=5, sampled=True) - results = test({'in1': [[1,2]]}, sampled=True) - self.assertEqual((1,2), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134]]) - results = test({'in1': [1, 2, 3, 4, 5]})# Qui vengono costruite gli input a due a due con shift di 1 - self.assertEqual((4,2), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134], [1.5152631998062134, 2.272894859313965], [2.272894859313965, 3.0305263996124268], [3.0305263996124268, 3.7881579399108887]]) - results = test({'in1': [[1, 2], [2, 3], [3, 4], [4, 5]]}, sampled=True) - self.assertEqual((4,2), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[0.7576315999031067, 1.5152631998062134], [1.5152631998062134, 2.272894859313965], [2.272894859313965, 3.0305263996124268], [3.0305263996124268, 3.7881579399108887]]) - - out = Output('out2', ParamFun(myfun)(in1.last(),in1.last())) - test = Modely(visualizer=None, seed = 1) - test.addModel('out',out) + results = test({"in1": [2]}) + results = test({"in1": [1, 2]}) + self.assertEqual((1, 2), np.array(results["out"]).shape) + self.TestAlmostEqual(results["out"], [[0.7576315999031067, 1.5152631998062134]]) + test({"in1": [[1, 2]]}, num_of_samples=5, sampled=True) + results = test({"in1": [[1, 2]]}, sampled=True) + self.assertEqual((1, 2), np.array(results["out"]).shape) + self.TestAlmostEqual(results["out"], [[0.7576315999031067, 1.5152631998062134]]) + results = test( + {"in1": [1, 2, 3, 4, 5]} + ) # Qui vengono costruite gli input a due a due con shift di 1 + self.assertEqual((4, 2), np.array(results["out"]).shape) + self.TestAlmostEqual( + results["out"], + [ + [0.7576315999031067, 1.5152631998062134], + [1.5152631998062134, 2.272894859313965], + [2.272894859313965, 3.0305263996124268], + [3.0305263996124268, 3.7881579399108887], + ], + ) + results = test({"in1": [[1, 2], [2, 3], [3, 4], [4, 5]]}, sampled=True) + self.assertEqual((4, 2), np.array(results["out"]).shape) + self.TestAlmostEqual( + results["out"], + [ + [0.7576315999031067, 1.5152631998062134], + [1.5152631998062134, 2.272894859313965], + [2.272894859313965, 3.0305263996124268], + [3.0305263996124268, 3.7881579399108887], + ], + ) + + out = Output("out2", ParamFun(myfun)(in1.last(), in1.last())) + test = Modely(visualizer=None, seed=1) + test.addModel("out", out) test.neuralizeModel(0.1) - #results = test({'in1': 1}) - #self.TestAlmostEqual(results['out'],[1]) - results = test({'in1': [1]}) - self.TestAlmostEqual(results['out2'],[1]) - results = test({'in1': [2]}) - self.TestAlmostEqual(results['out2'],[4]) - results = test({'in1': [1,2]}) - self.TestAlmostEqual(results['out2'],[1,4]) + # results = test({'in1': 1}) + # self.TestAlmostEqual(results['out'],[1]) + results = test({"in1": [1]}) + self.TestAlmostEqual(results["out2"], [1]) + results = test({"in1": [2]}) + self.TestAlmostEqual(results["out2"], [4]) + results = test({"in1": [1, 2]}) + self.TestAlmostEqual(results["out2"], [1, 4]) - out = Output('out3', ParamFun(myfun)(in1.tw(0.1), in1.tw(0.1))) + out = Output("out3", ParamFun(myfun)(in1.tw(0.1), in1.tw(0.1))) test = Modely(visualizer=None) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) - #results = test({'in1': 2}) - #self.TestAlmostEqual(results['out'], [4]) - results = test({'in1': [2]}) - self.TestAlmostEqual(results['out3'], [4]) - results = test({'in1': [2,1]}) - self.TestAlmostEqual(results['out3'], [4,1]) + # results = test({'in1': 2}) + # self.TestAlmostEqual(results['out'], [4]) + results = test({"in1": [2]}) + self.TestAlmostEqual(results["out3"], [4]) + results = test({"in1": [2, 1]}) + self.TestAlmostEqual(results["out3"], [4, 1]) - out = Output('out4', ParamFun(myfun)(in1.tw(0.2), in1.tw(0.2))) + out = Output("out4", ParamFun(myfun)(in1.tw(0.2), in1.tw(0.2))) test = Modely(visualizer=None) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) - results = test({'in1': [2,4]}) - self.assertEqual((1,2), np.array(results['out4']).shape) - self.TestAlmostEqual(results['out4'], [[4,16]]) - results = test({'in1': [[1, 2], [3, 2]]}, sampled=True) - self.assertEqual((2,2), np.array(results['out4']).shape) - self.TestAlmostEqual(results['out4'], [[1.0, 4.0], [9.0, 4.0]]) + results = test({"in1": [2, 4]}) + self.assertEqual((1, 2), np.array(results["out4"]).shape) + self.TestAlmostEqual(results["out4"], [[4, 16]]) + results = test({"in1": [[1, 2], [3, 2]]}, sampled=True) + self.assertEqual((2, 2), np.array(results["out4"]).shape) + self.TestAlmostEqual(results["out4"], [[1.0, 4.0], [9.0, 4.0]]) - out = Output('out5', ParamFun(myfun)(in1.tw(0.3), in1.tw(0.3))) + out = Output("out5", ParamFun(myfun)(in1.tw(0.3), in1.tw(0.3))) test = Modely(visualizer=None) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) with self.assertRaises(StopIteration): - test({'in1': [2]}) + test({"in1": [2]}) with self.assertRaises(StopIteration): - test({'in1': [2, 4]}) - - results = test({'in1': [3,2,1]}) - self.assertEqual((1,3), np.array(results['out5']).shape) - self.TestAlmostEqual(results['out5'], [[9,4,1]]) - results = test({'in1': [[1,2,2],[3,4,5]]}, sampled=True) - self.assertEqual((2,3), np.array(results['out5']).shape) - self.TestAlmostEqual(results['out5'], [[1, 4, 4], [9, 16, 25]]) - results = test({'in1': [[3, 2, 1], [2, 1, 0]]}, sampled=True) - self.assertEqual((2,3), np.array(results['out5']).shape) - self.TestAlmostEqual(results['out5'], [[9, 4, 1],[4, 1, 0]]) - results = test({'in1': [3,2,1,0]}) - self.assertEqual((2,3), np.array(results['out5']).shape) - self.TestAlmostEqual(results['out5'], [[9, 4, 1],[4, 1, 0]]) - - out = Output('out6', ParamFun(myfun)(in1.tw(0.4), in1.tw(0.4))) + test({"in1": [2, 4]}) + + results = test({"in1": [3, 2, 1]}) + self.assertEqual((1, 3), np.array(results["out5"]).shape) + self.TestAlmostEqual(results["out5"], [[9, 4, 1]]) + results = test({"in1": [[1, 2, 2], [3, 4, 5]]}, sampled=True) + self.assertEqual((2, 3), np.array(results["out5"]).shape) + self.TestAlmostEqual(results["out5"], [[1, 4, 4], [9, 16, 25]]) + results = test({"in1": [[3, 2, 1], [2, 1, 0]]}, sampled=True) + self.assertEqual((2, 3), np.array(results["out5"]).shape) + self.TestAlmostEqual(results["out5"], [[9, 4, 1], [4, 1, 0]]) + results = test({"in1": [3, 2, 1, 0]}) + self.assertEqual((2, 3), np.array(results["out5"]).shape) + self.TestAlmostEqual(results["out5"], [[9, 4, 1], [4, 1, 0]]) + + out = Output("out6", ParamFun(myfun)(in1.tw(0.4), in1.tw(0.4))) test = Modely(visualizer=None) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) with self.assertRaises(StopIteration): - test({'in1': [[1, 2, 2], [3, 4, 5]]}) - + test({"in1": [[1, 2, 2], [3, 4, 5]]}) + def test_vectorial_input_parametric_function(self): NeuObj.clearNames() # Vector input for parametric function - in1 = Input('in1', dimensions=3) - in2 = Input('in2', dimensions=2) - p1 = Parameter('p1', sw=1, dimensions=(3,2), values=[[[1,2],[3,4],[5,6]]]) - p2 = Parameter('p2', sw=1, dimensions=(1,3), values=[[1,2,3]]) - parfun = ParamFun(myfun3, parameters_and_constants=[p1,p2]) - out = Output('out', parfun(in1.last(),in2.last())) - test = Modely(visualizer = None, seed = 1) - test.addModel('out',out) + in1 = Input("in1", dimensions=3) + in2 = Input("in2", dimensions=2) + p1 = Parameter("p1", sw=1, dimensions=(3, 2), values=[[[1, 2], [3, 4], [5, 6]]]) + p2 = Parameter("p2", sw=1, dimensions=(1, 3), values=[[1, 2, 3]]) + parfun = ParamFun(myfun3, parameters_and_constants=[p1, p2]) + out = Output("out", parfun(in1.last(), in2.last())) + test = Modely(visualizer=None, seed=1) + test.addModel("out", out) test.neuralizeModel(0.1) - results = test({'in1': [[1,2,3]],'in2':[[5,6]]}) - self.assertEqual((1,3), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[23,52,81]]) + results = test({"in1": [[1, 2, 3]], "in2": [[5, 6]]}) + self.assertEqual((1, 3), np.array(results["out"]).shape) + self.TestAlmostEqual(results["out"], [[23, 52, 81]]) + + results = test({"in1": [[1, 2, 3], [5, 6, 7]], "in2": [[5, 6], [7, 8]]}) + self.assertEqual((2, 3), np.array(results["out"]).shape) + self.TestAlmostEqual(results["out"], [[23, 52, 81], [41, 94, 147]]) - results = test({'in1': [[1,2,3],[5,6,7]],'in2':[[5,6],[7,8]]}) - self.assertEqual((2,3), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[23,52,81],[41,94,147]]) - def test_parametric_function_and_fir(self): NeuObj.clearNames() - in1 = Input('in1') - in2 = Input('in2') - out = Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4)))) + in1 = Input("in1") + in2 = Input("in2") + out = Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4)))) test = Modely(visualizer=None, seed=1) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) with self.assertRaises(StopIteration): - test({'in1': [1, 2, 2]}) - results = test({'in1': [[1], [2], [2], [4]]}) - self.TestAlmostEqual(results['out'][0], -0.03262542933225632) - results = test({'in1': [[[1], [2], [2], [4]],[[2], [2], [4], [5]]]}, sampled=True) - self.TestAlmostEqual(results['out'], [-0.03262542933225632, -0.001211114227771759]) - results = test({'in1': [[1], [2], [2], [4], [5]]}) - self.TestAlmostEqual(results['out'], [-0.03262542933225632, -0.001211114227771759]) + test({"in1": [1, 2, 2]}) + results = test({"in1": [[1], [2], [2], [4]]}) + self.TestAlmostEqual(results["out"][0], -0.03262542933225632) + results = test( + {"in1": [[[1], [2], [2], [4]], [[2], [2], [4], [5]]]}, sampled=True + ) + self.TestAlmostEqual( + results["out"], [-0.03262542933225632, -0.001211114227771759] + ) + results = test({"in1": [[1], [2], [2], [4], [5]]}) + self.TestAlmostEqual( + results["out"], [-0.03262542933225632, -0.001211114227771759] + ) with self.assertRaises(RuntimeError): - Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4),in2.tw(0.2)))) + Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.2)))) NeuObj.clearNames() - in1 = Input('in1') - in2 = Input('in2') - out = Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4),in2.tw(0.4)))) + in1 = Input("in1") + in2 = Input("in2") + out = Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.4)))) test = Modely(visualizer=None, seed=1) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) - with self.assertRaises(StopIteration): ## TODO: change to KeyError when checking the inputs - test({'in1': [[1, 2, 2, 4]]}) - - results = test({'in1': [1, 2, 2, 4], 'in2': [1, 2, 2, 4]}) - self.TestAlmostEqual(results['out'], [-0.4379930794239044]) - results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [-0.4379930794239044]) - results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]}) - self.TestAlmostEqual(results['out'], [-0.4379930794239044]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [-0.4379930794239044]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True) - self.TestAlmostEqual(results['out'], [-0.4379930794239044, 0.5163354873657227]) - - results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [-0.4379930794239044]) - results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]}) - self.TestAlmostEqual(results['out'], [-0.4379930794239044]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [-0.4379930794239044]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True) - self.TestAlmostEqual(results['out'], [-0.4379930794239044, 0.5163354873657227]) + with self.assertRaises( + StopIteration + ): ## TODO: change to KeyError when checking the inputs + test({"in1": [[1, 2, 2, 4]]}) + + results = test({"in1": [1, 2, 2, 4], "in2": [1, 2, 2, 4]}) + self.TestAlmostEqual(results["out"], [-0.4379930794239044]) + results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True) + self.TestAlmostEqual(results["out"], [-0.4379930794239044]) + results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]}) + self.TestAlmostEqual(results["out"], [-0.4379930794239044]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True + ) + self.TestAlmostEqual(results["out"], [-0.4379930794239044]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]}, + sampled=True, + ) + self.TestAlmostEqual(results["out"], [-0.4379930794239044, 0.5163354873657227]) + + results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True) + self.TestAlmostEqual(results["out"], [-0.4379930794239044]) + results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]}) + self.TestAlmostEqual(results["out"], [-0.4379930794239044]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True + ) + self.TestAlmostEqual(results["out"], [-0.4379930794239044]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]}, + sampled=True, + ) + self.TestAlmostEqual(results["out"], [-0.4379930794239044, 0.5163354873657227]) NeuObj.clearNames() - in1 = Input('in1') - in2 = Input('in2') - out = Output('out', Fir(3)(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.4)))) + in1 = Input("in1") + in2 = Input("in2") + out = Output("out", Fir(3)(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.4)))) test = Modely(visualizer=None, seed=1) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [1, 2, 2, 4]]}, sampled=True) - self.assertEqual((2,1,3), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]], [[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]]]) - - NeuObj.clearNames('out') + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [1, 2, 2, 4]]}, + sampled=True, + ) + self.assertEqual((2, 1, 3), np.array(results["out"]).shape) + self.TestAlmostEqual( + results["out"], + [ + [[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]], + [[0.22182656824588776, -0.11421152949333191, 0.5385046601295471]], + ], + ) + + NeuObj.clearNames("out") parfun = ParamFun(myfun2) with self.assertRaises(TypeError): - Output('out', parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4))))) + Output("out", parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4))))) parfun = ParamFun(myfun2) - out = Output('out', parfun(Fir(3)(parfun(in1.tw(0.4))))) + out = Output("out", parfun(Fir(3)(parfun(in1.tw(0.4))))) test = Modely(visualizer=None, seed=1) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) - results = test({'in1': [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True) - self.assertEqual((2,1,3), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], [[0.24776744842529297, 0.2278038114309311, 0.2481299340724945]]]) - - results = test({'in1': [1, 2, 2, 4, 3],'in2': [6, 2, 2, 4, 4]}) - self.assertEqual((2,1,3), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], [[0.1667831689119339,0.16757671535015106, 0.1605043113231659]]]) - results = test({'in1': [[1, 2, 2, 4],[2, 2, 4, 3]],'in2': [[6, 2, 2, 4],[2, 2, 4, 4]]}, sampled=True) - self.assertEqual((2,1,3), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], [[0.1667831689119339, 0.16757671535015106,0.1605043113231659]]]) + results = test({"in1": [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True) + self.assertEqual((2, 1, 3), np.array(results["out"]).shape) + self.TestAlmostEqual( + results["out"], + [ + [[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], + [[0.24776744842529297, 0.2278038114309311, 0.2481299340724945]], + ], + ) + + results = test({"in1": [1, 2, 2, 4, 3], "in2": [6, 2, 2, 4, 4]}) + self.assertEqual((2, 1, 3), np.array(results["out"]).shape) + self.TestAlmostEqual( + results["out"], + [ + [[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], + [[0.1667831689119339, 0.16757671535015106, 0.1605043113231659]], + ], + ) + results = test( + {"in1": [[1, 2, 2, 4], [2, 2, 4, 3]], "in2": [[6, 2, 2, 4], [2, 2, 4, 4]]}, + sampled=True, + ) + self.assertEqual((2, 1, 3), np.array(results["out"]).shape) + self.TestAlmostEqual( + results["out"], + [ + [[0.2126065045595169, 0.21099068224430084, 0.20540902018547058]], + [[0.1667831689119339, 0.16757671535015106, 0.1605043113231659]], + ], + ) def test_parametric_function_and_fir_with_parameters(self): NeuObj.clearNames() - in1 = Input('in1') - in2 = Input('in2') - k1 = Parameter('k1', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) - k2 = Parameter('k2', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) - out = Output('out', Fir(ParamFun(myfun2, parameters_and_constants=[k1, k2])(in1.tw(0.4)))) + in1 = Input("in1") + in2 = Input("in2") + k1 = Parameter("k1", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) + k2 = Parameter("k2", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) + out = Output( + "out", Fir(ParamFun(myfun2, parameters_and_constants=[k1, k2])(in1.tw(0.4))) + ) test = Modely(visualizer=None, seed=42) - test.addModel('out', out) + test.addModel("out", out) test.neuralizeModel(0.1) with self.assertRaises(StopIteration): - test({'in1': [1, 2, 2]}) - results = test({'in1': [[1], [2], [2], [4]]}) - self.TestAlmostEqual(results['out'][0], 0.06549876928329468) - results = test({'in1': [[[1], [2], [2], [4]], [[2], [2], [4], [5]]]}, sampled=True) - self.TestAlmostEqual(results['out'], [0.06549876928329468, -0.38155099749565125]) - results = test({'in1': [[1], [2], [2], [4], [5]]}) - self.TestAlmostEqual(results['out'], [0.06549876928329468, -0.38155099749565125]) + test({"in1": [1, 2, 2]}) + results = test({"in1": [[1], [2], [2], [4]]}) + self.TestAlmostEqual(results["out"][0], 0.06549876928329468) + results = test( + {"in1": [[[1], [2], [2], [4]], [[2], [2], [4], [5]]]}, sampled=True + ) + self.TestAlmostEqual( + results["out"], [0.06549876928329468, -0.38155099749565125] + ) + results = test({"in1": [[1], [2], [2], [4], [5]]}) + self.TestAlmostEqual( + results["out"], [0.06549876928329468, -0.38155099749565125] + ) with self.assertRaises(RuntimeError): - Output('out', Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.2)))) + Output("out", Fir(ParamFun(myfun2)(in1.tw(0.4), in2.tw(0.2)))) NeuObj.clearNames() - in1 = Input('in1') - in2 = Input('in2') - k1 = Parameter('k1', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) - k_fir = Parameter('k_fir', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) - out = Output('out', Fir(W=k_fir)(ParamFun(myfun2, parameters_and_constants=[k1])(in1.tw(0.4), in2.tw(0.4)))) + in1 = Input("in1") + in2 = Input("in2") + k1 = Parameter("k1", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) + k_fir = Parameter( + "k_fir", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]] + ) + out = Output( + "out", + Fir(W=k_fir)( + ParamFun(myfun2, parameters_and_constants=[k1])( + in1.tw(0.4), in2.tw(0.4) + ) + ), + ) test = Modely(visualizer=None, seed=42) - test.addModel('out', out) + test.addModel("out", out) test.neuralizeModel(0.1) with self.assertRaises(StopIteration): - test({'in1': [[1, 2, 2, 4]]}) - - results = test({'in1': [1, 2, 2, 4], 'in2': [1, 2, 2, 4]}) - self.TestAlmostEqual(results['out'], [0.3850506544113159]) - results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [0.3850506544113159]) - results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]}) - self.TestAlmostEqual(results['out'], [0.3850506544113159]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [0.3850506544113159]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True) - self.TestAlmostEqual(results['out'], [0.3850506544113159, 1.446676254272461]) - - results = test({'in1': [[1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [0.3850506544113159]) - results = test({'in1': [1, 2, 2, 4, 5], 'in2': [1, 2, 2, 4]}) - self.TestAlmostEqual(results['out'], [0.3850506544113159]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4]]}, sampled=True) - self.TestAlmostEqual(results['out'], [0.3850506544113159]) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [5, 5, 5, 5]]}, sampled=True) - self.TestAlmostEqual(results['out'], [0.3850506544113159, 1.446676254272461]) + test({"in1": [[1, 2, 2, 4]]}) + + results = test({"in1": [1, 2, 2, 4], "in2": [1, 2, 2, 4]}) + self.TestAlmostEqual(results["out"], [0.3850506544113159]) + results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True) + self.TestAlmostEqual(results["out"], [0.3850506544113159]) + results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]}) + self.TestAlmostEqual(results["out"], [0.3850506544113159]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True + ) + self.TestAlmostEqual(results["out"], [0.3850506544113159]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]}, + sampled=True, + ) + self.TestAlmostEqual(results["out"], [0.3850506544113159, 1.446676254272461]) + + results = test({"in1": [[1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True) + self.TestAlmostEqual(results["out"], [0.3850506544113159]) + results = test({"in1": [1, 2, 2, 4, 5], "in2": [1, 2, 2, 4]}) + self.TestAlmostEqual(results["out"], [0.3850506544113159]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4]]}, sampled=True + ) + self.TestAlmostEqual(results["out"], [0.3850506544113159]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [5, 5, 5, 5]]}, + sampled=True, + ) + self.TestAlmostEqual(results["out"], [0.3850506544113159, 1.446676254272461]) NeuObj.clearNames() - in1 = Input('in1') - in2 = Input('in2') - k1 = Parameter('k1', dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) - k_fir = Parameter('k_fir', dimensions=3, tw=0.4, - values=[[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]]) - out = Output('out', Fir(3, W=k_fir)(ParamFun(myfun2, parameters_and_constants=[k1])(in1.tw(0.4), in2.tw(0.4)))) + in1 = Input("in1") + in2 = Input("in2") + k1 = Parameter("k1", dimensions=1, tw=0.4, values=[[1.0], [1.0], [1.0], [1.0]]) + k_fir = Parameter( + "k_fir", + dimensions=3, + tw=0.4, + values=[[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], + ) + out = Output( + "out", + Fir(3, W=k_fir)( + ParamFun(myfun2, parameters_and_constants=[k1])( + in1.tw(0.4), in2.tw(0.4) + ) + ), + ) test = Modely(visualizer=None) - test.addModel('out', out) + test.addModel("out", out) test.neuralizeModel(0.1) - results = test({'in1': [[1, 2, 2, 4], [1, 2, 2, 4]], 'in2': [[1, 2, 2, 4], [1, 2, 2, 4]]}, sampled=True) - self.assertEqual((2, 1, 3), np.array(results['out']).shape) - self.TestAlmostEqual(results['out'], [[[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]], - [[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]]]) + results = test( + {"in1": [[1, 2, 2, 4], [1, 2, 2, 4]], "in2": [[1, 2, 2, 4], [1, 2, 2, 4]]}, + sampled=True, + ) + self.assertEqual((2, 1, 3), np.array(results["out"]).shape) + self.TestAlmostEqual( + results["out"], + [ + [[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]], + [[0.3850506544113159, 0.3850506544113159, 0.3850506544113159]], + ], + ) parfun = ParamFun(myfun2) with self.assertRaises(TypeError): - Output('out', parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4))))) + Output("out", parfun(Fir(3)(parfun(in1.tw(0.4), in2.tw(0.4))))) parfun = ParamFun(myfun2) - out = Output('out2', parfun(Fir(3)(parfun(in1.tw(0.4))))) + out = Output("out2", parfun(Fir(3)(parfun(in1.tw(0.4))))) test = Modely(visualizer=None, seed=42) - test.addModel('out', out) + test.addModel("out", out) test.neuralizeModel(0.1) - results = test({'in1': [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True) - self.assertEqual((2, 1, 3), np.array(results['out2']).shape) - self.TestAlmostEqual(results['out2'], [[[0.790096640586853, 0.7821592688560486, 0.8219361901283264]], - [[0.8505557179450989, 0.7224123477935791, 0.77630215883255]]]) - - results = test({'in1': [1, 2, 2, 4, 3], 'in2': [6, 2, 2, 4, 4]}) - self.assertEqual((2, 1, 3), np.array(results['out2']).shape) - self.TestAlmostEqual(results['out2'], [[[0.790096640586853, 0.7821592688560486, 0.8219361901283264]], - [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]]]) - results = test({'in1': [[1, 2, 2, 4], [2, 2, 4, 3]], 'in2': [[6, 2, 2, 4], [2, 2, 4, 4]]}, sampled=True) - self.assertEqual((2, 1, 3), np.array(results['out2']).shape) - self.TestAlmostEqual(results['out2'], [[[0.790096640586853, 0.7821592688560486, 0.8219361901283264]], - [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]]]) + results = test({"in1": [[1, 2, 2, 4], [2, 1, 1, 3]]}, sampled=True) + self.assertEqual((2, 1, 3), np.array(results["out2"]).shape) + self.TestAlmostEqual( + results["out2"], + [ + [[0.790096640586853, 0.7821592688560486, 0.8219361901283264]], + [[0.8505557179450989, 0.7224123477935791, 0.77630215883255]], + ], + ) + + results = test({"in1": [1, 2, 2, 4, 3], "in2": [6, 2, 2, 4, 4]}) + self.assertEqual((2, 1, 3), np.array(results["out2"]).shape) + self.TestAlmostEqual( + results["out2"], + [ + [[0.790096640586853, 0.7821592688560486, 0.8219361901283264]], + [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]], + ], + ) + results = test( + {"in1": [[1, 2, 2, 4], [2, 2, 4, 3]], "in2": [[6, 2, 2, 4], [2, 2, 4, 4]]}, + sampled=True, + ) + self.assertEqual((2, 1, 3), np.array(results["out2"]).shape) + self.TestAlmostEqual( + results["out2"], + [ + [[0.790096640586853, 0.7821592688560486, 0.8219361901283264]], + [[-0.09300854057073593, 0.44719645380973816, -0.03044888749718666]], + ], + ) def test_trigonometri_parameter_and_numeric_constant(self): NeuObj.clearNames() - in1 = Input('in1').last() - par = Parameter('par', values=5) - in4 = Input('in4', dimensions=4).last() - par4 = Parameter('par4', values=[1,2,3,4]) + in1 = Input("in1").last() + par = Parameter("par", values=5) + in4 = Input("in4", dimensions=4).last() + par4 = Parameter("par4", values=[1, 2, 3, 4]) add = in1 + par + 5.2 sub = in1 - par - 5.2 mul = in1 * par * 5.2 div = in1 / par / 5.2 - pow = in1 ** par ** 2 + pow = in1**par**2 sin1 = Sin(par) + Sin(5.2) cos1 = Cos(par) + Cos(5.2) tan1 = Tan(par) + Tan(5.2) @@ -726,17 +997,17 @@ def test_trigonometri_parameter_and_numeric_constant(self): tot1 = add + sub + mul + div + pow + sin1 + cos1 + tan1 + relu1 + tanh1 add = 5.2 + in1 + (3 + par) - sub = - 5.2 - par + (3 - in1) + sub = -5.2 - par + (3 - in1) mul = 5.2 * in1 * (2 * par) div = 5.2 / in1 / (3 / par) - pow = (0.2 ** in1) + (2 ** par) + pow = (0.2**in1) + (2**par) tot11 = add + sub + mul + div + pow add4 = in4 + par4 + 5.2 sub4 = in4 - par4 - 5.2 mul4 = in4 * par4 * 5.2 div4 = in4 / par4 / 5.2 - pow4 = in4 ** par4 ** 2 + pow4 = in4**par4**2 sin4 = Sin(par4) + Sin(5.2) cos4 = Cos(par4) + Cos(5.2) tan4 = Tan(par4) + Tan(5.2) @@ -745,92 +1016,124 @@ def test_trigonometri_parameter_and_numeric_constant(self): tot4 = add4 + sub4 + mul4 + div4 + pow4 + sin4 + cos4 + tan4 + relu4 + tanh4 add4 = 5.2 + in4 + (3 + par4) - sub4 = - 5.2 - par4 + (3 - in4) + sub4 = -5.2 - par4 + (3 - in4) mul4 = 5.2 * in4 * (2 * par4) div4 = 5.2 / in4 / (3 / par4) - pow4 = (0.2 ** in4) + (2 ** par4) + pow4 = (0.2**in4) + (2**par4) tot41 = add4 + sub4 + mul4 + div4 + pow4 - out1 = Output('out1', tot1) - out11 = Output('out11', tot11) - out4 = Output('out4', tot4) - out41 = Output('out41', tot41) + out1 = Output("out1", tot1) + out11 = Output("out11", tot11) + out4 = Output("out4", tot4) + out41 = Output("out41", tot41) - linW = Parameter('linW', dimensions=(4,1),values=[[1],[1],[1],[1]]) - outtot = Output('outtot', tot1 + Linear(W=linW)(tot4)) + linW = Parameter("linW", dimensions=(4, 1), values=[[1], [1], [1], [1]]) + outtot = Output("outtot", tot1 + Linear(W=linW)(tot4)) test = Modely(visualizer=None, seed=1) - test.addModel('out',[out1,out4,outtot,out11,out41]) + test.addModel("out", [out1, out4, outtot, out11, out41]) test.neuralizeModel() - results = test({'in1': [1, 2, -2],'in4': [[6, 2, 2, 4], [7, 2, 2, 4], [-6, -5, 5, 4]]}) - self.assertEqual((3,), np.array(results['out1']).shape) - self.assertEqual((3,1,4), np.array(results['out4']).shape) - self.assertEqual((3,), np.array(results['outtot']).shape) - - self.TestAlmostEqual([34.8819529, 33554496.0, -33554480.0], results['out1'] ) - self.TestAlmostEqual([[[58.9539756, 46.1638031, 554.231201171875, 4294967296.0]], [[67.3462829589843, 46.16380310058594, 554.231201171875, 4294967296.0]], [[ -41.75371170043945, 567.6907348632812, 1953220.375, 4294967296.0]]], results['out4']) - self.TestAlmostEqual([4294967808.0, 4328522240.0, 4263366656.0], results['outtot']) - - self.assertEqual((3,), np.array(results['out11']).shape) - self.assertEqual((3,1,4), np.array(results['out41']).shape) - - self.TestAlmostEqual([98.86666667, 146.37333333, -45.33333333], results['out11']) - self.TestAlmostEqual([[[ 70.68895289, 53.37333333, 79.04 , 190.13493333]], - [[ 81.04763185, 53.37333333, 79.04 , 190.13493333]], - [[15570.31111111, 3030.30666667, 171.04032 , 190.13493333]]], results['out41'],precision=2) + results = test( + {"in1": [1, 2, -2], "in4": [[6, 2, 2, 4], [7, 2, 2, 4], [-6, -5, 5, 4]]} + ) + self.assertEqual((3,), np.array(results["out1"]).shape) + self.assertEqual((3, 1, 4), np.array(results["out4"]).shape) + self.assertEqual((3,), np.array(results["outtot"]).shape) + + self.TestAlmostEqual([34.8819529, 33554496.0, -33554480.0], results["out1"]) + self.TestAlmostEqual( + [ + [[58.9539756, 46.1638031, 554.231201171875, 4294967296.0]], + [[67.3462829589843, 46.16380310058594, 554.231201171875, 4294967296.0]], + [[-41.75371170043945, 567.6907348632812, 1953220.375, 4294967296.0]], + ], + results["out4"], + ) + self.TestAlmostEqual( + [4294967808.0, 4328522240.0, 4263366656.0], results["outtot"] + ) + + self.assertEqual((3,), np.array(results["out11"]).shape) + self.assertEqual((3, 1, 4), np.array(results["out41"]).shape) + + self.TestAlmostEqual( + [98.86666667, 146.37333333, -45.33333333], results["out11"] + ) + self.TestAlmostEqual( + [ + [[70.68895289, 53.37333333, 79.04, 190.13493333]], + [[81.04763185, 53.37333333, 79.04, 190.13493333]], + [[15570.31111111, 3030.30666667, 171.04032, 190.13493333]], + ], + results["out41"], + precision=2, + ) def test_check_modify_stream(self): NeuObj.clearNames() - in1 = Input('in1').last() - par = Parameter('par', values=5) - add1 = in1 + par # 1 + 5 = 6 - add2 = add1 + 5.2 # 6 + 5.2 = 11.2 - tot1 = add1 + add2 # 6 + 11.2 = 17.2 - out1 = Output('out1', tot1) # = 17.2 - tot2 = add1 + in1 # 6 + 1 = 7 - out12 = Output('out12', tot1 + tot2) # = 24.2 - out2 = Output('out2', tot2) #= 7 - test = Modely(visualizer=None,seed=1) - test.addModel('out',[out1,out12,out2]) + in1 = Input("in1").last() + par = Parameter("par", values=5) + add1 = in1 + par # 1 + 5 = 6 + add2 = add1 + 5.2 # 6 + 5.2 = 11.2 + tot1 = add1 + add2 # 6 + 11.2 = 17.2 + out1 = Output("out1", tot1) # = 17.2 + tot2 = add1 + in1 # 6 + 1 = 7 + out12 = Output("out12", tot1 + tot2) # = 24.2 + out2 = Output("out2", tot2) # = 7 + test = Modely(visualizer=None, seed=1) + test.addModel("out", [out1, out12, out2]) test.neuralizeModel() - results = test({'in1': [1]}) - self.assertEqual((1,), np.array(results['out1']).shape) - self.TestAlmostEqual([17.2], results['out1'] ) - self.TestAlmostEqual([24.2], results['out12']) - self.TestAlmostEqual([7], results['out2']) - + results = test({"in1": [1]}) + self.assertEqual((1,), np.array(results["out1"]).shape) + self.TestAlmostEqual([17.2], results["out1"]) + self.TestAlmostEqual([24.2], results["out12"]) + self.TestAlmostEqual([7], results["out2"]) def test_parameter_and_linear(self): NeuObj.clearNames() - input = Input('in1').last() - W15 = Parameter('W15', dimensions=(1, 5), values=[[1,2,3,4,5]]) - b15 = Parameter('b15', dimensions=5, values=[1,2,3,4,5]) - input4 = Input('in4', dimensions=4).last() - W45 = Parameter('W45', dimensions=(4, 5), values=[[1,2,3,4,5],[5,3,3,4,5],[1,2,3,4,7],[-8,2,3,4,5]]) - b45 = Parameter('b45', dimensions=5, values=[5,2,3,4,5]) - - o = Output('out' , Linear(input) + Linear(input4)) - o3 = Output('out3' , Linear(3)(input) + Linear(3)(input4)) - oW = Output('outW' , Linear(W = W15)(input) + Linear(W = W45)(input4)) - oWb = Output('outWb' , Linear(W = W15,b = b15)(input) + Linear(W = W45, b = b45)(input4)) + input = Input("in1").last() + W15 = Parameter("W15", dimensions=(1, 5), values=[[1, 2, 3, 4, 5]]) + b15 = Parameter("b15", dimensions=5, values=[1, 2, 3, 4, 5]) + input4 = Input("in4", dimensions=4).last() + W45 = Parameter( + "W45", + dimensions=(4, 5), + values=[ + [1, 2, 3, 4, 5], + [5, 3, 3, 4, 5], + [1, 2, 3, 4, 7], + [-8, 2, 3, 4, 5], + ], + ) + b45 = Parameter("b45", dimensions=5, values=[5, 2, 3, 4, 5]) + + o = Output("out", Linear(input) + Linear(input4)) + o3 = Output("out3", Linear(3)(input) + Linear(3)(input4)) + oW = Output("outW", Linear(W=W15)(input) + Linear(W=W45)(input4)) + oWb = Output( + "outWb", Linear(W=W15, b=b15)(input) + Linear(W=W45, b=b45)(input4) + ) n = Modely(visualizer=None, seed=1) - n.addModel('out',[o,o3,oW,oWb]) - #n.addModel('out', [oW]) + n.addModel("out", [o, o3, oW, oWb]) + # n.addModel('out', [oW]) n.neuralizeModel() - results = n({'in1': [1, 2], 'in4': [[6, 2, 2, 4], [7, 2, 2, 4]]}) - #self.assertEqual((2,), np.array(results['out']).shape) - #self.TestAlmostEqual([9.274794578552246,10.3853759765625], results['out']) - #self.assertEqual((2,1,3), np.array(results['out3']).shape) - #self.TestAlmostEqual([[[9.247159004211426, 6.103044033050537,7.719359397888184]],[[10.68740463256836, 6.687504291534424,8.585973739624023]]], results['out3']) - #W15 = torch.tensor([[1,2,3,4,5]]) - #in1 = torch.tensor([[1, 2]]) - #W45 = torch.tensor([[1, 2, 3, 4, 5], [5, 3, 3, 4, 5], [1, 2, 3, 4, 7], [-8, 2, 3, 4, 5]]) - #in4 = torch.tensor([[6, 2, 2, 4], [7, 2, 2, 4]]) - #torch.matmul(W45.t(),in4.t())+torch.matmul(W15.t(),in1) - self.assertEqual((2, 1, 5), np.array(results['outW']).shape) - self.TestAlmostEqual([[[-13.0,32.0,45.0,60.0,79.0]],[[-11.0,36.0,51.,68.,89.]]], results['outW']) + results = n({"in1": [1, 2], "in4": [[6, 2, 2, 4], [7, 2, 2, 4]]}) + # self.assertEqual((2,), np.array(results['out']).shape) + # self.TestAlmostEqual([9.274794578552246,10.3853759765625], results['out']) + # self.assertEqual((2,1,3), np.array(results['out3']).shape) + # self.TestAlmostEqual([[[9.247159004211426, 6.103044033050537,7.719359397888184]],[[10.68740463256836, 6.687504291534424,8.585973739624023]]], results['out3']) + # W15 = torch.tensor([[1,2,3,4,5]]) + # in1 = torch.tensor([[1, 2]]) + # W45 = torch.tensor([[1, 2, 3, 4, 5], [5, 3, 3, 4, 5], [1, 2, 3, 4, 7], [-8, 2, 3, 4, 5]]) + # in4 = torch.tensor([[6, 2, 2, 4], [7, 2, 2, 4]]) + # torch.matmul(W45.t(),in4.t())+torch.matmul(W15.t(),in1) + self.assertEqual((2, 1, 5), np.array(results["outW"]).shape) + self.TestAlmostEqual( + [[[-13.0, 32.0, 45.0, 60.0, 79.0]], [[-11.0, 36.0, 51.0, 68.0, 89.0]]], + results["outW"], + ) # W15 = torch.tensor([[1,2,3,4,5]]) # b15 = torch.tensor([[5, 2, 3, 4, 5]]) # in1 = torch.tensor([[1, 2]]) @@ -838,107 +1141,247 @@ def test_parameter_and_linear(self): # b45 = torch.tensor([[1, 2, 3, 4, 5]]) # in4 = torch.tensor([[6, 2, 2, 4], [7, 2, 2, 4]]) # oo = torch.matmul(W45.t(),in4.t())+b45.t()+torch.matmul(W15.t(),in1)+b15.t() - self.assertEqual((2, 1, 5), np.array(results['outWb']).shape) - self.TestAlmostEqual([[[-7, 36, 51, 68, 89]],[[-5, 40, 57, 76, 99]]], results['outWb']) + self.assertEqual((2, 1, 5), np.array(results["outWb"]).shape) + self.TestAlmostEqual( + [[[-7, 36, 51, 68, 89]], [[-5, 40, 57, 76, 99]]], results["outWb"] + ) NeuObj.clearNames() - input2 = Input('in1').sw([-1,1]) - input42 = Input('in4', dimensions=4).sw([-1,1]) - - o = Output('out' , Linear(input2) + Linear(input42)) - o3 = Output('out3' , Linear(3)(input2) + Linear(3)(input42)) - oW = Output('outW' , Linear(W = W15)(input2) + Linear(W = W45)(input42)) - oWb = Output('outWb' , Linear(W = W15,b = b15)(input2) + Linear(W = W45, b = b45)(input42)) + input2 = Input("in1").sw([-1, 1]) + input42 = Input("in4", dimensions=4).sw([-1, 1]) + + o = Output("out", Linear(input2) + Linear(input42)) + o3 = Output("out3", Linear(3)(input2) + Linear(3)(input42)) + oW = Output("outW", Linear(W=W15)(input2) + Linear(W=W45)(input42)) + oWb = Output( + "outWb", Linear(W=W15, b=b15)(input2) + Linear(W=W45, b=b45)(input42) + ) n = Modely(visualizer=None) - n.addModel('out',[o,o3,oW,oWb]) + n.addModel("out", [o, o3, oW, oWb]) n.neuralizeModel() - results = n({'in1': [1, 2], 'in4': [[6, 2, 2, 4], [7, 2, 2, 4]]}) - self.assertEqual((1, 2), np.array(results['out']).shape) - self.TestAlmostEqual([[7.3881096839904785, 7.91458797454834]], results['out']) - self.assertEqual((1, 2, 3), np.array(results['out3']).shape) - self.TestAlmostEqual([[[8.117439270019531, 6.014362812042236, 7.489190578460693], - [9.261265754699707, 6.2568135261535645, 7.929978370666504]]], results['out3']) - self.assertEqual((1, 2, 5), np.array(results['outW']).shape) - self.TestAlmostEqual([[[-13.0, 32.0, 45.0, 60.0, 79.0], [-11.0, 36.0, 51., 68., 89.]]], results['outW']) - self.assertEqual((1, 2, 5), np.array(results['outWb']).shape) - self.TestAlmostEqual([[[-7, 36, 51, 68, 89], [-5, 40, 57, 76, 99]]], results['outWb']) + results = n({"in1": [1, 2], "in4": [[6, 2, 2, 4], [7, 2, 2, 4]]}) + self.assertEqual((1, 2), np.array(results["out"]).shape) + self.TestAlmostEqual([[7.3881096839904785, 7.91458797454834]], results["out"]) + self.assertEqual((1, 2, 3), np.array(results["out3"]).shape) + self.TestAlmostEqual( + [ + [ + [8.117439270019531, 6.014362812042236, 7.489190578460693], + [9.261265754699707, 6.2568135261535645, 7.929978370666504], + ] + ], + results["out3"], + ) + self.assertEqual((1, 2, 5), np.array(results["outW"]).shape) + self.TestAlmostEqual( + [[[-13.0, 32.0, 45.0, 60.0, 79.0], [-11.0, 36.0, 51.0, 68.0, 89.0]]], + results["outW"], + ) + self.assertEqual((1, 2, 5), np.array(results["outWb"]).shape) + self.TestAlmostEqual( + [[[-7, 36, 51, 68, 89], [-5, 40, 57, 76, 99]]], results["outWb"] + ) def test_initialization(self): NeuObj.clearNames() - input = Input('in1') - W = Parameter('W', dimensions=(1,1), init='init_constant') - b = Parameter('b', dimensions=1, init='init_constant') - o = Output('out', Linear(W=W,b=b)(input.last())) - - W5 = Parameter('W5', dimensions=(1,1), init='init_constant', init_params={'value':5}) - b2 = Parameter('b2', dimensions=1, init='init_constant', init_params={'value':2}) - o52 = Output('out52', Linear(W=W5,b=b2)(input.last())) - - par = Parameter('par', dimensions=3, sw=2, init='init_constant') - opar = Output('outpar', Fir(W=par)(input.sw(2))) - - par2 = Parameter('par2', dimensions=3, sw=2, init='init_constant', init_params={'value':2}) - opar2 = Output('outpar2', Fir(W=par2, b=False)(input.sw(2))) - - ol = Output('outl', Linear(output_dimension=1,b=True,W_init='init_constant',b_init='init_constant')(input.last())) - ol52 = Output('outl52', Linear(output_dimension=1,b=True,W_init='init_constant',b_init='init_constant',W_init_params={'value':5},b_init_params={'value':2})(input.last())) - ofpar = Output('outfpar', Fir(output_dimension=3,W_init='init_constant')(input.sw(2))) - ofpar2 = Output('outfpar2', Fir(output_dimension=3,W_init='init_constant',W_init_params={'value':2})(input.sw(2))) + input = Input("in1") + W = Parameter("W", dimensions=(1, 1), init="init_constant") + b = Parameter("b", dimensions=1, init="init_constant") + o = Output("out", Linear(W=W, b=b)(input.last())) + + W5 = Parameter( + "W5", dimensions=(1, 1), init="init_constant", init_params={"value": 5} + ) + b2 = Parameter( + "b2", dimensions=1, init="init_constant", init_params={"value": 2} + ) + o52 = Output("out52", Linear(W=W5, b=b2)(input.last())) + + par = Parameter("par", dimensions=3, sw=2, init="init_constant") + opar = Output("outpar", Fir(W=par)(input.sw(2))) + + par2 = Parameter( + "par2", dimensions=3, sw=2, init="init_constant", init_params={"value": 2} + ) + opar2 = Output("outpar2", Fir(W=par2, b=False)(input.sw(2))) + + ol = Output( + "outl", + Linear( + output_dimension=1, + b=True, + W_init="init_constant", + b_init="init_constant", + )(input.last()), + ) + ol52 = Output( + "outl52", + Linear( + output_dimension=1, + b=True, + W_init="init_constant", + b_init="init_constant", + W_init_params={"value": 5}, + b_init_params={"value": 2}, + )(input.last()), + ) + ofpar = Output( + "outfpar", Fir(output_dimension=3, W_init="init_constant")(input.sw(2)) + ) + ofpar2 = Output( + "outfpar2", + Fir(output_dimension=3, W_init="init_constant", W_init_params={"value": 2})( + input.sw(2) + ), + ) + + outnegexp = Output( + "outnegexp", Fir(output_dimension=3, W_init="init_negexp")(input.sw(2)) + ) + outnegexp2 = Output( + "outnegexp2", + Fir( + output_dimension=3, + W_init="init_negexp", + W_init_params={"size_index": 1, "first_value": 3, "lambda": 1}, + )(input.sw(2)), + ) + + outexp = Output( + "outexp", Fir(output_dimension=3, W_init="init_exp")(input.sw(2)) + ) + outexp2 = Output( + "outexp2", + Fir( + output_dimension=3, + W_init="init_exp", + W_init_params={ + "size_index": 1, + "max_value": 2, + "lambda": 2, + "monotonicity": "increasing", + }, + )(input.sw(2)), + ) + outexp2D = Output( + "outexp2D", + Fir( + output_dimension=3, + W_init="init_exp", + W_init_params={ + "size_index": 1, + "max_value": 2, + "lambda": 2, + "monotonicity": "decreasing", + }, + )(input.sw(2)), + ) + + outlin = Output( + "outlin", Fir(output_dimension=3, W_init="init_lin")(input.sw(2)) + ) + outlin2 = Output( + "outlin2", + Fir( + output_dimension=3, + W_init="init_lin", + W_init_params={"size_index": 1, "first_value": 4, "last_value": 5}, + )(input.sw(2)), + ) - outnegexp = Output('outnegexp', Fir(output_dimension=3,W_init='init_negexp')(input.sw(2))) - outnegexp2 = Output('outnegexp2', Fir(output_dimension=3,W_init='init_negexp',W_init_params={'size_index':1, 'first_value':3, 'lambda':1})(input.sw(2))) - - outexp = Output('outexp', Fir(output_dimension=3,W_init='init_exp')(input.sw(2))) - outexp2 = Output('outexp2', Fir(output_dimension=3,W_init='init_exp',W_init_params={'size_index':1, 'max_value':2, 'lambda':2, 'monotonicity':'increasing'})(input.sw(2))) - outexp2D = Output('outexp2D', Fir(output_dimension=3,W_init='init_exp',W_init_params={'size_index':1, 'max_value':2, 'lambda':2, 'monotonicity':'decreasing'})(input.sw(2))) - - outlin = Output('outlin', Fir(output_dimension=3,W_init='init_lin')(input.sw(2))) - outlin2 = Output('outlin2', Fir(output_dimension=3,W_init='init_lin',W_init_params={'size_index':1, 'first_value':4, 'last_value':5})(input.sw(2))) - - n = Modely(visualizer=None,seed=1) - n.addModel('model',[o,o52,opar,opar2,ol,ol52,ofpar,ofpar2,outnegexp,outnegexp2,outexp,outexp2,outexp2D,outlin,outlin2]) + n = Modely(visualizer=None, seed=1) + n.addModel( + "model", + [ + o, + o52, + opar, + opar2, + ol, + ol52, + ofpar, + ofpar2, + outnegexp, + outnegexp2, + outexp, + outexp2, + outexp2D, + outlin, + outlin2, + ], + ) n.neuralizeModel() - results = n({'in1': [1, 1, 2]}) - self.assertEqual((2,), np.array(results['out']).shape) - self.TestAlmostEqual([2,3], results['out']) - self.assertEqual((2,), np.array(results['out52']).shape) - self.TestAlmostEqual([7,12], results['out52']) - - self.assertEqual((2,1,3), np.array(results['outpar']).shape) - self.TestAlmostEqual([[[2,2,2]],[[3,3,3]]], results['outpar']) - self.assertEqual((2,1,3), np.array(results['outpar2']).shape) - self.TestAlmostEqual([[[4,4,4]],[[6,6,6]]], results['outpar2']) - - self.assertEqual((2,), np.array(results['outl']).shape) - self.TestAlmostEqual([2,3], results['outl']) - self.assertEqual((2,), np.array(results['outl52']).shape) - self.TestAlmostEqual([7.0,12.0], results['outl52']) - - self.assertEqual((2,1,3), np.array(results['outfpar']).shape) - self.TestAlmostEqual([[[2,2,2]],[[3,3,3]]], results['outfpar']) - self.assertEqual((2,1,3), np.array(results['outfpar2']).shape) - self.TestAlmostEqual([[[4,4,4]],[[6,6,6]]], results['outfpar2']) - - self.assertEqual((2,1,3), np.array(results['outnegexp']).shape) - self.TestAlmostEqual([[[1.0497870445251465,1.0497870445251465,1.0497870445251465]],[[2.0497870445251465,2.0497870445251465,2.0497870445251465]]], results['outnegexp']) - self.assertEqual((2,1,3), np.array(results['outnegexp2']).shape) - self.TestAlmostEqual([[[2.2072765827178955,3.63918399810791,6.0]],[[3.310914993286133,5.458775997161865,9.0]]], results['outnegexp2']) - - self.assertEqual((2,1,3), np.array(results['outexp']).shape) - self.TestAlmostEqual([[[1.0497870445251465,1.0497870445251465,1.0497870445251465]],[[1.099574089050293,1.099574089050293,1.099574089050293]]], results['outexp']) - self.assertEqual((2,1,3), np.array(results['outexp2']).shape) - self.TestAlmostEqual([[[0.5413411259651184,1.47151780128479,4]],[[0.81201171875,2.2072768211364746,6]]], results['outexp2']) - self.assertEqual((2,1,3), np.array(results['outexp2D']).shape) - self.TestAlmostEqual([[[4.0,1.47151780128479,0.5413411259651184]],[[6,2.2072768211364746,0.81201171875]]], results['outexp2D']) - - self.assertEqual((2,1,3), np.array(results['outlin']).shape) - self.TestAlmostEqual([[[1,1,1]],[[1,1,1]]], results['outlin']) - self.assertEqual((2,1,3), np.array(results['outlin2']).shape) - self.TestAlmostEqual([[[8,9,10]],[[12,13.5,15.0]]], results['outlin2']) + results = n({"in1": [1, 1, 2]}) + self.assertEqual((2,), np.array(results["out"]).shape) + self.TestAlmostEqual([2, 3], results["out"]) + self.assertEqual((2,), np.array(results["out52"]).shape) + self.TestAlmostEqual([7, 12], results["out52"]) + + self.assertEqual((2, 1, 3), np.array(results["outpar"]).shape) + self.TestAlmostEqual([[[2, 2, 2]], [[3, 3, 3]]], results["outpar"]) + self.assertEqual((2, 1, 3), np.array(results["outpar2"]).shape) + self.TestAlmostEqual([[[4, 4, 4]], [[6, 6, 6]]], results["outpar2"]) + + self.assertEqual((2,), np.array(results["outl"]).shape) + self.TestAlmostEqual([2, 3], results["outl"]) + self.assertEqual((2,), np.array(results["outl52"]).shape) + self.TestAlmostEqual([7.0, 12.0], results["outl52"]) + + self.assertEqual((2, 1, 3), np.array(results["outfpar"]).shape) + self.TestAlmostEqual([[[2, 2, 2]], [[3, 3, 3]]], results["outfpar"]) + self.assertEqual((2, 1, 3), np.array(results["outfpar2"]).shape) + self.TestAlmostEqual([[[4, 4, 4]], [[6, 6, 6]]], results["outfpar2"]) + + self.assertEqual((2, 1, 3), np.array(results["outnegexp"]).shape) + self.TestAlmostEqual( + [ + [[1.0497870445251465, 1.0497870445251465, 1.0497870445251465]], + [[2.0497870445251465, 2.0497870445251465, 2.0497870445251465]], + ], + results["outnegexp"], + ) + self.assertEqual((2, 1, 3), np.array(results["outnegexp2"]).shape) + self.TestAlmostEqual( + [ + [[2.2072765827178955, 3.63918399810791, 6.0]], + [[3.310914993286133, 5.458775997161865, 9.0]], + ], + results["outnegexp2"], + ) + + self.assertEqual((2, 1, 3), np.array(results["outexp"]).shape) + self.TestAlmostEqual( + [ + [[1.0497870445251465, 1.0497870445251465, 1.0497870445251465]], + [[1.099574089050293, 1.099574089050293, 1.099574089050293]], + ], + results["outexp"], + ) + self.assertEqual((2, 1, 3), np.array(results["outexp2"]).shape) + self.TestAlmostEqual( + [ + [[0.5413411259651184, 1.47151780128479, 4]], + [[0.81201171875, 2.2072768211364746, 6]], + ], + results["outexp2"], + ) + self.assertEqual((2, 1, 3), np.array(results["outexp2D"]).shape) + self.TestAlmostEqual( + [ + [[4.0, 1.47151780128479, 0.5413411259651184]], + [[6, 2.2072768211364746, 0.81201171875]], + ], + results["outexp2D"], + ) + + self.assertEqual((2, 1, 3), np.array(results["outlin"]).shape) + self.TestAlmostEqual([[[1, 1, 1]], [[1, 1, 1]]], results["outlin"]) + self.assertEqual((2, 1, 3), np.array(results["outlin2"]).shape) + self.TestAlmostEqual([[[8, 9, 10]], [[12, 13.5, 15.0]]], results["outlin2"]) def test_sample_part_and_select(self): NeuObj.clearNames() - in1 = Input('in1') + in1 = Input("in1") # Offset before the sample window with self.assertRaises(IndexError): in1.sw([-5, -2], offset=-6) @@ -952,25 +1395,25 @@ def test_sample_part_and_select(self): with self.assertRaises(IndexError): in1.sw([0, 3], offset=3) - sw3,sw32 = in1.sw([-5, -2], offset=-4), in1.sw([0, 3], offset=0) - out_sw3 = Output('in_sw3', sw3) - out_sw32 = Output('in_sw32', sw32) - #Get after the window + sw3, sw32 = in1.sw([-5, -2], offset=-4), in1.sw([0, 3], offset=0) + out_sw3 = Output("in_sw3", sw3) + out_sw32 = Output("in_sw32", sw32) + # Get after the window with self.assertRaises(ValueError): SamplePart(sw3, 0, 4) - #Empty sample window + # Empty sample window with self.assertRaises(ValueError): SamplePart(sw3, 0, 0) - #Get before the sample window + # Get before the sample window with self.assertRaises(ValueError): SamplePart(sw3, -1, 0) # Get after the window with self.assertRaises(ValueError): SamplePart(sw32, 0, 4) - #Empty sample window + # Empty sample window with self.assertRaises(ValueError): SamplePart(sw32, 0, 0) - #Get before the sample window + # Get before the sample window with self.assertRaises(ValueError): SamplePart(sw32, -1, 0) @@ -986,13 +1429,13 @@ def test_sample_part_and_select(self): # Offset after the sample window with self.assertRaises(IndexError): SamplePart(sw32, 0, 3, offset=3) - in_SP1first = Output('in_SP1first', SamplePart(sw3, 0, 1)) - in_SP1mid = Output('in_SP1mid', SamplePart(sw3, 1, 2)) - in_SP1last = Output('in_SP1last', SamplePart(sw3, 2, 3)) - in_SP1all = Output('in_SP1all', SamplePart(sw3, 0, 3)) - in_SP1off1 = Output('in_SP1off1', SamplePart(sw3, 0, 3, offset=0)) - in_SP1off2 = Output('in_SP1off2', SamplePart(sw3, 0, 3, offset=1)) - in_SP1off3 = Output('in_SP1off3', SamplePart(sw3, 0, 3, offset=2)) + in_SP1first = Output("in_SP1first", SamplePart(sw3, 0, 1)) + in_SP1mid = Output("in_SP1mid", SamplePart(sw3, 1, 2)) + in_SP1last = Output("in_SP1last", SamplePart(sw3, 2, 3)) + in_SP1all = Output("in_SP1all", SamplePart(sw3, 0, 3)) + in_SP1off1 = Output("in_SP1off1", SamplePart(sw3, 0, 3, offset=0)) + in_SP1off2 = Output("in_SP1off2", SamplePart(sw3, 0, 3, offset=1)) + in_SP1off3 = Output("in_SP1off3", SamplePart(sw3, 0, 3, offset=2)) with self.assertRaises(ValueError): SampleSelect(sw3, -1) with self.assertRaises(ValueError): @@ -1001,75 +1444,89 @@ def test_sample_part_and_select(self): SampleSelect(sw32, -1) with self.assertRaises(ValueError): SampleSelect(sw32, 3) - in_SS1 = Output('in_SS1', SampleSelect(sw3, 0)) - in_SS2 = Output('in_SS2', SampleSelect(sw3, 1)) - in_SS3 = Output('in_SS3', SampleSelect(sw3, 2)) + in_SS1 = Output("in_SS1", SampleSelect(sw3, 0)) + in_SS2 = Output("in_SS2", SampleSelect(sw3, 1)) + in_SS3 = Output("in_SS3", SampleSelect(sw3, 2)) test = Modely(visualizer=None) - test.addModel('out',[out_sw3, out_sw32, - in_SP1first, in_SP1mid, in_SP1last, in_SP1all, in_SP1off1, in_SP1off2, in_SP1off3, - in_SS1, in_SS2, in_SS3]) + test.addModel( + "out", + [ + out_sw3, + out_sw32, + in_SP1first, + in_SP1mid, + in_SP1last, + in_SP1all, + in_SP1off1, + in_SP1off2, + in_SP1off3, + in_SS1, + in_SS2, + in_SS3, + ], + ) test.neuralizeModel() - results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7]}) - - self.assertEqual((1, 3), np.array(results['in_sw3']).shape) - self.TestAlmostEqual([[-1,0,1]], results['in_sw3']) - self.assertEqual((1, 3), np.array(results['in_sw32']).shape) - self.TestAlmostEqual([[0,1,2]], results['in_sw32']) - - self.assertEqual((1,), np.array(results['in_SP1first']).shape) - self.TestAlmostEqual([-1], results['in_SP1first']) - self.assertEqual((1,), np.array(results['in_SP1mid']).shape) - self.TestAlmostEqual([0], results['in_SP1mid']) - self.assertEqual((1,), np.array(results['in_SP1last']).shape) - self.TestAlmostEqual([1], results['in_SP1last']) - self.assertEqual((1,3), np.array(results['in_SP1all']).shape) - self.TestAlmostEqual([[-1,0,1]], results['in_SP1all']) - self.assertEqual((1,3), np.array(results['in_SP1off1']).shape) - self.TestAlmostEqual([[0,1,2]], results['in_SP1off1']) - self.assertEqual((1,3), np.array(results['in_SP1off2']).shape) - self.TestAlmostEqual([[-1,0,1]], results['in_SP1off2']) - self.assertEqual((1,3), np.array(results['in_SP1off3']).shape) - self.TestAlmostEqual([[-2,-1,0]], results['in_SP1off3']) - - self.assertEqual((1,), np.array(results['in_SS1']).shape) - self.TestAlmostEqual([-1], results['in_SS1']) - self.assertEqual((1,), np.array(results['in_SS2']).shape) - self.TestAlmostEqual([0], results['in_SS2']) - self.assertEqual((1,), np.array(results['in_SS3']).shape) - self.TestAlmostEqual([1], results['in_SS3']) - - results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7, 10]}) - - self.assertEqual((2, 3), np.array(results['in_sw3']).shape) - self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_sw3']) - self.assertEqual((2, 3), np.array(results['in_sw32']).shape) - self.TestAlmostEqual([[0, 1, 2],[0, 1, 4]], results['in_sw32']) - - self.assertEqual((2,), np.array(results['in_SP1first']).shape) - self.TestAlmostEqual([-1,-1], results['in_SP1first']) - self.assertEqual((2,), np.array(results['in_SP1mid']).shape) - self.TestAlmostEqual([0,0], results['in_SP1mid']) - self.assertEqual((2,), np.array(results['in_SP1last']).shape) - self.TestAlmostEqual([1,1], results['in_SP1last']) - self.assertEqual((2, 3), np.array(results['in_SP1all']).shape) - self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_SP1all']) - self.assertEqual((2, 3), np.array(results['in_SP1off1']).shape) - self.TestAlmostEqual([[0, 1, 2],[0, 1, 2]], results['in_SP1off1']) - self.assertEqual((2, 3), np.array(results['in_SP1off2']).shape) - self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_SP1off2']) - self.assertEqual((2, 3), np.array(results['in_SP1off3']).shape) - self.TestAlmostEqual([[-2, -1, 0],[-2, -1, 0]], results['in_SP1off3']) - - self.assertEqual((2,), np.array(results['in_SS1']).shape) - self.TestAlmostEqual([-1,-1], results['in_SS1']) - self.assertEqual((2,), np.array(results['in_SS2']).shape) - self.TestAlmostEqual([0,0], results['in_SS2']) - self.assertEqual((2,), np.array(results['in_SS3']).shape) - self.TestAlmostEqual([1,1], results['in_SS3']) + results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7]}) + + self.assertEqual((1, 3), np.array(results["in_sw3"]).shape) + self.TestAlmostEqual([[-1, 0, 1]], results["in_sw3"]) + self.assertEqual((1, 3), np.array(results["in_sw32"]).shape) + self.TestAlmostEqual([[0, 1, 2]], results["in_sw32"]) + + self.assertEqual((1,), np.array(results["in_SP1first"]).shape) + self.TestAlmostEqual([-1], results["in_SP1first"]) + self.assertEqual((1,), np.array(results["in_SP1mid"]).shape) + self.TestAlmostEqual([0], results["in_SP1mid"]) + self.assertEqual((1,), np.array(results["in_SP1last"]).shape) + self.TestAlmostEqual([1], results["in_SP1last"]) + self.assertEqual((1, 3), np.array(results["in_SP1all"]).shape) + self.TestAlmostEqual([[-1, 0, 1]], results["in_SP1all"]) + self.assertEqual((1, 3), np.array(results["in_SP1off1"]).shape) + self.TestAlmostEqual([[0, 1, 2]], results["in_SP1off1"]) + self.assertEqual((1, 3), np.array(results["in_SP1off2"]).shape) + self.TestAlmostEqual([[-1, 0, 1]], results["in_SP1off2"]) + self.assertEqual((1, 3), np.array(results["in_SP1off3"]).shape) + self.TestAlmostEqual([[-2, -1, 0]], results["in_SP1off3"]) + + self.assertEqual((1,), np.array(results["in_SS1"]).shape) + self.TestAlmostEqual([-1], results["in_SS1"]) + self.assertEqual((1,), np.array(results["in_SS2"]).shape) + self.TestAlmostEqual([0], results["in_SS2"]) + self.assertEqual((1,), np.array(results["in_SS3"]).shape) + self.TestAlmostEqual([1], results["in_SS3"]) + + results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7, 10]}) + + self.assertEqual((2, 3), np.array(results["in_sw3"]).shape) + self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_sw3"]) + self.assertEqual((2, 3), np.array(results["in_sw32"]).shape) + self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_sw32"]) + + self.assertEqual((2,), np.array(results["in_SP1first"]).shape) + self.TestAlmostEqual([-1, -1], results["in_SP1first"]) + self.assertEqual((2,), np.array(results["in_SP1mid"]).shape) + self.TestAlmostEqual([0, 0], results["in_SP1mid"]) + self.assertEqual((2,), np.array(results["in_SP1last"]).shape) + self.TestAlmostEqual([1, 1], results["in_SP1last"]) + self.assertEqual((2, 3), np.array(results["in_SP1all"]).shape) + self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_SP1all"]) + self.assertEqual((2, 3), np.array(results["in_SP1off1"]).shape) + self.TestAlmostEqual([[0, 1, 2], [0, 1, 2]], results["in_SP1off1"]) + self.assertEqual((2, 3), np.array(results["in_SP1off2"]).shape) + self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_SP1off2"]) + self.assertEqual((2, 3), np.array(results["in_SP1off3"]).shape) + self.TestAlmostEqual([[-2, -1, 0], [-2, -1, 0]], results["in_SP1off3"]) + + self.assertEqual((2,), np.array(results["in_SS1"]).shape) + self.TestAlmostEqual([-1, -1], results["in_SS1"]) + self.assertEqual((2,), np.array(results["in_SS2"]).shape) + self.TestAlmostEqual([0, 0], results["in_SS2"]) + self.assertEqual((2,), np.array(results["in_SS3"]).shape) + self.TestAlmostEqual([1, 1], results["in_SS3"]) def test_time_part(self): NeuObj.clearNames() - in1 = Input('in1') + in1 = Input("in1") # Offset before the time window with self.assertRaises(IndexError): in1.tw([-5, -2], offset=-6) @@ -1084,8 +1541,8 @@ def test_time_part(self): in1.tw([0, 3], offset=3) tw3, tw32 = in1.tw([-5, -2], offset=-4), in1.tw([0, 3], offset=0) - out_tw3 = Output('in_tw3', tw3) - out_tw32 = Output('in_tw32', tw32) + out_tw3 = Output("in_tw3", tw3) + out_tw32 = Output("in_tw32", tw32) # Get after the window with self.assertRaises(ValueError): TimePart(tw3, 0, 4) @@ -1118,69 +1575,81 @@ def test_time_part(self): with self.assertRaises(IndexError): TimePart(tw32, 0, 3, offset=3) - in_TP1first = Output('in_TP1first', TimePart(tw32, 0, 1)) - in_TP1mid = Output('in_TP1mid', TimePart(tw32, 1, 2)) - in_TP1last = Output('in_TP1last', TimePart(tw32, 2, 3)) - in_TP1all = Output('in_TP1all', TimePart(tw32, 0, 3)) - in_TP1off1 = Output('in_TP1off1', TimePart(tw32, 0, 3, offset=0)) - in_TP1off2 = Output('in_TP1off2', TimePart(tw32, 0, 3, offset=1)) - in_TP1off3 = Output('in_TP1off3', TimePart(tw32, 0, 3, offset=2)) + in_TP1first = Output("in_TP1first", TimePart(tw32, 0, 1)) + in_TP1mid = Output("in_TP1mid", TimePart(tw32, 1, 2)) + in_TP1last = Output("in_TP1last", TimePart(tw32, 2, 3)) + in_TP1all = Output("in_TP1all", TimePart(tw32, 0, 3)) + in_TP1off1 = Output("in_TP1off1", TimePart(tw32, 0, 3, offset=0)) + in_TP1off2 = Output("in_TP1off2", TimePart(tw32, 0, 3, offset=1)) + in_TP1off3 = Output("in_TP1off3", TimePart(tw32, 0, 3, offset=2)) test = Modely(visualizer=None) - test.addModel('out',[out_tw3, out_tw32, - in_TP1first, in_TP1mid, in_TP1last, in_TP1all, in_TP1off1, in_TP1off2, in_TP1off3]) + test.addModel( + "out", + [ + out_tw3, + out_tw32, + in_TP1first, + in_TP1mid, + in_TP1last, + in_TP1all, + in_TP1off1, + in_TP1off2, + in_TP1off3, + ], + ) test.neuralizeModel(1) - results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7]}) - - self.assertEqual((1, 3), np.array(results['in_tw3']).shape) - self.TestAlmostEqual([[-1, 0, 1]], results['in_tw3']) - self.assertEqual((1, 3), np.array(results['in_tw32']).shape) - self.TestAlmostEqual([[0, 1, 2]], results['in_tw32']) - - self.assertEqual((1,), np.array(results['in_TP1first']).shape) - self.TestAlmostEqual([0], results['in_TP1first']) - self.assertEqual((1,), np.array(results['in_TP1mid']).shape) - self.TestAlmostEqual([1], results['in_TP1mid']) - self.assertEqual((1,), np.array(results['in_TP1last']).shape) - self.TestAlmostEqual([2], results['in_TP1last']) - self.assertEqual((1, 3), np.array(results['in_TP1all']).shape) - self.TestAlmostEqual([[0, 1, 2]], results['in_TP1all']) - self.assertEqual((1, 3), np.array(results['in_TP1off1']).shape) - self.TestAlmostEqual([[0, 1, 2]], results['in_TP1off1']) - self.assertEqual((1, 3), np.array(results['in_TP1off2']).shape) - self.TestAlmostEqual([[-1, 0, 1]], results['in_TP1off2']) - self.assertEqual((1, 3), np.array(results['in_TP1off3']).shape) - self.TestAlmostEqual([[-2, -1, 0]], results['in_TP1off3']) - - results = test({'in1': [0, 1, 2, 3, 4, 5, 6, 7, 10]}) - - self.assertEqual((2, 3), np.array(results['in_tw3']).shape) - self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results['in_tw3']) - self.assertEqual((2, 3), np.array(results['in_tw32']).shape) - self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results['in_tw32']) - - self.assertEqual((2,), np.array(results['in_TP1first']).shape) - self.TestAlmostEqual([0, 0], results['in_TP1first']) - self.assertEqual((2,), np.array(results['in_TP1mid']).shape) - self.TestAlmostEqual([1, 1], results['in_TP1mid']) - self.assertEqual((2,), np.array(results['in_TP1last']).shape) - self.TestAlmostEqual([2, 4], results['in_TP1last']) - self.assertEqual((2, 3), np.array(results['in_TP1all']).shape) - self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results['in_TP1all']) - self.assertEqual((2, 3), np.array(results['in_TP1off1']).shape) - self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results['in_TP1off1']) - self.assertEqual((2, 3), np.array(results['in_TP1off2']).shape) - self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 3]], results['in_TP1off2']) - self.assertEqual((2, 3), np.array(results['in_TP1off3']).shape) - self.TestAlmostEqual([[-2, -1, 0], [-4, -3, 0]], results['in_TP1off3']) - + results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7]}) + + self.assertEqual((1, 3), np.array(results["in_tw3"]).shape) + self.TestAlmostEqual([[-1, 0, 1]], results["in_tw3"]) + self.assertEqual((1, 3), np.array(results["in_tw32"]).shape) + self.TestAlmostEqual([[0, 1, 2]], results["in_tw32"]) + + self.assertEqual((1,), np.array(results["in_TP1first"]).shape) + self.TestAlmostEqual([0], results["in_TP1first"]) + self.assertEqual((1,), np.array(results["in_TP1mid"]).shape) + self.TestAlmostEqual([1], results["in_TP1mid"]) + self.assertEqual((1,), np.array(results["in_TP1last"]).shape) + self.TestAlmostEqual([2], results["in_TP1last"]) + self.assertEqual((1, 3), np.array(results["in_TP1all"]).shape) + self.TestAlmostEqual([[0, 1, 2]], results["in_TP1all"]) + self.assertEqual((1, 3), np.array(results["in_TP1off1"]).shape) + self.TestAlmostEqual([[0, 1, 2]], results["in_TP1off1"]) + self.assertEqual((1, 3), np.array(results["in_TP1off2"]).shape) + self.TestAlmostEqual([[-1, 0, 1]], results["in_TP1off2"]) + self.assertEqual((1, 3), np.array(results["in_TP1off3"]).shape) + self.TestAlmostEqual([[-2, -1, 0]], results["in_TP1off3"]) + + results = test({"in1": [0, 1, 2, 3, 4, 5, 6, 7, 10]}) + + self.assertEqual((2, 3), np.array(results["in_tw3"]).shape) + self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_tw3"]) + self.assertEqual((2, 3), np.array(results["in_tw32"]).shape) + self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_tw32"]) + + self.assertEqual((2,), np.array(results["in_TP1first"]).shape) + self.TestAlmostEqual([0, 0], results["in_TP1first"]) + self.assertEqual((2,), np.array(results["in_TP1mid"]).shape) + self.TestAlmostEqual([1, 1], results["in_TP1mid"]) + self.assertEqual((2,), np.array(results["in_TP1last"]).shape) + self.TestAlmostEqual([2, 4], results["in_TP1last"]) + self.assertEqual((2, 3), np.array(results["in_TP1all"]).shape) + self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_TP1all"]) + self.assertEqual((2, 3), np.array(results["in_TP1off1"]).shape) + self.TestAlmostEqual([[0, 1, 2], [0, 1, 4]], results["in_TP1off1"]) + self.assertEqual((2, 3), np.array(results["in_TP1off2"]).shape) + self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 3]], results["in_TP1off2"]) + self.assertEqual((2, 3), np.array(results["in_TP1off3"]).shape) + self.TestAlmostEqual([[-2, -1, 0], [-4, -3, 0]], results["in_TP1off3"]) + def test_part_and_select(self): NeuObj.clearNames() - in1 = Input('in1',dimensions=4) + in1 = Input("in1", dimensions=4) tw3, tw32 = in1.tw([-5, -2], offset=-4), in1.tw([0, 3], offset=0) - out_tw3 = Output('in_tw3', tw3) - out_tw32 = Output('in_tw32', tw32) + out_tw3 = Output("in_tw3", tw3) + out_tw32 = Output("in_tw32", tw32) # Get after the window with self.assertRaises(IndexError): Part(tw3, 0, 5) @@ -1200,10 +1669,10 @@ def test_part_and_select(self): with self.assertRaises(IndexError): Part(tw32, -1, 0) - in_P1first = Output('in_P1first', Part(tw32, 0, 1)) - in_P1mid = Output('in_P1mid', Part(tw32, 1, 2)) - in_P1last = Output('in_P1last', Part(tw32, 2, 4)) - in_P1all = Output('in_P1all', Part(tw32, 0, 4)) + in_P1first = Output("in_P1first", Part(tw32, 0, 1)) + in_P1mid = Output("in_P1mid", Part(tw32, 1, 2)) + in_P1last = Output("in_P1last", Part(tw32, 2, 4)) + in_P1all = Output("in_P1all", Part(tw32, 0, 4)) with self.assertRaises(IndexError): Select(tw3, -1) @@ -1213,279 +1682,395 @@ def test_part_and_select(self): Select(tw32, -1) with self.assertRaises(IndexError): Select(tw32, 4) - in_S1 = Output('in_S1', Select(tw3, 0)) - in_S2 = Output('in_S2', Select(tw3, 1)) - in_S3 = Output('in_S3', Select(tw3, 2)) - in_S4 = Output('in_S4', Select(tw3, 3)) + in_S1 = Output("in_S1", Select(tw3, 0)) + in_S2 = Output("in_S2", Select(tw3, 1)) + in_S3 = Output("in_S3", Select(tw3, 2)) + in_S4 = Output("in_S4", Select(tw3, 3)) test = Modely(visualizer=None) - test.addModel('out',[out_tw3, out_tw32, - in_P1first, in_P1mid, in_P1last, in_P1all, - in_S1, in_S2, in_S3, in_S4]) + test.addModel( + "out", + [ + out_tw3, + out_tw32, + in_P1first, + in_P1mid, + in_P1last, + in_P1all, + in_S1, + in_S2, + in_S3, + in_S4, + ], + ) test.neuralizeModel(1) - results = test({'in1': [[0,1,2,4], [1,3,4,5], [2,5,6,7], [3,3,4,1], [4,4,6,7], [5,6,7,8], [6,7,5,4],[7,2,3,1]]}) - - self.assertEqual((1, 3, 4), np.array(results['in_tw3']).shape) - self.TestAlmostEqual([[[-1,-2,-2,-1], [0,0,0,0], [1,2,2,2]]], results['in_tw3']) - self.assertEqual((1, 3, 4), np.array(results['in_tw32']).shape) - self.TestAlmostEqual([[[0,0,0,0], [1,1,-2,-4],[2,-4,-4,-7]]], results['in_tw32']) - - self.assertEqual((1,3), np.array(results['in_P1first']).shape) - self.TestAlmostEqual([[0,1,2]], results['in_P1first']) - self.assertEqual((1,3), np.array(results['in_P1mid']).shape) - self.TestAlmostEqual([[0,1,-4]], results['in_P1mid']) - self.assertEqual((1,3,2), np.array(results['in_P1last']).shape) - self.TestAlmostEqual([[[0,0],[-2,-4],[-4,-7]]], results['in_P1last']) - self.assertEqual((1,3,4), np.array(results['in_P1all']).shape) - self.TestAlmostEqual([[[0,0,0,0], [1,1,-2,-4],[2,-4,-4,-7]]], results['in_P1all']) - - self.assertEqual((1,3), np.array(results['in_S1']).shape) - self.TestAlmostEqual([[-1,0,1]], results['in_S1']) - self.assertEqual((1,3), np.array(results['in_S2']).shape) - self.TestAlmostEqual([[-2,0,2]], results['in_S2']) - self.assertEqual((1,3), np.array(results['in_S3']).shape) - self.TestAlmostEqual([[-2,0,2]], results['in_S3']) - self.assertEqual((1,3), np.array(results['in_S4']).shape) - self.TestAlmostEqual([[-1,0,2]], results['in_S4']) - - results = test({'in1': [[0,1,2,4], [1,3,4,5], [2,5,6,7], [3,3,4,1], [4,4,6,7], [5,6,7,8], [6,7,5,4],[7,2,3,1],[0,7,0,0]]}) - - self.assertEqual((2, 3, 4), np.array(results['in_tw3']).shape) - self.TestAlmostEqual([[[-1, -2, -2, -1], [0, 0, 0, 0], [1, 2, 2, 2]], - [[-1, -2, -2, -2], [0, 0, 0, 0], [1, -2, -2, -6]]], results['in_tw3']) - self.assertEqual((2, 3, 4), np.array(results['in_tw32']).shape) - self.TestAlmostEqual([[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]], - [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]]], results['in_tw32']) - - self.assertEqual((2, 3), np.array(results['in_P1first']).shape) - self.TestAlmostEqual([[0, 1, 2],[0, 1, -6]], results['in_P1first']) - self.assertEqual((2, 3), np.array(results['in_P1mid']).shape) - self.TestAlmostEqual([[0, 1, -4],[0, -5, 0]], results['in_P1mid']) - self.assertEqual((2, 3, 2), np.array(results['in_P1last']).shape) - self.TestAlmostEqual([[[0, 0], [-2, -4], [-4, -7]], - [[0, 0], [-2, -3], [-5, -4]]], results['in_P1last']) - self.assertEqual((2, 3, 4), np.array(results['in_P1all']).shape) - self.TestAlmostEqual([[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]], - [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]]], results['in_P1all']) - - self.assertEqual((2, 3), np.array(results['in_S1']).shape) - self.TestAlmostEqual([[-1, 0, 1],[-1, 0, 1]], results['in_S1']) - self.assertEqual((2, 3), np.array(results['in_S2']).shape) - self.TestAlmostEqual([[-2, 0, 2],[-2, 0, -2]], results['in_S2']) - self.assertEqual((2, 3), np.array(results['in_S3']).shape) - self.TestAlmostEqual([[-2, 0, 2],[-2, 0, -2]], results['in_S3']) - self.assertEqual((2, 3), np.array(results['in_S4']).shape) - self.TestAlmostEqual([[-1, 0, 2],[-2, 0, -6]], results['in_S4']) + results = test( + { + "in1": [ + [0, 1, 2, 4], + [1, 3, 4, 5], + [2, 5, 6, 7], + [3, 3, 4, 1], + [4, 4, 6, 7], + [5, 6, 7, 8], + [6, 7, 5, 4], + [7, 2, 3, 1], + ] + } + ) + + self.assertEqual((1, 3, 4), np.array(results["in_tw3"]).shape) + self.TestAlmostEqual( + [[[-1, -2, -2, -1], [0, 0, 0, 0], [1, 2, 2, 2]]], results["in_tw3"] + ) + self.assertEqual((1, 3, 4), np.array(results["in_tw32"]).shape) + self.TestAlmostEqual( + [[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]]], results["in_tw32"] + ) + + self.assertEqual((1, 3), np.array(results["in_P1first"]).shape) + self.TestAlmostEqual([[0, 1, 2]], results["in_P1first"]) + self.assertEqual((1, 3), np.array(results["in_P1mid"]).shape) + self.TestAlmostEqual([[0, 1, -4]], results["in_P1mid"]) + self.assertEqual((1, 3, 2), np.array(results["in_P1last"]).shape) + self.TestAlmostEqual([[[0, 0], [-2, -4], [-4, -7]]], results["in_P1last"]) + self.assertEqual((1, 3, 4), np.array(results["in_P1all"]).shape) + self.TestAlmostEqual( + [[[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]]], results["in_P1all"] + ) + + self.assertEqual((1, 3), np.array(results["in_S1"]).shape) + self.TestAlmostEqual([[-1, 0, 1]], results["in_S1"]) + self.assertEqual((1, 3), np.array(results["in_S2"]).shape) + self.TestAlmostEqual([[-2, 0, 2]], results["in_S2"]) + self.assertEqual((1, 3), np.array(results["in_S3"]).shape) + self.TestAlmostEqual([[-2, 0, 2]], results["in_S3"]) + self.assertEqual((1, 3), np.array(results["in_S4"]).shape) + self.TestAlmostEqual([[-1, 0, 2]], results["in_S4"]) + + results = test( + { + "in1": [ + [0, 1, 2, 4], + [1, 3, 4, 5], + [2, 5, 6, 7], + [3, 3, 4, 1], + [4, 4, 6, 7], + [5, 6, 7, 8], + [6, 7, 5, 4], + [7, 2, 3, 1], + [0, 7, 0, 0], + ] + } + ) + + self.assertEqual((2, 3, 4), np.array(results["in_tw3"]).shape) + self.TestAlmostEqual( + [ + [[-1, -2, -2, -1], [0, 0, 0, 0], [1, 2, 2, 2]], + [[-1, -2, -2, -2], [0, 0, 0, 0], [1, -2, -2, -6]], + ], + results["in_tw3"], + ) + self.assertEqual((2, 3, 4), np.array(results["in_tw32"]).shape) + self.TestAlmostEqual( + [ + [[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]], + [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]], + ], + results["in_tw32"], + ) + + self.assertEqual((2, 3), np.array(results["in_P1first"]).shape) + self.TestAlmostEqual([[0, 1, 2], [0, 1, -6]], results["in_P1first"]) + self.assertEqual((2, 3), np.array(results["in_P1mid"]).shape) + self.TestAlmostEqual([[0, 1, -4], [0, -5, 0]], results["in_P1mid"]) + self.assertEqual((2, 3, 2), np.array(results["in_P1last"]).shape) + self.TestAlmostEqual( + [[[0, 0], [-2, -4], [-4, -7]], [[0, 0], [-2, -3], [-5, -4]]], + results["in_P1last"], + ) + self.assertEqual((2, 3, 4), np.array(results["in_P1all"]).shape) + self.TestAlmostEqual( + [ + [[0, 0, 0, 0], [1, 1, -2, -4], [2, -4, -4, -7]], + [[0, 0, 0, 0], [1, -5, -2, -3], [-6, 0, -5, -4]], + ], + results["in_P1all"], + ) + + self.assertEqual((2, 3), np.array(results["in_S1"]).shape) + self.TestAlmostEqual([[-1, 0, 1], [-1, 0, 1]], results["in_S1"]) + self.assertEqual((2, 3), np.array(results["in_S2"]).shape) + self.TestAlmostEqual([[-2, 0, 2], [-2, 0, -2]], results["in_S2"]) + self.assertEqual((2, 3), np.array(results["in_S3"]).shape) + self.TestAlmostEqual([[-2, 0, 2], [-2, 0, -2]], results["in_S3"]) + self.assertEqual((2, 3), np.array(results["in_S4"]).shape) + self.TestAlmostEqual([[-1, 0, 2], [-2, 0, -6]], results["in_S4"]) def test_predict_paramfun_param_const(self): NeuObj.clearNames() - input2 = Input('in2') - pp = Parameter('pp', values=[[7],[8],[9]]) - ll = Constant('ll', values=[[12],[13],[14]]) - oo = Constant('oo', values=[[1],[2],[3]]) + input2 = Input("in2") + pp = Parameter("pp", values=[[7], [8], [9]]) + ll = Constant("ll", values=[[12], [13], [14]]) + oo = Constant("oo", values=[[1], [2], [3]]) pp, oo, input2.tw(0.03), ll + def fun_test(x, y, z, k): return (x + y) * (z - k) NeuObj.clearNames() - out = Output('out',ParamFun(fun_test,parameters_and_constants=[ll,oo,pp])(input2.tw(0.03))) + out = Output( + "out", + ParamFun(fun_test, parameters_and_constants=[ll, oo, pp])(input2.tw(0.03)), + ) test = Modely(visualizer=None) - test.addModel('out',[out]) + test.addModel("out", [out]) test.neuralizeModel(0.01) - results = test({'in2': [0, 1, 2]}) - self.assertEqual((1, 3), np.array(results['out']).shape) - self.assertEqual([[-72.0, -84.0, -96.0]], results['out']) + results = test({"in2": [0, 1, 2]}) + self.assertEqual((1, 3), np.array(results["out"]).shape) + self.assertEqual([[-72.0, -84.0, -96.0]], results["out"]) NeuObj.clearNames() - out = Output('out',ParamFun(fun_test,parameters_and_constants={'z':pp,'y':ll,'k':oo})(input2.tw(0.03))) + out = Output( + "out", + ParamFun(fun_test, parameters_and_constants={"z": pp, "y": ll, "k": oo})( + input2.tw(0.03) + ), + ) test = Modely(visualizer=None) - test.addModel('out',[out]) + test.addModel("out", [out]) test.neuralizeModel(0.01) - results = test({'in2': [0, 1, 2]}) - self.assertEqual((1, 3), np.array(results['out']).shape) - self.assertEqual([[72.0, 84.0, 96.0]], results['out']) + results = test({"in2": [0, 1, 2]}) + self.assertEqual((1, 3), np.array(results["out"]).shape) + self.assertEqual([[72.0, 84.0, 96.0]], results["out"]) NeuObj.clearNames() parfun = ParamFun(fun_test) - out1 = Output('out1', parfun(input2.tw(0.03), ll, pp, oo)) - out2 = Output('out2', parfun(input2.tw(0.03), ll, oo, pp)) - out3 = Output('out3', parfun(pp, oo, input2.tw(0.03), ll)) + out1 = Output("out1", parfun(input2.tw(0.03), ll, pp, oo)) + out2 = Output("out2", parfun(input2.tw(0.03), ll, oo, pp)) + out3 = Output("out3", parfun(pp, oo, input2.tw(0.03), ll)) test = Modely(visualizer=None) - test.addModel('out',[out1,out2,out3]) + test.addModel("out", [out1, out2, out3]) test.neuralizeModel(0.01) - results = test({'in2': [0, 1, 2]}) - self.assertEqual((1, 3), np.array(results['out1']).shape) - self.assertEqual((1, 3), np.array(results['out2']).shape) - self.assertEqual((1, 3), np.array(results['out3']).shape) - self.assertEqual([[72.0, 84.0, 96.0]], results['out1']) - self.assertEqual([[-72.0, -84.0, -96.0]], results['out2']) - self.assertEqual([[-96.0, -120.0, -144.0]], results['out3']) + results = test({"in2": [0, 1, 2]}) + self.assertEqual((1, 3), np.array(results["out1"]).shape) + self.assertEqual((1, 3), np.array(results["out2"]).shape) + self.assertEqual((1, 3), np.array(results["out3"]).shape) + self.assertEqual([[72.0, 84.0, 96.0]], results["out1"]) + self.assertEqual([[-72.0, -84.0, -96.0]], results["out2"]) + self.assertEqual([[-96.0, -120.0, -144.0]], results["out3"]) def test_predict_paramfun_map_over_batch(self): NeuObj.clearNames() - input2 = Input('in2') - pp = Parameter('pp', sw=3, values=[[7],[8],[9]]) - ll = Constant('ll', sw=3, values=[[12],[13],[14]]) - oo = Constant('oo', sw=3, values=[[1],[2],[3]]) + input2 = Input("in2") + pp = Parameter("pp", sw=3, values=[[7], [8], [9]]) + ll = Constant("ll", sw=3, values=[[12], [13], [14]]) + oo = Constant("oo", sw=3, values=[[1], [2], [3]]) def fun_test(x, y, z, k): return (x + y) * (z - k) - fun_map = ParamFun(fun_test,parameters_and_constants=[ll,oo, pp]) - fun = ParamFun(fun_test, parameters_and_constants=[ll,oo, pp]) + fun_map = ParamFun(fun_test, parameters_and_constants=[ll, oo, pp]) + fun = ParamFun(fun_test, parameters_and_constants=[ll, oo, pp]) fun_map_2 = ParamFun(fun_test, map_over_batch=True) - out1 = Output('out1',fun_map(input2.tw(0.03))) - out2 = Output('out2', fun(input2.tw(0.03))) + out1 = Output("out1", fun_map(input2.tw(0.03))) + out2 = Output("out2", fun(input2.tw(0.03))) test = Modely(visualizer=None) - test.addModel('out',[out1,out2]) + test.addModel("out", [out1, out2]) test.neuralizeModel(0.01) - results = test({'in2': [0, 1, 2]}) - self.assertEqual((1, 3), np.array(results['out1']).shape) - self.assertEqual([[-72.0, -84.0, -96.0]], results['out1']) - self.assertEqual((1, 3), np.array(results['out2']).shape) - self.assertEqual([[-72.0, -84.0, -96.0]], results['out2']) - - out3 = Output('out3', fun_map_2(input2.tw(0.03), 4.0, pp, ll)) - out4 = Output('out4', fun_map_2(input2.tw(0.01), 2.0, pp, oo)) + results = test({"in2": [0, 1, 2]}) + self.assertEqual((1, 3), np.array(results["out1"]).shape) + self.assertEqual([[-72.0, -84.0, -96.0]], results["out1"]) + self.assertEqual((1, 3), np.array(results["out2"]).shape) + self.assertEqual([[-72.0, -84.0, -96.0]], results["out2"]) + + out3 = Output("out3", fun_map_2(input2.tw(0.03), 4.0, pp, ll)) + out4 = Output("out4", fun_map_2(input2.tw(0.01), 2.0, pp, oo)) with self.assertRaises(ValueError): fun_map_2(4.0, 1, pp, ll) - test.addModel('out-new', [out3,out4]) + test.addModel("out-new", [out3, out4]) with self.assertRaises(NameError): - test.addModel('out',[out1,out2]) + test.addModel("out", [out1, out2]) test.neuralizeModel(0.01) - results = test({'in2': [0, 1, 2]}) - self.assertEqual((1, 3), np.array(results['out3']).shape) - self.assertEqual((1, 3), np.array(results['out4']).shape) + results = test({"in2": [0, 1, 2]}) + self.assertEqual((1, 3), np.array(results["out3"]).shape) + self.assertEqual((1, 3), np.array(results["out4"]).shape) # ([0,1,2]+4)*([7,8,9]-[12,13,14]) -> [4,5,6]*[-5,-5,-5] - self.assertEqual([[-20.0, -25.0, -30.0]], results['out3']) - self.assertEqual([[24.0, 24.0, 24.0]], results['out4']) + self.assertEqual([[-20.0, -25.0, -30.0]], results["out3"]) + self.assertEqual([[24.0, 24.0, 24.0]], results["out4"]) def test_predict_fuzzify(self): NeuObj.clearNames() - input = Input('in1') - fuzzi = Fuzzify(6, range=[0, 5], functions='Rectangular')(input.last()) - out = Output('out', fuzzi) + input = Input("in1") + fuzzi = Fuzzify(6, range=[0, 5], functions="Rectangular")(input.last()) + out = Output("out", fuzzi) test = Modely(visualizer=None) - test.addModel('out',[out]) + test.addModel("out", [out]) test.neuralizeModel() - results = test({'in1': [0, 1, 2]}) - self.assertEqual((3, 1, 6), np.array(results['out']).shape) - self.assertEqual([[[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]],[[0.0, 1.0, 0.0, 0.0, 0.0, 0.0]],[[0.0, 0.0, 1.0, 0.0, 0.0, 0.0]]], results['out']) + results = test({"in1": [0, 1, 2]}) + self.assertEqual((3, 1, 6), np.array(results["out"]).shape) + self.assertEqual( + [ + [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]], + [[0.0, 1.0, 0.0, 0.0, 0.0, 0.0]], + [[0.0, 0.0, 1.0, 0.0, 0.0, 0.0]], + ], + results["out"], + ) def fun(x): import torch + return torch.sign(x) - fuz = Fuzzify(output_dimension=11, range=[-5, 5], functions=[fun,fun])(input.last()) - out = Output('out2', fuz) - test.addModel('out2',[out]) + fuz = Fuzzify(output_dimension=11, range=[-5, 5], functions=[fun, fun])( + input.last() + ) + out = Output("out2", fuz) + test.addModel("out2", [out]) test.neuralizeModel() - results = test({'in1': [0, 1, 2]}) - self.assertEqual((3, 1, 11), np.array(results['out2']).shape) - self.assertEqual([[[1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, -1.0]], - [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0]], - [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0]]], results['out2']) + results = test({"in1": [0, 1, 2]}) + self.assertEqual((3, 1, 11), np.array(results["out2"]).shape) + self.assertEqual( + [ + [[1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, -1.0]], + [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0]], + [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, -1.0]], + ], + results["out2"], + ) def test_sw_on_stream_sw_by_heand(self): NeuObj.clearNames() - input = Input('in1') + input = Input("in1") sw_from_input = input.sw(7) - state = Input('state') + state = Input("state") sw_from_output = Connect(sw_from_input, state) - out_aux = Output('out_aux', sw_from_output) - out1 = Output('out1', state.sw(3)) + out_aux = Output("out_aux", sw_from_output) + out1 = Output("out1", state.sw(3)) test = Modely(visualizer=None) - test.addModel('out_A', [out_aux,out1]) + test.addModel("out_A", [out_aux, out1]) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual((4, 3), np.array(results['out1']).shape) - self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1']) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual((4, 3), np.array(results["out1"]).shape) + self.assertEqual( + [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], + results["out1"], + ) NeuObj.clearNames() - state = Input('state') - out_aux = Output('out_aux', sw_from_input) - out1 = Output('out1', state.sw(3)) + state = Input("state") + out_aux = Output("out_aux", sw_from_input) + out1 = Output("out1", state.sw(3)) test = Modely(visualizer=None) - test.addModel('out_A', [out_aux,out1]) + test.addModel("out_A", [out_aux, out1]) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={'state': 'out_aux'}) + results = test( + {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={"state": "out_aux"} + ) with self.assertRaises(ValueError): - test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={'out_aux': 'state'}) - self.assertEqual((4, 3), np.array(results['out1']).shape) - self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1']) + test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, connect={"out_aux": "state"}) + self.assertEqual((4, 3), np.array(results["out1"]).shape) + self.assertEqual( + [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], + results["out1"], + ) def test_sw_on_stream_sw(self): NeuObj.clearNames() - input = Input('in1') + input = Input("in1") sw_from_input = input.sw(7) - out1 = Output('out1', sw_from_input.sw(3)) + out1 = Output("out1", sw_from_input.sw(3)) test = Modely(visualizer=None) - test.addModel('out_A', out1) + test.addModel("out_A", out1) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual((4, 3), np.array(results['out1']).shape) - self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1']) - - NeuObj.clearNames(['out1','out2']) - out1 = Output('out1', sw_from_input.sw(3)) - out2 = Output('out2', sw_from_input.sw(8)) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual((4, 3), np.array(results["out1"]).shape) + self.assertEqual( + [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], + results["out1"], + ) + + NeuObj.clearNames(["out1", "out2"]) + out1 = Output("out1", sw_from_input.sw(3)) + out2 = Output("out2", sw_from_input.sw(8)) with self.assertRaises(ValueError): - Output('out3', sw_from_input.sw(-1)) + Output("out3", sw_from_input.sw(-1)) test = Modely(visualizer=None) - test.addModel('out_A', [out1,out2]) + test.addModel("out_A", [out1, out2]) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual((4, 3), np.array(results['out1']).shape) - self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1']) - self.assertEqual((4, 8), np.array(results['out2']).shape) - self.assertEqual([[0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], - [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], - [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], - [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]], results['out2']) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual((4, 3), np.array(results["out1"]).shape) + self.assertEqual( + [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], + results["out1"], + ) + self.assertEqual((4, 8), np.array(results["out2"]).shape) + self.assertEqual( + [ + [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], + [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + ], + results["out2"], + ) def test_tw_on_stream_tw(self): NeuObj.clearNames() - input = Input('in1') + input = Input("in1") tw_from_input = input.tw(3.5) - out1 = Output('out1', tw_from_input.tw(1.5)) + out1 = Output("out1", tw_from_input.tw(1.5)) test = Modely(visualizer=None) - test.addModel('out_A', out1) + test.addModel("out_A", out1) test.neuralizeModel(0.5) - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual((4, 3), np.array(results['out1']).shape) - self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out1']) - - out1 = Output('out11', tw_from_input.tw(1.5)) - out2 = Output('out21', tw_from_input.tw(4)) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual((4, 3), np.array(results["out1"]).shape) + self.assertEqual( + [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], + results["out1"], + ) + + out1 = Output("out11", tw_from_input.tw(1.5)) + out2 = Output("out21", tw_from_input.tw(4)) with self.assertRaises(ValueError): - Output('out3', tw_from_input.tw(-1)) + Output("out3", tw_from_input.tw(-1)) test = Modely(visualizer=None) - test.addModel('out_A', [out1,out2]) + test.addModel("out_A", [out1, out2]) test.neuralizeModel(0.5) - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual((4, 3), np.array(results['out11']).shape) - self.assertEqual([[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], results['out11']) - self.assertEqual((4, 8), np.array(results['out21']).shape) - self.assertEqual([[0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], - [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], - [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], - [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]], results['out21']) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual((4, 3), np.array(results["out11"]).shape) + self.assertEqual( + [[4.0, 5.0, 6.0], [5.0, 6.0, 7.0], [6.0, 7.0, 8.0], [7.0, 8.0, 9.0]], + results["out11"], + ) + self.assertEqual((4, 8), np.array(results["out21"]).shape) + self.assertEqual( + [ + [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], + [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + ], + results["out21"], + ) def test_sw_on_stream_sw_complex(self): NeuObj.clearNames() - input = Input('in1') - state = Input('state') + input = Input("in1") + state = Input("state") sw_3 = input.sw(3) sw_7 = input.sw(7) @@ -1493,200 +2078,317 @@ def test_sw_on_stream_sw_complex(self): state4 = state.sw(4) state8 = state.sw(8) - out21 = Output('out21', sw_3.sw(2)) - out61 = Output('out61', sw_7.sw(6)) - out22 = Output('out22', SamplePart(sw_3,1,3)) - out62 = Output('out62', SamplePart(sw_7,1,7)) + out21 = Output("out21", sw_3.sw(2)) + out61 = Output("out61", sw_7.sw(6)) + out22 = Output("out22", SamplePart(sw_3, 1, 3)) + out62 = Output("out62", SamplePart(sw_7, 1, 7)) test = Modely(visualizer=None) - test.addModel('out_A', [out21,out61,out22,out62]) + test.addModel("out_A", [out21, out61, out22, out62]) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual(results['out21'], results['out22']) - self.assertEqual(results['out61'], results['out62']) - - out31 = Output('out31', sw_3) - out71 = Output('out71', sw_7) - out32 = Output('out32', SamplePart(state4,1,4)) - out72 = Output('out72', SamplePart(state8,1,8)) - out33 = Output('out33', sw_3.sw(3)) - out73 = Output('out73', sw_7.sw(7)) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual(results["out21"], results["out22"]) + self.assertEqual(results["out61"], results["out62"]) + + out31 = Output("out31", sw_3) + out71 = Output("out71", sw_7) + out32 = Output("out32", SamplePart(state4, 1, 4)) + out72 = Output("out72", SamplePart(state8, 1, 8)) + out33 = Output("out33", sw_3.sw(3)) + out73 = Output("out73", sw_7.sw(7)) test = Modely(visualizer=None) - test.addModel('out_B', [out31,out71,out32,out72,out33,out73]) + test.addModel("out_B", [out31, out71, out32, out72, out33, out73]) test.addConnect(sw_7, state) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual(results['out31'], results['out32']) - self.assertEqual(results['out32'], results['out33']) - self.assertEqual(results['out71'], results['out72']) - self.assertEqual(results['out72'], results['out73']) - - out41 = Output('out41', state4) - out42 = Output('out42', sw_3.sw(4)) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual(results["out31"], results["out32"]) + self.assertEqual(results["out32"], results["out33"]) + self.assertEqual(results["out71"], results["out72"]) + self.assertEqual(results["out72"], results["out73"]) + + out41 = Output("out41", state4) + out42 = Output("out42", sw_3.sw(4)) test = Modely(visualizer=None) - test.addModel('out_C', [out41,out42]) + test.addModel("out_C", [out41, out42]) test.addConnect(sw_3, state) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual(results['out41'], results['out42']) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual(results["out41"], results["out42"]) def test_sw_on_stream_tw_and_opposite(self): NeuObj.clearNames() - input = Input('in1') + input = Input("in1") sw_3 = input.sw(3) tw_4 = input.tw(1) - out3tw1 = Output('out3tw1', sw_3.tw(0.2)) - out3tw10 = Output('out3tw10', sw_3.tw(2)) - out4sw2 = Output('out4sw2', tw_4.sw(2)) - out4sw6 = Output('out4sw6', tw_4.sw(6)) + out3tw1 = Output("out3tw1", sw_3.tw(0.2)) + out3tw10 = Output("out3tw10", sw_3.tw(2)) + out4sw2 = Output("out4sw2", tw_4.sw(2)) + out4sw6 = Output("out4sw6", tw_4.sw(6)) test = Modely(visualizer=None) - test.addModel('out_A', [out3tw1, out3tw10, out4sw2, out4sw6]) + test.addModel("out_A", [out3tw1, out3tw10, out4sw2, out4sw6]) test.neuralizeModel(0.2) - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9, -3]}) - self.assertEqual(results['out3tw1'], [4, 5, 6, 7, 8, 9, -3]) - self.assertEqual(results['out3tw10'], [[0, 0, 0, 0, 0, 0, 0, 2, 3, 4], - [0, 0, 0, 0, 0,0, 2, 3, 4, 5], - [0, 0, 0, 0, 0, 2, 3, 4 ,5, 6], - [0, 0, 0, 0, 2, 3, 4, 5, 6, 7], - [0, 0, 0, 2, 3, 4, 5, 6, 7, 8], - [0, 0, 2, 3, 4, 5, 6, 7, 8, 9], - [0, 2, 3, 4, 5, 6, 7, 8, 9, -3]]) - self.assertEqual(results['out4sw2'], [[3, 4],[4,5],[5,6],[6,7],[7,8],[8,9], [9,-3]]) - self.assertEqual(results['out4sw6'], [[0, 14, 1, 2, 3, 4], - [14, 1, 2, 3, 4, 5], - [1, 2, 3, 4 ,5, 6], - [2, 3, 4, 5, 6, 7], - [3, 4, 5, 6, 7, 8], - [4, 5, 6, 7, 8, 9], - [5, 6, 7, 8, 9, -3]]) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9, -3]}) + self.assertEqual(results["out3tw1"], [4, 5, 6, 7, 8, 9, -3]) + self.assertEqual( + results["out3tw10"], + [ + [0, 0, 0, 0, 0, 0, 0, 2, 3, 4], + [0, 0, 0, 0, 0, 0, 2, 3, 4, 5], + [0, 0, 0, 0, 0, 2, 3, 4, 5, 6], + [0, 0, 0, 0, 2, 3, 4, 5, 6, 7], + [0, 0, 0, 2, 3, 4, 5, 6, 7, 8], + [0, 0, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 2, 3, 4, 5, 6, 7, 8, 9, -3], + ], + ) + self.assertEqual( + results["out4sw2"], + [[3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, -3]], + ) + self.assertEqual( + results["out4sw6"], + [ + [0, 14, 1, 2, 3, 4], + [14, 1, 2, 3, 4, 5], + [1, 2, 3, 4, 5, 6], + [2, 3, 4, 5, 6, 7], + [3, 4, 5, 6, 7, 8], + [4, 5, 6, 7, 8, 9], + [5, 6, 7, 8, 9, -3], + ], + ) def test_sw_on_stream_sw_delay(self): NeuObj.clearNames() - input = Input('in1') + input = Input("in1") sw_3 = input.sw(3) - out21 = Output('out21', sw_3.sw(2)) - out41 = Output('out41', sw_3.sw(4)) - out22 = Output('out22', sw_3.sw([-2,0])) - out42 = Output('out42', sw_3.sw([-4,0])) + out21 = Output("out21", sw_3.sw(2)) + out41 = Output("out41", sw_3.sw(4)) + out22 = Output("out22", sw_3.sw([-2, 0])) + out42 = Output("out42", sw_3.sw([-4, 0])) - out231 = Output('out231', sw_3.sw([-3,-1])) - out451 = Output('out451', sw_3.sw([-5,-1])) + out231 = Output("out231", sw_3.sw([-3, -1])) + out451 = Output("out451", sw_3.sw([-5, -1])) - out242 = Output('out242', sw_3.sw([-4,-2])) - out462 = Output('out462', sw_3.sw([-6,-2])) + out242 = Output("out242", sw_3.sw([-4, -2])) + out462 = Output("out462", sw_3.sw([-6, -2])) test = Modely(visualizer=None) - test.addModel('out_A', [out21,out41,out22,out42,out231,out451,out242,out462]) + test.addModel( + "out_A", [out21, out41, out22, out42, out231, out451, out242, out462] + ) test.neuralizeModel() - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual(results['out21'], results['out22']) - self.assertEqual(results['out21'], [[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9]]) - - self.assertEqual(results['out41'], results['out42']) - self.assertEqual(results['out41'], [[0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8], [6, 7, 8, 9]]) - - self.assertEqual(results['out231'], [[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]]) - self.assertEqual(results['out451'], [[0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8]]) - - self.assertEqual(results['out242'], [[0,14],[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7]]) - self.assertEqual(results['out462'], [[0, 0, 0, 14], [0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]]) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual(results["out21"], results["out22"]) + self.assertEqual( + results["out21"], + [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9]], + ) + + self.assertEqual(results["out41"], results["out42"]) + self.assertEqual( + results["out41"], + [ + [0, 14, 1, 2], + [14, 1, 2, 3], + [1, 2, 3, 4], + [2, 3, 4, 5], + [3, 4, 5, 6], + [4, 5, 6, 7], + [5, 6, 7, 8], + [6, 7, 8, 9], + ], + ) + + self.assertEqual( + results["out231"], + [[14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8]], + ) + self.assertEqual( + results["out451"], + [ + [0, 0, 14, 1], + [0, 14, 1, 2], + [14, 1, 2, 3], + [1, 2, 3, 4], + [2, 3, 4, 5], + [3, 4, 5, 6], + [4, 5, 6, 7], + [5, 6, 7, 8], + ], + ) + + self.assertEqual( + results["out242"], + [[0, 14], [14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]], + ) + self.assertEqual( + results["out462"], + [ + [0, 0, 0, 14], + [0, 0, 14, 1], + [0, 14, 1, 2], + [14, 1, 2, 3], + [1, 2, 3, 4], + [2, 3, 4, 5], + [3, 4, 5, 6], + [4, 5, 6, 7], + ], + ) def test_tw_on_stream_tw_delay(self): NeuObj.clearNames() - input = Input('in1') + input = Input("in1") tw_3 = input.tw(1.5) - out21 = Output('out21', tw_3.tw(1)) - out41 = Output('out41', tw_3.tw(2)) - out22 = Output('out22', tw_3.tw([-1,0])) - out42 = Output('out42', tw_3.tw([-2,0])) + out21 = Output("out21", tw_3.tw(1)) + out41 = Output("out41", tw_3.tw(2)) + out22 = Output("out22", tw_3.tw([-1, 0])) + out42 = Output("out42", tw_3.tw([-2, 0])) - out231 = Output('out231', tw_3.tw([-1.5,-0.5])) - out451 = Output('out451', tw_3.tw([-2.5,-0.5])) + out231 = Output("out231", tw_3.tw([-1.5, -0.5])) + out451 = Output("out451", tw_3.tw([-2.5, -0.5])) - out242 = Output('out242', tw_3.tw([-2,-1])) - out462 = Output('out462', tw_3.tw([-3,-1])) + out242 = Output("out242", tw_3.tw([-2, -1])) + out462 = Output("out462", tw_3.tw([-3, -1])) test = Modely(visualizer=None) - test.addModel('out_A', [out21,out41,out22,out42,out231,out451,out242,out462]) + test.addModel( + "out_A", [out21, out41, out22, out42, out231, out451, out242, out462] + ) test.neuralizeModel(0.5) - results = test({'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual(results['out21'], results['out22']) - self.assertEqual(results['out21'], [[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9]]) - - self.assertEqual(results['out41'], results['out42']) - self.assertEqual(results['out41'], [[0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8], [6, 7, 8, 9]]) - - self.assertEqual(results['out231'], [[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]]) - self.assertEqual(results['out451'], [[0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8]]) - - self.assertEqual(results['out242'], [[0,14],[14,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7]]) - self.assertEqual(results['out462'], [[0, 0, 0, 14], [0, 0, 14, 1], [0, 14, 1, 2], [14, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]]) + results = test({"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual(results["out21"], results["out22"]) + self.assertEqual( + results["out21"], + [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9]], + ) + + self.assertEqual(results["out41"], results["out42"]) + self.assertEqual( + results["out41"], + [ + [0, 14, 1, 2], + [14, 1, 2, 3], + [1, 2, 3, 4], + [2, 3, 4, 5], + [3, 4, 5, 6], + [4, 5, 6, 7], + [5, 6, 7, 8], + [6, 7, 8, 9], + ], + ) + + self.assertEqual( + results["out231"], + [[14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8]], + ) + self.assertEqual( + results["out451"], + [ + [0, 0, 14, 1], + [0, 14, 1, 2], + [14, 1, 2, 3], + [1, 2, 3, 4], + [2, 3, 4, 5], + [3, 4, 5, 6], + [4, 5, 6, 7], + [5, 6, 7, 8], + ], + ) + + self.assertEqual( + results["out242"], + [[0, 14], [14, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]], + ) + self.assertEqual( + results["out462"], + [ + [0, 0, 0, 14], + [0, 0, 14, 1], + [0, 14, 1, 2], + [14, 1, 2, 3], + [1, 2, 3, 4], + [2, 3, 4, 5], + [3, 4, 5, 6], + [4, 5, 6, 7], + ], + ) def test_z_on_stream_sw(self): NeuObj.clearNames() - input = Input('inin') + input = Input("inin") sw_from_input = input.sw(5) - out2 = Output('out2', sw_from_input.z(1)) + out2 = Output("out2", sw_from_input.z(1)) with self.assertRaises(ValueError): - Output('out3', sw_from_input.z(-1)) + Output("out3", sw_from_input.z(-1)) with self.assertRaises(TypeError): - Output('out3', sw_from_input.delay(1)) + Output("out3", sw_from_input.delay(1)) test = Modely(visualizer=None) - test.addModel('out_A', [out2]) + test.addModel("out_A", [out2]) test.neuralizeModel(0.5) - results = test({'inin': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual((6, 5), np.array(results['out2']).shape) - self.assertEqual( [[0.0, 14.0, 1.0, 2.0, 3.0], - [14.0, 1.0, 2.0, 3.0, 4.0], - [1.0, 2.0, 3.0, 4.0, 5.0], - [2.0, 3.0, 4.0, 5.0, 6.0], - [3.0, 4.0, 5.0, 6.0, 7.0], - [4.0, 5.0, 6.0, 7.0, 8.0]], results['out2']) + results = test({"inin": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual((6, 5), np.array(results["out2"]).shape) + self.assertEqual( + [ + [0.0, 14.0, 1.0, 2.0, 3.0], + [14.0, 1.0, 2.0, 3.0, 4.0], + [1.0, 2.0, 3.0, 4.0, 5.0], + [2.0, 3.0, 4.0, 5.0, 6.0], + [3.0, 4.0, 5.0, 6.0, 7.0], + [4.0, 5.0, 6.0, 7.0, 8.0], + ], + results["out2"], + ) def test_delay_on_stream_tw(self): NeuObj.clearNames() - input = Input('inin') + input = Input("inin") tw_from_input = input.tw(3.5) - out1 = Output('out1', tw_from_input.delay(1)) + out1 = Output("out1", tw_from_input.delay(1)) with self.assertRaises(ValueError): - Output('out3', tw_from_input.delay(-1)) + Output("out3", tw_from_input.delay(-1)) with self.assertRaises(TypeError): - Output('out3', tw_from_input.z(1)) + Output("out3", tw_from_input.z(1)) test = Modely(visualizer=None) - test.addModel('out_A', [out1]) + test.addModel("out_A", [out1]) test.neuralizeModel(0.5) - results = test({'inin': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) - self.assertEqual((4,7), np.array(results['out1']).shape) - self.assertEqual([[0.0, 0.0, 14.0, 1.0, 2.0, 3.0, 4.0], - [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0], - [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], - [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]], results['out1']) - #TODO add test with initialization of state variable + results = test({"inin": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual((4, 7), np.array(results["out1"]).shape) + self.assertEqual( + [ + [0.0, 0.0, 14.0, 1.0, 2.0, 3.0, 4.0], + [0.0, 14.0, 1.0, 2.0, 3.0, 4.0, 5.0], + [14.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + ], + results["out1"], + ) + # TODO add test with initialization of state variable def test_localmodel(self): NeuObj.clearNames() - x = Input('x') - F = Input('F') - activationA = Fuzzify(2, [0, 1], functions='Triangular')(x.tw(1)) - activationB = Fuzzify(2, [0, 1], functions='Triangular')(F.tw(1)) + x = Input("x") + F = Input("F") + activationA = Fuzzify(2, [0, 1], functions="Triangular")(x.tw(1)) + activationB = Fuzzify(2, [0, 1], functions="Triangular")(F.tw(1)) def myFun(in1, p1, p2): return p1 * in1 + p2 - p1_0 = Parameter('p1_0', values=[[1]]) - p1_1 = Parameter('p1_1', values=[[2]]) - p2_0 = Parameter('p2_0', values=[[2]]) - p2_1 = Parameter('p2_1', values=[[3]]) + p1_0 = Parameter("p1_0", values=[[1]]) + p1_1 = Parameter("p1_1", values=[[2]]) + p2_0 = Parameter("p2_0", values=[[2]]) + p2_1 = Parameter("p2_1", values=[[3]]) def input_function_gen(idx_list): if idx_list == [0, 0]: @@ -1700,248 +2402,470 @@ def input_function_gen(idx_list): return ParamFun(myFun, parameters_and_constants=[p1, p2]) def output_function_gen(idx_list): - pfir = Parameter('pfir_' + str(idx_list), tw=1, dimensions=2, - values=[[1 + idx_list[0], 2 + idx_list[1]], [3 + idx_list[0], 4 + idx_list[1]]]) + pfir = Parameter( + "pfir_" + str(idx_list), + tw=1, + dimensions=2, + values=[ + [1 + idx_list[0], 2 + idx_list[1]], + [3 + idx_list[0], 4 + idx_list[1]], + ], + ) return Fir(2, W=pfir) - loc = LocalModel(input_function=input_function_gen, output_function=output_function_gen, pass_indexes=True)(x.tw(1), (activationA, activationB)) + loc = LocalModel( + input_function=input_function_gen, + output_function=output_function_gen, + pass_indexes=True, + )(x.tw(1), (activationA, activationB)) # Example of the structure of the local model - pfir00 = Parameter('N_pfir_[0, 0]', tw=1, dimensions=2, values=[[1, 2], [3, 4]]) - pfir01 = Parameter('N_pfir_[0, 1]', tw=1, dimensions=2, values=[[1, 3], [3, 5]]) - pfir10 = Parameter('N_pfir_[1, 0]', tw=1, dimensions=2, values=[[2, 2], [4, 4]]) - pfir11 = Parameter('N_pfir_[1, 1]', tw=1, dimensions=2, values=[[2, 3], [4, 5]]) + pfir00 = Parameter("N_pfir_[0, 0]", tw=1, dimensions=2, values=[[1, 2], [3, 4]]) + pfir01 = Parameter("N_pfir_[0, 1]", tw=1, dimensions=2, values=[[1, 3], [3, 5]]) + pfir10 = Parameter("N_pfir_[1, 0]", tw=1, dimensions=2, values=[[2, 2], [4, 4]]) + pfir11 = Parameter("N_pfir_[1, 1]", tw=1, dimensions=2, values=[[2, 3], [4, 5]]) parfun_00 = ParamFun(myFun, parameters_and_constants=[p1_0, p2_0])(x.tw(1)) parfun_01 = ParamFun(myFun, parameters_and_constants=[p1_0, p2_1])(x.tw(1)) parfun_10 = ParamFun(myFun, parameters_and_constants=[p1_1, p2_0])(x.tw(1)) parfun_11 = ParamFun(myFun, parameters_and_constants=[p1_1, p2_1])(x.tw(1)) - out_in_00 = Output('parfun00', parfun_00) - out_in_01 = Output('parfun01', parfun_01) - out_in_10 = Output('parfun10', parfun_10) - out_in_11 = Output('parfun11', parfun_11) - actA = Output('fuzzyA', activationA) - actB = Output('fuzzyB', activationB) + out_in_00 = Output("parfun00", parfun_00) + out_in_01 = Output("parfun01", parfun_01) + out_in_10 = Output("parfun10", parfun_10) + out_in_11 = Output("parfun11", parfun_11) + actA = Output("fuzzyA", activationA) + actB = Output("fuzzyB", activationB) act_selA0 = Select(activationA, 0) act_selA1 = Select(activationA, 1) act_selB0 = Select(activationB, 0) act_selB1 = Select(activationB, 1) - out_act_selA0 = Output('fuzzy_selA0', act_selA0) - out_act_selA1 = Output('fuzzy_selA1', act_selA1) - out_act_selB0 = Output('fuzzy_selB0', act_selB0) - out_act_selB1 = Output('fuzzy_selB1', act_selB1) + out_act_selA0 = Output("fuzzy_selA0", act_selA0) + out_act_selA1 = Output("fuzzy_selA1", act_selA1) + out_act_selB0 = Output("fuzzy_selB0", act_selB0) + out_act_selB1 = Output("fuzzy_selB1", act_selB1) mul00 = parfun_00 * act_selA0 * act_selB0 mul01 = parfun_01 * act_selA0 * act_selB1 mul10 = parfun_10 * act_selA1 * act_selB0 mul11 = parfun_11 * act_selA1 * act_selB1 - out_mul00 = Output('mul00', mul00) - out_mul01 = Output('mul01', mul01) - out_mul10 = Output('mul10', mul10) - out_mul11 = Output('mul11', mul11) + out_mul00 = Output("mul00", mul00) + out_mul01 = Output("mul01", mul01) + out_mul10 = Output("mul10", mul10) + out_mul11 = Output("mul11", mul11) fir00 = Fir(2, W=pfir00)(mul00) fir01 = Fir(2, W=pfir01)(mul01) fir10 = Fir(2, W=pfir10)(mul10) fir11 = Fir(2, W=pfir11)(mul11) - out_fir00 = Output('fir00', fir00) - out_fir01 = Output('fir01', fir01) - out_fir10 = Output('fir10', fir10) - out_fir11 = Output('fir11', fir11) + out_fir00 = Output("fir00", fir00) + out_fir01 = Output("fir01", fir01) + out_fir10 = Output("fir10", fir10) + out_fir11 = Output("fir11", fir11) sum = fir00 + fir01 + fir10 + fir11 - out_sum = Output('out_sum', sum) - out = Output('out', loc) + out_sum = Output("out_sum", sum) + out = Output("out", loc) test = Modely(visualizer=None) - test.addModel('all_out', [out_in_00, out_in_01, out_in_10, out_in_11, - out_act_selA0, out_act_selA1, out_act_selB0, out_act_selB1, - out_mul00, out_mul01, out_mul10, out_mul11, - out_fir00, out_fir01, out_fir10, out_fir11, - out_sum]) - test.addModel('out', out) + test.addModel( + "all_out", + [ + out_in_00, + out_in_01, + out_in_10, + out_in_11, + out_act_selA0, + out_act_selA1, + out_act_selB0, + out_act_selB1, + out_mul00, + out_mul01, + out_mul10, + out_mul11, + out_fir00, + out_fir01, + out_fir10, + out_fir11, + out_sum, + ], + ) + test.addModel("out", out) test.neuralizeModel(0.5) # Three semples with a dimensions 2 - result = test({'x': [0, 1, -2, 3], 'F': [-2, 2, 1, 5]}) - self.assertEqual(result['out_sum'],result['out']) + result = test({"x": [0, 1, -2, 3], "F": [-2, 2, 1, 5]}) + self.assertEqual(result["out_sum"], result["out"]) def test_integrate_derivate(self): NeuObj.clearNames() - input = Input('in1') - - in1_s = Output('in1_s', input.s(1)) - in1_s2 = Output('in1_s2', input.s(2)) - in1_s2_2 = Output('in1_s2_2', Differentiate(input.s(1))) - in1_s2_3 = Output('in1_s2_3', input.s(1).s(1)) - in1_s_2 = Output('in1_s_2', input.s(2).s(-1)) - - in1_sm = Output('in1_sm', input.s(-1)) - in1_sm2 = Output('in1_sm2', input.s(-2)) - in1_sm2_2 = Output('in1_sm2_2', Integrate(input.s(-1))) - in1_sm2_3 = Output('in1_sm2_3', input.s(-1).s(-1)) - in1_sm_2 = Output('in1_sm_2', input.s(-2).s(1)) - - in1_1 = Output('in1_1', Integrate(input.s(1))) - in1_2 = Output('in1_2', Integrate(Integrate(input.s(2)))) - in1_3 = Output('in1_3', Integrate(Integrate(Differentiate(input.s(1))))) - - in1_1_2 = Output('in1_1_2', Differentiate(input.s(-1))) - in1_2_2 = Output('in1_2_2', Differentiate(Differentiate(input.s(-2)))) - in1_3_2 = Output('in1_3_2', Differentiate(Differentiate(Integrate(input.s(-1))))) + input = Input("in1") + + in1_s = Output("in1_s", input.s(1)) + in1_s2 = Output("in1_s2", input.s(2)) + in1_s2_2 = Output("in1_s2_2", Differentiate(input.s(1))) + in1_s2_3 = Output("in1_s2_3", input.s(1).s(1)) + in1_s_2 = Output("in1_s_2", input.s(2).s(-1)) + + in1_sm = Output("in1_sm", input.s(-1)) + in1_sm2 = Output("in1_sm2", input.s(-2)) + in1_sm2_2 = Output("in1_sm2_2", Integrate(input.s(-1))) + in1_sm2_3 = Output("in1_sm2_3", input.s(-1).s(-1)) + in1_sm_2 = Output("in1_sm_2", input.s(-2).s(1)) + + in1_1 = Output("in1_1", Integrate(input.s(1))) + in1_2 = Output("in1_2", Integrate(Integrate(input.s(2)))) + in1_3 = Output("in1_3", Integrate(Integrate(Differentiate(input.s(1))))) + + in1_1_2 = Output("in1_1_2", Differentiate(input.s(-1))) + in1_2_2 = Output("in1_2_2", Differentiate(Differentiate(input.s(-2)))) + in1_3_2 = Output( + "in1_3_2", Differentiate(Differentiate(Integrate(input.s(-1)))) + ) test = Modely(visualizer=None) - test.addModel('out_A', [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2]) + test.addModel( + "out_A", + [ + in1_s, + in1_s2, + in1_s2_2, + in1_s2_3, + in1_s_2, + in1_sm, + in1_sm2, + in1_sm2_2, + in1_sm2_3, + in1_sm_2, + in1_1, + in1_2, + in1_3, + in1_1_2, + in1_2_2, + in1_3_2, + ], + ) test.neuralizeModel(1) - inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} + inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} results = test(inin) - self.assertEqual(results['in1_1'], inin['in1']) - self.assertEqual(results['in1_2'], inin['in1']) - self.assertEqual(results['in1_3'], inin['in1']) - self.assertEqual(results['in1_1_2'], inin['in1']) - self.assertEqual(results['in1_2_2'], inin['in1']) - self.assertEqual(results['in1_3_2'], inin['in1']) + self.assertEqual(results["in1_1"], inin["in1"]) + self.assertEqual(results["in1_2"], inin["in1"]) + self.assertEqual(results["in1_3"], inin["in1"]) + self.assertEqual(results["in1_1_2"], inin["in1"]) + self.assertEqual(results["in1_2_2"], inin["in1"]) + self.assertEqual(results["in1_3_2"], inin["in1"]) inin_s = [14, -13, 1, 1, 1, 1, 1, 1, 1, 1] inin_s2 = [14, -27, 14, 0, 0, 0, 0, 0, 0, 0] - self.assertEqual(results['in1_s'], inin_s) - self.assertEqual(results['in1_s_2'], inin_s) - self.assertEqual(results['in1_s2'], inin_s2) - self.assertEqual(results['in1_s2_2'], inin_s2) - self.assertEqual(results['in1_s2_3'], inin_s2) + self.assertEqual(results["in1_s"], inin_s) + self.assertEqual(results["in1_s_2"], inin_s) + self.assertEqual(results["in1_s2"], inin_s2) + self.assertEqual(results["in1_s2_2"], inin_s2) + self.assertEqual(results["in1_s2_3"], inin_s2) inin_sm = [14, 15, 17, 20, 24, 29, 35, 42, 50, 59] inin_sm2 = [14, 29, 46, 66, 90, 119, 154, 196, 246, 305] - self.assertEqual(results['in1_sm'], inin_sm) - self.assertEqual(results['in1_sm_2'], inin_sm) - self.assertEqual(results['in1_sm2'], inin_sm2) - self.assertEqual(results['in1_sm2_2'], inin_sm2) - self.assertEqual(results['in1_sm2_3'], inin_sm2) + self.assertEqual(results["in1_sm"], inin_sm) + self.assertEqual(results["in1_sm_2"], inin_sm) + self.assertEqual(results["in1_sm2"], inin_sm2) + self.assertEqual(results["in1_sm2_2"], inin_sm2) + self.assertEqual(results["in1_sm2_3"], inin_sm2) test = Modely(visualizer=None) - test.addModel('out_A', - [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2]) + test.addModel( + "out_A", + [ + in1_s, + in1_s2, + in1_s2_2, + in1_s2_3, + in1_s_2, + in1_sm, + in1_sm2, + in1_sm2_2, + in1_sm2_3, + in1_sm_2, + in1_1, + in1_2, + in1_3, + in1_1_2, + in1_2_2, + in1_3_2, + ], + ) test.neuralizeModel(0.01) - inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} + inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} results = test(inin) - self.TestAlmostEqual(results['in1_1'], inin['in1']) - self.TestAlmostEqual(results['in1_2'], inin['in1']) - self.TestAlmostEqual(results['in1_3'], inin['in1']) - self.TestAlmostEqual(results['in1_1_2'], inin['in1']) - self.TestAlmostEqual(results['in1_2_2'], inin['in1']) - self.TestAlmostEqual(results['in1_3_2'], inin['in1']) + self.TestAlmostEqual(results["in1_1"], inin["in1"]) + self.TestAlmostEqual(results["in1_2"], inin["in1"]) + self.TestAlmostEqual(results["in1_3"], inin["in1"]) + self.TestAlmostEqual(results["in1_1_2"], inin["in1"]) + self.TestAlmostEqual(results["in1_2_2"], inin["in1"]) + self.TestAlmostEqual(results["in1_3_2"], inin["in1"]) inin_s = [1400, -1300, 100, 100, 100, 100, 100, 100, 100, 100] inin_s2 = [140000, -270000, 140000, 0, 0, 0, 0, 0, 0, 0] - self.TestAlmostEqual(results['in1_s'], inin_s) - self.TestAlmostEqual(results['in1_s_2'], inin_s) - self.TestAlmostEqual(results['in1_s2'], inin_s2) - self.TestAlmostEqual(results['in1_s2_2'], inin_s2) - self.TestAlmostEqual(results['in1_s2_3'], inin_s2) + self.TestAlmostEqual(results["in1_s"], inin_s) + self.TestAlmostEqual(results["in1_s_2"], inin_s) + self.TestAlmostEqual(results["in1_s2"], inin_s2) + self.TestAlmostEqual(results["in1_s2_2"], inin_s2) + self.TestAlmostEqual(results["in1_s2_3"], inin_s2) inin_sm = [0.14, 0.15, 0.17, 0.20, 0.24, 0.29, 0.35, 0.42, 0.50, 0.59] - inin_sm2 = [0.0014, 0.0029, 0.0046, 0.0066, 0.0090, 0.0119, 0.0154, 0.0196, 0.0246, 0.0305] - self.TestAlmostEqual(results['in1_sm'], inin_sm) - self.TestAlmostEqual(results['in1_sm_2'], inin_sm) - self.TestAlmostEqual(results['in1_sm2'], inin_sm2) - self.TestAlmostEqual(results['in1_sm2_2'], inin_sm2) - self.TestAlmostEqual(results['in1_sm2_3'], inin_sm2) + inin_sm2 = [ + 0.0014, + 0.0029, + 0.0046, + 0.0066, + 0.0090, + 0.0119, + 0.0154, + 0.0196, + 0.0246, + 0.0305, + ] + self.TestAlmostEqual(results["in1_sm"], inin_sm) + self.TestAlmostEqual(results["in1_sm_2"], inin_sm) + self.TestAlmostEqual(results["in1_sm2"], inin_sm2) + self.TestAlmostEqual(results["in1_sm2_2"], inin_sm2) + self.TestAlmostEqual(results["in1_sm2_3"], inin_sm2) def test_integrate_derivate_trapezoidal(self): NeuObj.clearNames() - input = Input('in1') - - in1_s = Output('in1_s', input.s(1,method='trapezoidal')) - in1_s2 = Output('in1_s2', input.s(2,method='trapezoidal')) - in1_s2_2 = Output('in1_s2_2', Differentiate(input.s(1,method='trapezoidal'),method='trapezoidal')) - in1_s2_3 = Output('in1_s2_3', input.s(1,method='trapezoidal').s(1,method='trapezoidal')) - in1_s_2 = Output('in1_s_2', input.s(2,method='trapezoidal').s(-1,method='trapezoidal')) - - in1_sm = Output('in1_sm', input.s(-1,method='trapezoidal')) - in1_sm2 = Output('in1_sm2', input.s(-2,method='trapezoidal')) - in1_sm2_2 = Output('in1_sm2_2', Integrate(input.s(-1,method='trapezoidal'),method='trapezoidal')) - in1_sm2_3 = Output('in1_sm2_3', input.s(-1,method='trapezoidal').s(-1,method='trapezoidal')) - in1_sm_2 = Output('in1_sm_2', input.s(-2,method='trapezoidal').s(1,method='trapezoidal')) - - in1_1 = Output('in1_1', Integrate(input.s(1,method='trapezoidal'),method='trapezoidal')) - in1_2 = Output('in1_2', Integrate(Integrate(input.s(2,method='trapezoidal'),method='trapezoidal'),method='trapezoidal')) - in1_3 = Output('in1_3', Integrate(Integrate(Differentiate(input.s(1,method='trapezoidal'),method='trapezoidal'),method='trapezoidal'),method='trapezoidal')) - - in1_1_2 = Output('in1_1_2', Differentiate(input.s(-1,method='trapezoidal'),method='trapezoidal')) - in1_2_2 = Output('in1_2_2', Differentiate(Differentiate(input.s(-2,method='trapezoidal'),method='trapezoidal'),method='trapezoidal')) - in1_3_2 = Output('in1_3_2', Differentiate(Differentiate(Integrate(input.s(-1,method='trapezoidal'),method='trapezoidal'),method='trapezoidal'),method='trapezoidal')) + input = Input("in1") + + in1_s = Output("in1_s", input.s(1, method="trapezoidal")) + in1_s2 = Output("in1_s2", input.s(2, method="trapezoidal")) + in1_s2_2 = Output( + "in1_s2_2", + Differentiate(input.s(1, method="trapezoidal"), method="trapezoidal"), + ) + in1_s2_3 = Output( + "in1_s2_3", input.s(1, method="trapezoidal").s(1, method="trapezoidal") + ) + in1_s_2 = Output( + "in1_s_2", input.s(2, method="trapezoidal").s(-1, method="trapezoidal") + ) + + in1_sm = Output("in1_sm", input.s(-1, method="trapezoidal")) + in1_sm2 = Output("in1_sm2", input.s(-2, method="trapezoidal")) + in1_sm2_2 = Output( + "in1_sm2_2", + Integrate(input.s(-1, method="trapezoidal"), method="trapezoidal"), + ) + in1_sm2_3 = Output( + "in1_sm2_3", input.s(-1, method="trapezoidal").s(-1, method="trapezoidal") + ) + in1_sm_2 = Output( + "in1_sm_2", input.s(-2, method="trapezoidal").s(1, method="trapezoidal") + ) + + in1_1 = Output( + "in1_1", Integrate(input.s(1, method="trapezoidal"), method="trapezoidal") + ) + in1_2 = Output( + "in1_2", + Integrate( + Integrate(input.s(2, method="trapezoidal"), method="trapezoidal"), + method="trapezoidal", + ), + ) + in1_3 = Output( + "in1_3", + Integrate( + Integrate( + Differentiate( + input.s(1, method="trapezoidal"), method="trapezoidal" + ), + method="trapezoidal", + ), + method="trapezoidal", + ), + ) + + in1_1_2 = Output( + "in1_1_2", + Differentiate(input.s(-1, method="trapezoidal"), method="trapezoidal"), + ) + in1_2_2 = Output( + "in1_2_2", + Differentiate( + Differentiate(input.s(-2, method="trapezoidal"), method="trapezoidal"), + method="trapezoidal", + ), + ) + in1_3_2 = Output( + "in1_3_2", + Differentiate( + Differentiate( + Integrate(input.s(-1, method="trapezoidal"), method="trapezoidal"), + method="trapezoidal", + ), + method="trapezoidal", + ), + ) test = Modely(visualizer=None) - test.addModel('out_A', [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2]) + test.addModel( + "out_A", + [ + in1_s, + in1_s2, + in1_s2_2, + in1_s2_3, + in1_s_2, + in1_sm, + in1_sm2, + in1_sm2_2, + in1_sm2_3, + in1_sm_2, + in1_1, + in1_2, + in1_3, + in1_1_2, + in1_2_2, + in1_3_2, + ], + ) test.neuralizeModel(1) - inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} + inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} results = test(inin) - self.assertEqual(results['in1_1'], inin['in1']) - self.assertEqual(results['in1_2'], inin['in1']) - self.assertEqual(results['in1_3'], inin['in1']) - self.assertEqual(results['in1_1_2'], inin['in1']) - self.assertEqual(results['in1_2_2'], inin['in1']) - self.assertEqual(results['in1_3_2'], inin['in1']) - - inin_sm = [7., 14.5, 16., 18.5, 22., 26.5, 32., 38.5, 46., 54.5] - inin_sm2 = [ 3.5 , 14.25, 29.5 , 46.75, 67. , 91.25, 120.5 , 155.75, 198. , 248.25] - self.assertEqual(results['in1_sm'], inin_sm) - self.assertEqual(results['in1_sm_2'], inin_sm) - self.assertEqual(results['in1_sm2'], inin_sm2) - self.assertEqual(results['in1_sm2_2'], inin_sm2) - self.assertEqual(results['in1_sm2_3'], inin_sm2) + self.assertEqual(results["in1_1"], inin["in1"]) + self.assertEqual(results["in1_2"], inin["in1"]) + self.assertEqual(results["in1_3"], inin["in1"]) + self.assertEqual(results["in1_1_2"], inin["in1"]) + self.assertEqual(results["in1_2_2"], inin["in1"]) + self.assertEqual(results["in1_3_2"], inin["in1"]) + + inin_sm = [7.0, 14.5, 16.0, 18.5, 22.0, 26.5, 32.0, 38.5, 46.0, 54.5] + inin_sm2 = [3.5, 14.25, 29.5, 46.75, 67.0, 91.25, 120.5, 155.75, 198.0, 248.25] + self.assertEqual(results["in1_sm"], inin_sm) + self.assertEqual(results["in1_sm_2"], inin_sm) + self.assertEqual(results["in1_sm2"], inin_sm2) + self.assertEqual(results["in1_sm2_2"], inin_sm2) + self.assertEqual(results["in1_sm2_3"], inin_sm2) test = Modely(visualizer=None) - test.addModel('out_A', - [in1_s, in1_s2, in1_s2_2, in1_s2_3, in1_s_2, in1_sm, in1_sm2, in1_sm2_2, in1_sm2_3, in1_sm_2, in1_1, in1_2, in1_3, in1_1_2, in1_2_2, in1_3_2]) + test.addModel( + "out_A", + [ + in1_s, + in1_s2, + in1_s2_2, + in1_s2_3, + in1_s_2, + in1_sm, + in1_sm2, + in1_sm2_2, + in1_sm2_3, + in1_sm_2, + in1_1, + in1_2, + in1_3, + in1_1_2, + in1_2_2, + in1_3_2, + ], + ) test.neuralizeModel(0.01) - inin = {'in1': [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} + inin = {"in1": [14, 1, 2, 3, 4, 5, 6, 7, 8, 9]} results = test(inin) - self.TestAlmostEqual(results['in1_1'], inin['in1'], precision=4) - self.TestAlmostEqual(results['in1_2'], inin['in1'], precision=4) - self.TestAlmostEqual(results['in1_3'], inin['in1'], precision=4) - self.TestAlmostEqual(results['in1_1_2'], inin['in1'], precision=4) - self.TestAlmostEqual(results['in1_2_2'], inin['in1'], precision=3) - self.TestAlmostEqual(results['in1_3_2'], inin['in1'], precision=3) - - inin_sm = [ 0.07 , 0.145, 0.16 , 0.185, 0.22 , 0.265, 0.32 , 0.385, 0.46 , 0.545] - inin_sm2 = [0.00035 , 0.001425, 0.00295 , 0.004675, 0.0067 , 0.009125, 0.01205 , 0.015575, 0.0198 , 0.024825] - self.TestAlmostEqual(results['in1_sm'], inin_sm) - self.TestAlmostEqual(results['in1_sm_2'], inin_sm) - self.TestAlmostEqual(results['in1_sm2'], inin_sm2) - self.TestAlmostEqual(results['in1_sm2_2'], inin_sm2) - self.TestAlmostEqual(results['in1_sm2_3'], inin_sm2) + self.TestAlmostEqual(results["in1_1"], inin["in1"], precision=4) + self.TestAlmostEqual(results["in1_2"], inin["in1"], precision=4) + self.TestAlmostEqual(results["in1_3"], inin["in1"], precision=4) + self.TestAlmostEqual(results["in1_1_2"], inin["in1"], precision=4) + self.TestAlmostEqual(results["in1_2_2"], inin["in1"], precision=3) + self.TestAlmostEqual(results["in1_3_2"], inin["in1"], precision=3) + + inin_sm = [0.07, 0.145, 0.16, 0.185, 0.22, 0.265, 0.32, 0.385, 0.46, 0.545] + inin_sm2 = [ + 0.00035, + 0.001425, + 0.00295, + 0.004675, + 0.0067, + 0.009125, + 0.01205, + 0.015575, + 0.0198, + 0.024825, + ] + self.TestAlmostEqual(results["in1_sm"], inin_sm) + self.TestAlmostEqual(results["in1_sm_2"], inin_sm) + self.TestAlmostEqual(results["in1_sm2"], inin_sm2) + self.TestAlmostEqual(results["in1_sm2_2"], inin_sm2) + self.TestAlmostEqual(results["in1_sm2_3"], inin_sm2) def test_derivate_wrt_input(self): NeuObj.clearNames() - x = Input('x') + x = Input("x") x_last = x.last() def parametric_fun(x, a, b, c, d): import torch - return x ** 3 * a + x ** 2 * b + torch.sin(x) * c + d + + return x**3 * a + x**2 * b + torch.sin(x) * c + d def dx_parametric_fun(x, a, b, c, d): import torch - return (3 * x ** 2 * a) + (2 * x * b) + c * torch.cos(x) - fun = ParamFun(parametric_fun,['a','b','c','d'])(x_last) - approx_y = Output('out', fun) - approx_dy_dx = Output('d_out', Differentiate(fun, x_last)) + return (3 * x**2 * a) + (2 * x * b) + c * torch.cos(x) + + fun = ParamFun(parametric_fun, ["a", "b", "c", "d"])(x_last) + approx_y = Output("out", fun) + approx_dy_dx = Output("d_out", Differentiate(fun, x_last)) test = Modely(visualizer=None, seed=12) - test.addModel('model', [approx_dy_dx, approx_y]) + test.addModel("model", [approx_dy_dx, approx_y]) test.neuralizeModel() - results = test({'x':[1,2]}) - self.assertAlmostEqual(results['out'][0], parametric_fun(torch.tensor(1), torch.tensor(test.parameters['a']), - torch.tensor(test.parameters['b']), - torch.tensor(test.parameters['c']), - torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5) - self.assertAlmostEqual(results['d_out'][0], dx_parametric_fun(torch.tensor(1), torch.tensor(test.parameters['a']), - torch.tensor(test.parameters['b']), - torch.tensor(test.parameters['c']), - torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5) - self.assertAlmostEqual(results['out'][1], parametric_fun(torch.tensor(2), torch.tensor(test.parameters['a']), - torch.tensor(test.parameters['b']), - torch.tensor(test.parameters['c']), - torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5) - self.assertAlmostEqual(results['d_out'][1], dx_parametric_fun(torch.tensor(2), torch.tensor(test.parameters['a']), - torch.tensor(test.parameters['b']), - torch.tensor(test.parameters['c']), - torch.tensor(test.parameters['d'])).detach().numpy().tolist()[0],places=5) - + results = test({"x": [1, 2]}) + self.assertAlmostEqual( + results["out"][0], + parametric_fun( + torch.tensor(1), + torch.tensor(test.parameters["a"]), + torch.tensor(test.parameters["b"]), + torch.tensor(test.parameters["c"]), + torch.tensor(test.parameters["d"]), + ) + .detach() + .numpy() + .tolist()[0], + places=5, + ) + self.assertAlmostEqual( + results["d_out"][0], + dx_parametric_fun( + torch.tensor(1), + torch.tensor(test.parameters["a"]), + torch.tensor(test.parameters["b"]), + torch.tensor(test.parameters["c"]), + torch.tensor(test.parameters["d"]), + ) + .detach() + .numpy() + .tolist()[0], + places=5, + ) + self.assertAlmostEqual( + results["out"][1], + parametric_fun( + torch.tensor(2), + torch.tensor(test.parameters["a"]), + torch.tensor(test.parameters["b"]), + torch.tensor(test.parameters["c"]), + torch.tensor(test.parameters["d"]), + ) + .detach() + .numpy() + .tolist()[0], + places=5, + ) + self.assertAlmostEqual( + results["d_out"][1], + dx_parametric_fun( + torch.tensor(2), + torch.tensor(test.parameters["a"]), + torch.tensor(test.parameters["b"]), + torch.tensor(test.parameters["c"]), + torch.tensor(test.parameters["d"]), + ) + .detach() + .numpy() + .tolist()[0], + places=5, + ) diff --git a/tests/test_model_predict_recurrent.py b/tests/test_model_predict_recurrent.py index 5bbb37a8..b91b161d 100644 --- a/tests/test_model_predict_recurrent.py +++ b/tests/test_model_predict_recurrent.py @@ -19,17 +19,22 @@ # The second dimension indicates the output time dimension for each sample. # The third is the size of the signal + def myfun(x, P): - out = x*P - return out[:,1:,:] + out = x * P + return out[:, 1:, :] + def myfunsum(x, P): out = x + P return out -def matmul(x,y): + +def matmul(x, y): import torch - return torch.matmul(torch.transpose(x,1,2),y) + + return torch.matmul(torch.transpose(x, 1, 2), y) + # def myfun2(a, b ,c): # import torch @@ -41,10 +46,15 @@ def matmul(x,y): # bt = torch.transpose(b, 1, 2) # return torch.matmul(p1,at+bt)+p2.t() + class ModelyRecurrentPredictTest(unittest.TestCase): - def TestAlmostEqual(self, data1, data2, precision=4): - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) if type(data1) == type(data2) == list: self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): @@ -54,482 +64,666 @@ def TestAlmostEqual(self, data1, data2, precision=4): def test_predict_and_states_values_fir_simple_closed_loop(self): NeuObj.clearNames() - x = Input('x') - x_state = Input('x_state') - p = Parameter('p', dimensions=1, sw=1, values=[[1.0]]) + x = Input("x") + x_state = Input("x_state") + p = Parameter("p", dimensions=1, sw=1, values=[[1.0]]) rel_x = Fir(W=p)(x_state.last()) rel_x = ClosedLoop(rel_x, x_state) - out = Output('out', rel_x) + out = Output("out", rel_x) test = Modely(visualizer=None, seed=42) - test.addModel('out',out) - test.addMinimize('pos_x', x.next(), out) + test.addModel("out", out) + test.addMinimize("pos_x", x.next(), out) test.neuralizeModel(0.01) - result = test(inputs={'x': [2], 'x_state':[1]}) - self.assertEqual(test.states['x_state'], torch.tensor([[result['out']]]).tolist()) - self.assertEqual({'out': [1]}, result) - result = test(inputs={'x': [2]}) - self.assertEqual(test.states['x_state'], torch.tensor([[[1.0]]]).tolist()) - self.assertEqual({'out': [1.0]}, result) + result = test(inputs={"x": [2], "x_state": [1]}) + self.assertEqual( + test.states["x_state"], torch.tensor([[result["out"]]]).tolist() + ) + self.assertEqual({"out": [1]}, result) + result = test(inputs={"x": [2]}) + self.assertEqual(test.states["x_state"], torch.tensor([[[1.0]]]).tolist()) + self.assertEqual({"out": [1.0]}, result) test.resetStates() - result = test(inputs={'x': [2]}) - self.assertEqual(test.states['x_state'], torch.tensor([[[0.0]]]).tolist()) - self.assertEqual({'out': [0.0]}, result) + result = test(inputs={"x": [2]}) + self.assertEqual(test.states["x_state"], torch.tensor([[[0.0]]]).tolist()) + self.assertEqual({"out": [0.0]}, result) - test.removeConnection('x_state') + test.removeConnection("x_state") test.neuralizeModel(0.01) - result = test(inputs={'x': [2], 'x_state':[1]}) - self.assertEqual({'out': [1]}, result) - result = test(inputs={'x': [2]}) - self.assertEqual({'out': [0.0]}, result) - result = test(inputs={'x_state': [2.0]}) - self.assertEqual({'out': [2.0]}, result) - + result = test(inputs={"x": [2], "x_state": [1]}) + self.assertEqual({"out": [1]}, result) + result = test(inputs={"x": [2]}) + self.assertEqual({"out": [0.0]}, result) + result = test(inputs={"x_state": [2.0]}) + self.assertEqual({"out": [2.0]}, result) def test_predict_values_fir_simple_closed_loop_predict(self): NeuObj.clearNames() - x = Input('x') - x_in = Input('x_in') - p = Parameter('p', dimensions=1, sw=1, values=[[1.0]]) + x = Input("x") + x_in = Input("x_in") + p = Parameter("p", dimensions=1, sw=1, values=[[1.0]]) rel_x = Fir(W=p)(x_in.last()) - #rel_x = ClosedLoop(rel_x, x_state) - out = Output('out', rel_x) + # rel_x = ClosedLoop(rel_x, x_state) + out = Output("out", rel_x) test = Modely(visualizer=None, seed=42) - test.addModel('out',out) - test.addMinimize('pos_x', x.next(), out) + test.addModel("out", out) + test.addMinimize("pos_x", x.next(), out) test.neuralizeModel(0.01) - result = test(inputs={'x': [2], 'x_in':[1]},closed_loop={'x_in':'out'}) - self.assertEqual({'out':[1]}, result) - result = test(inputs={'x': [2]}, closed_loop={'x_in':'out'}) - self.assertEqual({'out': [0.0]}, result) + result = test(inputs={"x": [2], "x_in": [1]}, closed_loop={"x_in": "out"}) + self.assertEqual({"out": [1]}, result) + result = test(inputs={"x": [2]}, closed_loop={"x_in": "out"}) + self.assertEqual({"out": [0.0]}, result) def test_predict_values_fir_closed_loop(self): NeuObj.clearNames() ## the memory is not shared between different calls - x = Input('x') - F = Input('F') - p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - x_out = Fir(W=p)(x.tw(0.5))+F.last() + x = Input("x") + F = Input("F") + p = Parameter( + "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + x_out = Fir(W=p)(x.tw(0.5)) + F.last() x_out.closedLoop(F) - out = Output('out',x_out) + out = Output("out", x_out) test = Modely(visualizer=None, seed=42) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) ## one sample prediction with F initialized with zeros test.resetStates() - result = test(inputs={'x':[1,2,3,4,5]}) - self.assertEqual(result['out'], [15.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5]}) + self.assertEqual(result["out"], [15.0]) ## 5 samples prediction with F initialized with zero only the first time test.resetStates() - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]}) - self.assertEqual(result['out'], [15.0, 35.0, 60.0, 90.0, 125.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual(result["out"], [15.0, 35.0, 60.0, 90.0, 125.0]) ## one sample prediction with F initialized with [1] test.resetStates() - result = test(inputs={'x':[1,2,3,4,5], 'F':[1]}) - self.assertEqual(result['out'], [16.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1]}) + self.assertEqual(result["out"], [16.0]) ## 5 samples prediction with F initialized with [1] only the first time test.resetStates() - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1]}) - self.assertEqual(result['out'], [16.0, 36.0, 61.0, 91.0, 126.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1]}) + self.assertEqual(result["out"], [16.0, 36.0, 61.0, 91.0, 126.0]) ## 5 samples prediction with F initialized with [1] the first time, [2] the second time and [3] the third time test.resetStates() - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3]}) - self.assertEqual(result['out'], [16.0, 22.0, 28.0, 58.0, 93.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1, 2, 3]}) + self.assertEqual(result["out"], [16.0, 22.0, 28.0, 58.0, 93.0]) ## one sample prediction with F initialized with [1] (the other values are ignored) test.resetStates() - result = test(inputs={'x':[1,2,3,4,5], 'F':[1,2,3]}) - self.assertEqual(result['out'], [16.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1, 2, 3]}) + self.assertEqual(result["out"], [16.0]) def test_predict_values_fir_closed_loop_predict(self): NeuObj.clearNames() ## the memory is not shared between different calls - x = Input('x') - F = Input('F') - p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - x_out = Fir(W=p)(x.tw(0.5))+F.last() - out = Output('out',x_out) + x = Input("x") + F = Input("F") + p = Parameter( + "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + x_out = Fir(W=p)(x.tw(0.5)) + F.last() + out = Output("out", x_out) test = Modely(visualizer=None, seed=42) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) ## one sample prediction with F initialized with zeros - result = test(inputs={'x':[1,2,3,4,5]}, closed_loop={'F':'out'}) - self.assertEqual(result['out'], [15.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5]}, closed_loop={"F": "out"}) + self.assertEqual(result["out"], [15.0]) ## 5 samples prediction with F initialized with zero only the first time - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]}, closed_loop={'F':'out'}) - self.assertEqual(result['out'], [15.0, 35.0, 60.0, 90.0, 125.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]}, closed_loop={"F": "out"} + ) + self.assertEqual(result["out"], [15.0, 35.0, 60.0, 90.0, 125.0]) ## one sample prediction with F initialized with [1] - result = test(inputs={'x':[1,2,3,4,5], 'F':[1]}, closed_loop={'F':'out'}) - self.assertEqual(result['out'], [16.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1]}, closed_loop={"F": "out"}) + self.assertEqual(result["out"], [16.0]) ## 5 samples prediction with F initialized with [1] only the first time - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1]}, closed_loop={'F':'out'}) - self.assertEqual(result['out'], [16.0, 36.0, 61.0, 91.0, 126.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1]}, + closed_loop={"F": "out"}, + ) + self.assertEqual(result["out"], [16.0, 36.0, 61.0, 91.0, 126.0]) ## 5 samples prediction with F initialized with [1] the first time, [2] the second time and [3] the third time - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3]}, closed_loop={'F':'out'}) - self.assertEqual(result['out'], [16.0, 22.0, 28.0, 58.0, 93.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9], "F": [1, 2, 3]}, + closed_loop={"F": "out"}, + ) + self.assertEqual(result["out"], [16.0, 22.0, 28.0, 58.0, 93.0]) ## one sample prediction with F initialized with [1] (the other values are ignored) - result = test(inputs={'x':[1,2,3,4,5], 'F':[1,2,3]}, closed_loop={'F':'out'}) - self.assertEqual(result['out'], [16.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "F": [1, 2, 3]}, closed_loop={"F": "out"} + ) + self.assertEqual(result["out"], [16.0]) def test_predict_values_2fir_closed_loop(self): NeuObj.clearNames() ## the memory is not shared between different calls - x = Input('x') - y = Input('y') - p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - n = Parameter('n', tw=0.5, dimensions=1, values=[[-1.0],[-1.0],[-1.0],[-1.0],[-1.0]]) + x = Input("x") + y = Input("y") + p = Parameter( + "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + n = Parameter( + "n", tw=0.5, dimensions=1, values=[[-1.0], [-1.0], [-1.0], [-1.0], [-1.0]] + ) fir_pos = Fir(W=p)(x.tw(0.5)) fir_neg = Fir(W=n)(y.tw(0.5)) fir_pos.closedLoop(x) fir_neg.closedLoop(y) - out_pos = Output('out_pos', fir_pos) - out_neg = Output('out_neg', fir_neg) - out = Output('out',fir_neg+fir_pos) + out_pos = Output("out_pos", fir_pos) + out_neg = Output("out_neg", fir_neg) + out = Output("out", fir_neg + fir_pos) test = Modely(visualizer=None, seed=42) - test.addModel('out', out) - test.addModel('out_pos',out_pos) - test.addModel('out_neg',out_neg) + test.addModel("out", out) + test.addModel("out_pos", out_pos) + test.addModel("out_neg", out_neg) test.neuralizeModel(0.1) ## one sample prediction with both close loops - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]}) - self.assertEqual(result['out'], [0.0]) - self.assertEqual(result['out_pos'], [15.0]) - self.assertEqual(result['out_neg'], [-15.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]}) + self.assertEqual(result["out"], [0.0]) + self.assertEqual(result["out_pos"], [15.0]) + self.assertEqual(result["out_neg"], [-15.0]) ## three sample prediction due to the max dimensions of inputs + prediction_samples - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]}, prediction_samples=2, num_of_samples=3) - self.assertEqual(result['out'], [0.0, 30.0, 58.0]) - self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0]) - self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]}, + prediction_samples=2, + num_of_samples=3, + ) + self.assertEqual(result["out"], [0.0, 30.0, 58.0]) + self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0]) + self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0]) ## three sample prediction with both close loops but y gets initialized for 3 steps ## (!! since all the inputs are recurrent we must specify the prediction horizon (defualt=1)) - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5,6,7]}, prediction_samples=2, num_of_samples=3) - self.assertEqual(result['out'], [0.0, 30.0, 58.0]) - self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0]) - self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6, 7]}, + prediction_samples=2, + num_of_samples=3, + ) + self.assertEqual(result["out"], [0.0, 30.0, 58.0]) + self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0]) + self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0]) test.resetStates() - result = test(inputs={'x': [1, 2, 3, 4, 5], 'y': [1, 2, 3, 4, 5, 6, 7]}, num_of_samples=3) - self.assertEqual(result['out'], [0.0, 9.0, 31.0]) - self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0]) - self.assertEqual(result['out_neg'], [-15.0, -20.0, -25.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6, 7]}, num_of_samples=3 + ) + self.assertEqual(result["out"], [0.0, 9.0, 31.0]) + self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0]) + self.assertEqual(result["out_neg"], [-15.0, -20.0, -25.0]) def test_predict_values_2fir_closed_loop_predict(self): NeuObj.clearNames() ## the memory is not shared between different calls - x = Input('x') - y = Input('y') - p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - n = Parameter('n', tw=0.5, dimensions=1, values=[[-1.0],[-1.0],[-1.0],[-1.0],[-1.0]]) + x = Input("x") + y = Input("y") + p = Parameter( + "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + n = Parameter( + "n", tw=0.5, dimensions=1, values=[[-1.0], [-1.0], [-1.0], [-1.0], [-1.0]] + ) fir_pos = Fir(W=p)(x.tw(0.5)) fir_neg = Fir(W=n)(y.tw(0.5)) - out_pos = Output('out_pos', fir_pos) - out_neg = Output('out_neg', fir_neg) - out = Output('out',fir_neg+fir_pos) + out_pos = Output("out_pos", fir_pos) + out_neg = Output("out_neg", fir_neg) + out = Output("out", fir_neg + fir_pos) test = Modely(visualizer=None, seed=42) - test.addModel('out', out) - test.addModel('out_pos',out_pos) - test.addModel('out_neg',out_neg) + test.addModel("out", out) + test.addModel("out_pos", out_pos) + test.addModel("out_neg", out_neg) test.neuralizeModel(0.1) ## two sample one prediction for x in close loop - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5,6]}, closed_loop={'x':'out_pos'}) - self.assertEqual(result['out'], [0.0, 9.0]) - self.assertEqual(result['out_pos'], [15.0, 29.0]) - self.assertEqual(result['out_neg'], [-15.0, -20.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6]}, + closed_loop={"x": "out_pos"}, + ) + self.assertEqual(result["out"], [0.0, 9.0]) + self.assertEqual(result["out_pos"], [15.0, 29.0]) + self.assertEqual(result["out_neg"], [-15.0, -20.0]) ## two sample one prediction for y in close loop - result = test(inputs={'x':[1,2,3,4,5,6], 'y':[1,2,3,4,5]}, closed_loop={'y':'out_pos'}) - self.assertEqual(result['out'], [0.0, -9.0]) - self.assertEqual(result['out_pos'], [15.0, 20.0]) - self.assertEqual(result['out_neg'], [-15.0, -29.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5, 6], "y": [1, 2, 3, 4, 5]}, + closed_loop={"y": "out_pos"}, + ) + self.assertEqual(result["out"], [0.0, -9.0]) + self.assertEqual(result["out_pos"], [15.0, 20.0]) + self.assertEqual(result["out_neg"], [-15.0, -29.0]) ## one sample prediction with both close loops - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]}, closed_loop={'x':'out_pos', 'y':'out_neg'}) - self.assertEqual(result['out'], [0.0]) - self.assertEqual(result['out_pos'], [15.0]) - self.assertEqual(result['out_neg'], [-15.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]}, + closed_loop={"x": "out_pos", "y": "out_neg"}, + ) + self.assertEqual(result["out"], [0.0]) + self.assertEqual(result["out_pos"], [15.0]) + self.assertEqual(result["out_neg"], [-15.0]) ## three sample prediction due to the max dimensions of inputs + prediction_samples - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5]}, closed_loop={'x':'out_pos', 'y':'out_neg'}, prediction_samples=2, num_of_samples=3) - self.assertEqual(result['out'], [0.0, 30.0, 58.0]) - self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0]) - self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5]}, + closed_loop={"x": "out_pos", "y": "out_neg"}, + prediction_samples=2, + num_of_samples=3, + ) + self.assertEqual(result["out"], [0.0, 30.0, 58.0]) + self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0]) + self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0]) ## three sample prediction with both close loops but y gets initialized for 3 steps ## (!! since all the inputs are recurrent we must specify the prediction horizon (defualt=1)) - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5,6,7]}, closed_loop={'x':'out_pos', 'y':'out_neg'}, prediction_samples=2, num_of_samples=3) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5, 6, 7]}, + closed_loop={"x": "out_pos", "y": "out_neg"}, + prediction_samples=2, + num_of_samples=3, + ) ## 1+2+3+4+5 -1-2-3-4-5 2+3+4+5+15 -2-3-4-5+15 - #self.assertEqual(result['out'], [0.0, 9.0, 31.0]) - self.assertEqual(result['out_pos'], [15.0, 29.0, 56.0]) - self.assertEqual(result['out_neg'], [-15.0, 1.0, 2.0]) + # self.assertEqual(result['out'], [0.0, 9.0, 31.0]) + self.assertEqual(result["out_pos"], [15.0, 29.0, 56.0]) + self.assertEqual(result["out_neg"], [-15.0, 1.0, 2.0]) def test_predict_values_3states_closed_loop(self): NeuObj.clearNames() ## the state is saved inside the model so the memory is shared between different calls - x = Input('x') - F_state = Input('F') - y_state = Input('y') - z_state = Input('z') - p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - x_out = Fir(W=p)(x.tw(0.5))+F_state.last()+y_state.last()+z_state.last() + x = Input("x") + F_state = Input("F") + y_state = Input("y") + z_state = Input("z") + p = Parameter( + "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + x_out = Fir(W=p)(x.tw(0.5)) + F_state.last() + y_state.last() + z_state.last() x_out = ClosedLoop(x_out, F_state) x_out = ClosedLoop(x_out, y_state) x_out = ClosedLoop(x_out, z_state) - out = Output('out',x_out) + out = Output("out", x_out) test = Modely(visualizer=None, seed=42) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) ## one sample prediction with state variables not initialized ## (they will have the last valid state) - result = test(inputs={'x':[1,2,3,4,5]}) - self.assertEqual(result['out'], [15.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5]}) + self.assertEqual(result["out"], [15.0]) ## 5 sample prediction with state variables not initialized ## (the first prediction will preserve the state of the previous test [15.0]) - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]}) - self.assertEqual(result['out'], [60.0, 200.0, 625.0, 1905.0, 5750.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual(result["out"], [60.0, 200.0, 625.0, 1905.0, 5750.0]) test.resetStates() - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]}) - self.assertEqual(result['out'], [15.0, 65.0, 220.0, 220*3+30, (220*3+30)*3+35]) + result = test(inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]}) + self.assertEqual( + result["out"], [15.0, 65.0, 220.0, 220 * 3 + 30, (220 * 3 + 30) * 3 + 35] + ) ## one sample prediction with state variables initialized with zero test.resetStates() - result = test(inputs={'x':[1,2,3,4,5]}) - self.assertEqual(result['out'], [15.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5]}) + self.assertEqual(result["out"], [15.0]) ## one sample prediction with F initialized with [1] and the others not initialized (so they will have 15.0 in the memory) - result = test(inputs={'x':[1,2,3,4,5], 'F':[1]}) - self.assertEqual(result['out'], [46.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1]}) + self.assertEqual(result["out"], [46.0]) ## one sample prediction with all the state variables initialized - result = test(inputs={'x':[1,2,3,4,5], 'F':[1], 'y':[2], 'z':[3]}) - self.assertEqual(result['out'], [21.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5], "F": [1], "y": [2], "z": [3]}) + self.assertEqual(result["out"], [21.0]) ## 5 samples prediction with state variables initialized as many times as they have values to take - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3], 'y':[2,3], 'z':[3]}) - self.assertEqual(result['out'], [21.0, 46.0, 120.0, 390.0, 1205.0]) + result = test( + inputs={ + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "F": [1, 2, 3], + "y": [2, 3], + "z": [3], + } + ) + self.assertEqual(result["out"], [21.0, 46.0, 120.0, 390.0, 1205.0]) # 2 samples prediction with state variables inizialized only at %prediction_samples - result = test(inputs={'F': [1,2,3,4], 'y': [1,2], 'z': [1,2,3,4,5]}, prediction_samples=2, num_of_samples=4) + result = test( + inputs={"F": [1, 2, 3, 4], "y": [1, 2], "z": [1, 2, 3, 4, 5]}, + prediction_samples=2, + num_of_samples=4, + ) # 1+1+1 = 3, 3+3+3 = 9, 9+9+9 = 27, 4+0+4 = 8, 8+8+8 = 24 - self.assertEqual(result['out'], [3.0, 9.0, 27.0, 8.0]) - #self.assertEqual(result['out'], [3.0,9.0,27.0,8.0]) - #self.assertEqual(result['out'], [3.0, 6.0, 12.0, 20.0]) + self.assertEqual(result["out"], [3.0, 9.0, 27.0, 8.0]) + # self.assertEqual(result['out'], [3.0,9.0,27.0,8.0]) + # self.assertEqual(result['out'], [3.0, 6.0, 12.0, 20.0]) def test_predict_values_3states_closed_loop_predict(self): NeuObj.clearNames() ## the state is saved inside the model so the memory is shared between different calls - x = Input('x') - F_state = Input('F') - y_state = Input('y') - z_state = Input('z') - p = Parameter('p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - x_out = Fir(W=p)(x.tw(0.5))+F_state.last()+y_state.last()+z_state.last() - out = Output('out',x_out) + x = Input("x") + F_state = Input("F") + y_state = Input("y") + z_state = Input("z") + p = Parameter( + "p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + x_out = Fir(W=p)(x.tw(0.5)) + F_state.last() + y_state.last() + z_state.last() + out = Output("out", x_out) test = Modely(visualizer=None, seed=42) - test.addModel('out',out) + test.addModel("out", out) test.neuralizeModel(0.1) ## one sample prediction with state variables not initialized ## (they will have the last valid state) - result = test(inputs={'x':[1,2,3,4,5]},closed_loop={'F':'out','y':'out','z':'out'}) - self.assertEqual(result['out'], [15.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5]}, + closed_loop={"F": "out", "y": "out", "z": "out"}, + ) + self.assertEqual(result["out"], [15.0]) ## 5 sample prediction with state variables not initialized ## (the first prediction will preserve the state of the previous test [15.0]) - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9]}, closed_loop={'F':'out','y':'out','z':'out'}) - self.assertEqual(result['out'], [15.0, 65.0, 220.0, 220*3+30, (220*3+30)*3+35]) + result = test( + inputs={"x": [1, 2, 3, 4, 5, 6, 7, 8, 9]}, + closed_loop={"F": "out", "y": "out", "z": "out"}, + ) + self.assertEqual( + result["out"], [15.0, 65.0, 220.0, 220 * 3 + 30, (220 * 3 + 30) * 3 + 35] + ) ## one sample prediction with state variables initialized with zero test.resetStates() - result = test(inputs={'x':[1,2,3,4,5]}, closed_loop={'F':'out','y':'out','z':'out'}) - self.assertEqual(result['out'], [15.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5]}, + closed_loop={"F": "out", "y": "out", "z": "out"}, + ) + self.assertEqual(result["out"], [15.0]) ## one sample prediction with F initialized with [1] and the others not initialized (so they will have 15.0 in the memory) - result = test(inputs={'x':[1,2,3,4,5], 'F':[1]}, closed_loop={'F':'out','y':'out','z':'out'}) - self.assertEqual(result['out'], [16.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "F": [1]}, + closed_loop={"F": "out", "y": "out", "z": "out"}, + ) + self.assertEqual(result["out"], [16.0]) ## one sample prediction with all the state variables initialized - result = test(inputs={'x':[1,2,3,4,5], 'F':[1], 'y':[2], 'z':[3]}, closed_loop={'F':'out','y':'out','z':'out'}) - self.assertEqual(result['out'], [21.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "F": [1], "y": [2], "z": [3]}, + closed_loop={"F": "out", "y": "out", "z": "out"}, + ) + self.assertEqual(result["out"], [21.0]) ## 5 samples prediction with state variables initialized as many times as they have values to take - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'F':[1,2,3], 'y':[2,3], 'z':[3]}, closed_loop={'F':'out','y':'out','z':'out'}) - self.assertEqual(result['out'], [21.0, 46.0, 120.0, 390.0, 1205.0]) + result = test( + inputs={ + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "F": [1, 2, 3], + "y": [2, 3], + "z": [3], + }, + closed_loop={"F": "out", "y": "out", "z": "out"}, + ) + self.assertEqual(result["out"], [21.0, 46.0, 120.0, 390.0, 1205.0]) # 2 samples prediction with state variables inizialized only at %prediction_samples # 1+1+1 = 3, 3+3+3 = 9, 9+9+9 = 27, 4+0+4 = 8, 8+8+8 = 24 - result = test(inputs={'F': [1,2,3,4], 'y': [1,2], 'z': [1,2,3,4,5]}, closed_loop={'F':'out','y':'out','z':'out'}, prediction_samples=2, num_of_samples=4) - self.assertEqual(result['out'], [3.0,9.0,27.0,8.0]) + result = test( + inputs={"F": [1, 2, 3, 4], "y": [1, 2], "z": [1, 2, 3, 4, 5]}, + closed_loop={"F": "out", "y": "out", "z": "out"}, + prediction_samples=2, + num_of_samples=4, + ) + self.assertEqual(result["out"], [3.0, 9.0, 27.0, 8.0]) def test_predict_values_and_states_3states_more_window_closed_loop(self): NeuObj.clearNames() ## the state is saved inside the model so the memory is shared between different calls - x = Input('x') - y_state = Input('y') - z_state = Input('z') - x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]]) - z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]]) + x = Input("x") + y_state = Input("y") + z_state = Input("z") + x_p = Parameter( + "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + y_p = Parameter( + "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]] + ) + z_p = Parameter( + "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]] + ) x_fir = Fir(W=x_p)(x.tw(0.5)) y_fir = Fir(W=y_p)(y_state.tw(0.5)) z_fir = Fir(W=z_p)(z_state.tw(0.5)) y_fir = ClosedLoop(y_fir, y_state) z_fir = ClosedLoop(z_fir, z_state) - out_x = Output('out_x', x_fir) - out_y = Output('out_y', y_fir) - out_z = Output('out_z', z_fir) - out = Output('out',x_fir+y_fir+z_fir) + out_x = Output("out_x", x_fir) + out_y = Output("out_y", y_fir) + out_z = Output("out_z", z_fir) + out = Output("out", x_fir + y_fir + z_fir) test = Modely(visualizer=None, seed=42) - test.addModel('out_all',[out, out_x, out_y, out_z]) + test.addModel("out_all", [out, out_x, out_y, out_z]) test.neuralizeModel(0.1) ## one sample prediction with state variables not initialized ## (they will have the last valid state) - result = test(inputs={'x':[1,2,3,4,5]}) - self.assertEqual(result['out'], [15.0]) - self.assertEqual(result['out_x'], [15.0]) - self.assertEqual(result['out_y'], [0.0]) - self.assertEqual(result['out_z'], [0.0]) - self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) - self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + result = test(inputs={"x": [1, 2, 3, 4, 5]}) + self.assertEqual(result["out"], [15.0]) + self.assertEqual(result["out_x"], [15.0]) + self.assertEqual(result["out_y"], [0.0]) + self.assertEqual(result["out_z"], [0.0]) + self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + self.assertEqual(test.states["z"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) ## 1 sample prediction with state variables all initialized - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]}) - self.assertEqual(result['out'], [90.0]) - self.assertEqual(result['out_x'], [15.0]) - self.assertEqual(result['out_y'], [30.0]) - self.assertEqual(result['out_z'], [45.0]) - self.assertEqual(test.states['y'], [[[2.0], [3.0], [4.0], [5.0], [30.0]]]) - self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [45.0]]]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]} + ) + self.assertEqual(result["out"], [90.0]) + self.assertEqual(result["out_x"], [15.0]) + self.assertEqual(result["out_y"], [30.0]) + self.assertEqual(result["out_z"], [45.0]) + self.assertEqual(test.states["y"], [[[2.0], [3.0], [4.0], [5.0], [30.0]]]) + self.assertEqual(test.states["z"], [[[2.0], [3.0], [4.0], [5.0], [45.0]]]) ## clear state of y - test.resetStates({'y'}) - self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) - self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [45.0]]]) + test.resetStates({"y"}) + self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + self.assertEqual(test.states["z"], [[[2.0], [3.0], [4.0], [5.0], [45.0]]]) ## multi-sample prediction with states initialized as many times as they have values - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]}) - self.assertEqual(result['out'], [90.0, 120.0, 309.0, 1101.0, 4155.0]) - self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0]) - self.assertEqual(result['out_y'], [30.0, 40.0, 50.0, 144.0, 424.0]) - self.assertEqual(result['out_z'], [45.0, 60.0, 234.0, 927.0, 3696.0]) - self.assertEqual(test.states['y'], [[[6.0], [7.0], [50.0], [144.0], [424.0]]]) - self.assertEqual(test.states['z'], [[[6.0], [60.0], [234.0], [927.0], [3696.0]]]) + result = test( + inputs={ + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "y": [1, 2, 3, 4, 5, 6, 7], + "z": [1, 2, 3, 4, 5, 6], + } + ) + self.assertEqual(result["out"], [90.0, 120.0, 309.0, 1101.0, 4155.0]) + self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0]) + self.assertEqual(result["out_y"], [30.0, 40.0, 50.0, 144.0, 424.0]) + self.assertEqual(result["out_z"], [45.0, 60.0, 234.0, 927.0, 3696.0]) + self.assertEqual(test.states["y"], [[[6.0], [7.0], [50.0], [144.0], [424.0]]]) + self.assertEqual( + test.states["z"], [[[6.0], [60.0], [234.0], [927.0], [3696.0]]] + ) ## Clear all states test.resetStates() - self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) - self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + self.assertEqual(test.states["z"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) def test_predict_values_and_states_3states_more_window_closed_loop_predict(self): NeuObj.clearNames() ## the state is saved inside the model so the memory is shared between different calls - x = Input('x') - y_state = Input('y') - z_state = Input('z') - x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]]) - z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]]) + x = Input("x") + y_state = Input("y") + z_state = Input("z") + x_p = Parameter( + "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + y_p = Parameter( + "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]] + ) + z_p = Parameter( + "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]] + ) x_fir = Fir(W=x_p)(x.tw(0.5)) y_fir = Fir(W=y_p)(y_state.tw(0.5)) z_fir = Fir(W=z_p)(z_state.tw(0.5)) - out_x = Output('out_x', x_fir) - out_y = Output('out_y', y_fir) - out_z = Output('out_z', z_fir) - out = Output('out',x_fir+y_fir+z_fir) + out_x = Output("out_x", x_fir) + out_y = Output("out_y", y_fir) + out_z = Output("out_z", z_fir) + out = Output("out", x_fir + y_fir + z_fir) test = Modely(visualizer=None, seed=42) - test.addModel('out_all',[out, out_x, out_y, out_z]) + test.addModel("out_all", [out, out_x, out_y, out_z]) test.neuralizeModel(0.1) ## one sample prediction with state variables not initialized ## (they will have the last valid state) - result = test(inputs={'x':[1,2,3,4,5]}, closed_loop={'y':'out_y', 'z':'out_z'}) - self.assertEqual(result['out'], [15.0]) - self.assertEqual(result['out_x'], [15.0]) - self.assertEqual(result['out_y'], [0.0]) - self.assertEqual(result['out_z'], [0.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5]}, closed_loop={"y": "out_y", "z": "out_z"} + ) + self.assertEqual(result["out"], [15.0]) + self.assertEqual(result["out_x"], [15.0]) + self.assertEqual(result["out_y"], [0.0]) + self.assertEqual(result["out_z"], [0.0]) ## 1 sample prediction with state variables all initialized - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]}, closed_loop={'y':'out_y', 'z':'out_z'}) - self.assertEqual(result['out'], [90.0]) - self.assertEqual(result['out_x'], [15.0]) - self.assertEqual(result['out_y'], [30.0]) - self.assertEqual(result['out_z'], [45.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]}, + closed_loop={"y": "out_y", "z": "out_z"}, + ) + self.assertEqual(result["out"], [90.0]) + self.assertEqual(result["out_x"], [15.0]) + self.assertEqual(result["out_y"], [30.0]) + self.assertEqual(result["out_z"], [45.0]) ## multi-sample prediction with states initialized as many times as they have values - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]}, closed_loop={'y':'out_y', 'z':'out_z'}) - self.assertEqual(result['out'], [90.0, 120.0, 309.0, 1101.0, 4155.0]) - self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0]) - self.assertEqual(result['out_y'], [30.0, 40.0, 50.0, 144.0, 424.0]) - self.assertEqual(result['out_z'], [45.0, 60.0, 234.0, 927.0, 3696.0]) + result = test( + inputs={ + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "y": [1, 2, 3, 4, 5, 6, 7], + "z": [1, 2, 3, 4, 5, 6], + }, + closed_loop={"y": "out_y", "z": "out_z"}, + ) + self.assertEqual(result["out"], [90.0, 120.0, 309.0, 1101.0, 4155.0]) + self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0]) + self.assertEqual(result["out_y"], [30.0, 40.0, 50.0, 144.0, 424.0]) + self.assertEqual(result["out_z"], [45.0, 60.0, 234.0, 927.0, 3696.0]) def test_predict_values_and_states_2states_more_window_connect(self): NeuObj.clearNames() ## the state is saved inside the model so the memory is shared between different calls - x = Input('x') - y_state = Input('y') - z_state = Input('z') - x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]]) - z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]]) + x = Input("x") + y_state = Input("y") + z_state = Input("z") + x_p = Parameter( + "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + y_p = Parameter( + "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]] + ) + z_p = Parameter( + "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]] + ) x_fir = Fir(W=x_p)(x.tw(0.5)) y_fir = Fir(W=y_p)(y_state.tw(0.5)) z_fir = Fir(W=z_p)(z_state.tw(0.5)) x_fir = Connect(x_fir, y_state) x_fir = Connect(x_fir, z_state) - out_x = Output('out_x', x_fir) - out_y = Output('out_y', y_fir) - out_z = Output('out_z', z_fir) - out = Output('out',x_fir+y_fir+z_fir) + out_x = Output("out_x", x_fir) + out_y = Output("out_y", y_fir) + out_z = Output("out_z", z_fir) + out = Output("out", x_fir + y_fir + z_fir) test = Modely(visualizer=None, seed=42) - test.addModel('out_all',[out, out_x, out_y, out_z]) + test.addModel("out_all", [out, out_x, out_y, out_z]) test.neuralizeModel(0.1) ## one sample prediction with state variables not initialized ## (they will have the last valid state) - result = test(inputs={'x':[1,2,3,4,5]}) - self.assertEqual(result['out'], [90.0]) - self.assertEqual(result['out_x'], [15.0]) - self.assertEqual(result['out_y'], [30.0]) - self.assertEqual(result['out_z'], [45.0]) + result = test(inputs={"x": [1, 2, 3, 4, 5]}) + self.assertEqual(result["out"], [90.0]) + self.assertEqual(result["out_x"], [15.0]) + self.assertEqual(result["out_y"], [30.0]) + self.assertEqual(result["out_z"], [45.0]) # self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [15.0]]]) # self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [15.0]]]) - self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [15.0], [float('inf')]]]) - self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [15.0], [float('inf')]]]) + self.assertEqual( + test.states["y"], [[[0.0], [0.0], [0.0], [15.0], [float("inf")]]] + ) + self.assertEqual( + test.states["z"], [[[0.0], [0.0], [0.0], [15.0], [float("inf")]]] + ) # Replace insead of rolling # self.assertEqual(test.model.states['y'].numpy().tolist(), [[[0.0], [0.0], [0.0], [15.0], [0.0]]]) # self.assertEqual(test.model.states['z'].numpy().tolist(), [[[0.0], [0.0], [0.0], [15.0], [0.0]]]) ## 1 sample prediction with state variables all initialized - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]}) - self.assertEqual(result['out_x'], [15.0]) - #(1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3 + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]} + ) + self.assertEqual(result["out_x"], [15.0]) + # (1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3 # self.assertEqual(result['out'], [160.0]) # self.assertEqual(result['out_y'], [58.0]) # self.assertEqual(result['out_z'], [87.0]) - self.assertEqual(result['out'], [140.0]) # fir_x = (1+2+3+4+5)*1 fir_y = (1+2+3+4+15)*2 fir_z = (1+2+3+4+15)*3 result total = 140 - self.assertEqual(result['out_y'], [50.0]) - self.assertEqual(result['out_z'], [75.0]) + self.assertEqual( + result["out"], [140.0] + ) # fir_x = (1+2+3+4+5)*1 fir_y = (1+2+3+4+15)*2 fir_z = (1+2+3+4+15)*3 result total = 140 + self.assertEqual(result["out_y"], [50.0]) + self.assertEqual(result["out_z"], [75.0]) # self.assertEqual(test.states['y'], [[[2.0], [3.0], [4.0], [5.0], [15.0]]]) # self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [15.0]]]) - self.assertEqual(test.states['y'], [[[2.0], [3.0], [4.0], [15.0], [float('inf')]]]) - self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [15.0], [float('inf')]]]) + self.assertEqual( + test.states["y"], [[[2.0], [3.0], [4.0], [15.0], [float("inf")]]] + ) + self.assertEqual( + test.states["z"], [[[2.0], [3.0], [4.0], [15.0], [float("inf")]]] + ) # Replace instead of rolling - #(1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3 + # (1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3 # self.assertEqual(result['out'], [140.0]) # self.assertEqual(result['out_y'], [50.0]) # self.assertEqual(result['out_z'], [75.0]) # self.assertEqual(test.model.states['y'].numpy().tolist(), [[[2.0], [3.0], [4.0], [15.0], [1.0]]]) # self.assertEqual(test.model.states['z'].numpy().tolist(), [[[2.0], [3.0], [4.0], [15.0], [1.0]]]) ## clear state of y - test.resetStates({'y'}) - self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + test.resetStates({"y"}) + self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) # self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [5.0], [15.0]]]) - self.assertEqual(test.states['z'], [[[2.0], [3.0], [4.0], [15.0], [float('inf')]]]) + self.assertEqual( + test.states["z"], [[[2.0], [3.0], [4.0], [15.0], [float("inf")]]] + ) # # Replace insead of rolling # ## clear state of y # test.resetStates({'y'}) # self.assertEqual(test.model.states['y'].numpy().tolist(), [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) # self.assertEqual(test.model.states['z'].numpy().tolist(), [[[2.0], [3.0], [4.0], [15.0], [1.0]]]) ## multi-sample prediction with states initialized as many times as they have values - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]}) - self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0]) - self.assertEqual(result['out_y'], [2*(1+2+3+4+15), 2*(2+3+4+5+20), 2*(3+4+5+6+25), 2*(4+5+6+25+30), 2*(5+6+25+30+35)]) - self.assertEqual(result['out_z'], [3*(1+2+3+4+15), 3*(2+3+4+5+20), 3*(3+4+5+20+25), 3*(4+5+20+25+30), 3*(5+20+25+30+35)]) - self.assertEqual(result['out'], [sum(x) for x in zip(result['out_x'],result['out_y'],result['out_z'])]) + result = test( + inputs={ + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "y": [1, 2, 3, 4, 5, 6, 7], + "z": [1, 2, 3, 4, 5, 6], + } + ) + self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0]) + self.assertEqual( + result["out_y"], + [ + 2 * (1 + 2 + 3 + 4 + 15), + 2 * (2 + 3 + 4 + 5 + 20), + 2 * (3 + 4 + 5 + 6 + 25), + 2 * (4 + 5 + 6 + 25 + 30), + 2 * (5 + 6 + 25 + 30 + 35), + ], + ) + self.assertEqual( + result["out_z"], + [ + 3 * (1 + 2 + 3 + 4 + 15), + 3 * (2 + 3 + 4 + 5 + 20), + 3 * (3 + 4 + 5 + 20 + 25), + 3 * (4 + 5 + 20 + 25 + 30), + 3 * (5 + 20 + 25 + 30 + 35), + ], + ) + self.assertEqual( + result["out"], + [sum(x) for x in zip(result["out_x"], result["out_y"], result["out_z"])], + ) # self.assertEqual(test.states['y'], [[[6.0], [7.0], [25.0], [30.0], [35.0]]]) # self.assertEqual(test.states['z'], [[[6.0], [20.0], [25.0], [30.0], [35.0]]]) - self.assertEqual(test.states['y'], [[[6.0], [25.0], [30.0], [35.0], [float('inf')]]]) - self.assertEqual(test.states['z'], [[[20.0], [25.0], [30.0], [35.0], [float('inf')]]]) + self.assertEqual( + test.states["y"], [[[6.0], [25.0], [30.0], [35.0], [float("inf")]]] + ) + self.assertEqual( + test.states["z"], [[[20.0], [25.0], [30.0], [35.0], [float("inf")]]] + ) # Replace instead of rolling # self.assertEqual(result['out_y'], [2*(1+2+3+4+15), 2*(2+3+4+5+20), 2*(3+4+5+6+25), 2*(4+5+6+25+30), 2*(5+6+25+30+35)]) # self.assertEqual(result['out_z'], [3*(1+2+3+4+15), 3*(2+3+4+5+20), 3*(3+4+5+20+25), 3*(4+5+20+25+30), 3*(5+20+25+30+35)]) @@ -538,354 +732,547 @@ def test_predict_values_and_states_2states_more_window_connect(self): # self.assertEqual(test.model.states['z'].numpy().tolist(), [[[20.0], [25.0], [30.0], [35.0], [5.0]]]) ## Clear all states test.resetStates() - self.assertEqual(test.states['y'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) - self.assertEqual(test.states['z'], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + self.assertEqual(test.states["y"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) + self.assertEqual(test.states["z"], [[[0.0], [0.0], [0.0], [0.0], [0.0]]]) def test_predict_values_and_states_2states_more_window_connect_predict(self): NeuObj.clearNames() ## the state is saved inside the model so the memory is shared between different calls - x = Input('x') - y_state = Input('y') - z_state = Input('z') - x_p = Parameter('x_p', tw=0.5, dimensions=1, values=[[1.0],[1.0],[1.0],[1.0],[1.0]]) - y_p = Parameter('y_p', tw=0.5, dimensions=1, values=[[2.0],[2.0],[2.0],[2.0],[2.0]]) - z_p = Parameter('z_p', tw=0.5, dimensions=1, values=[[3.0],[3.0],[3.0],[3.0],[3.0]]) + x = Input("x") + y_state = Input("y") + z_state = Input("z") + x_p = Parameter( + "x_p", tw=0.5, dimensions=1, values=[[1.0], [1.0], [1.0], [1.0], [1.0]] + ) + y_p = Parameter( + "y_p", tw=0.5, dimensions=1, values=[[2.0], [2.0], [2.0], [2.0], [2.0]] + ) + z_p = Parameter( + "z_p", tw=0.5, dimensions=1, values=[[3.0], [3.0], [3.0], [3.0], [3.0]] + ) x_fir = Fir(W=x_p)(x.tw(0.5)) y_fir = Fir(W=y_p)(y_state.tw(0.5)) z_fir = Fir(W=z_p)(z_state.tw(0.5)) - out_x = Output('out_x', x_fir) - out_y = Output('out_y', y_fir) - out_z = Output('out_z', z_fir) - out = Output('out',x_fir+y_fir+z_fir) + out_x = Output("out_x", x_fir) + out_y = Output("out_y", y_fir) + out_z = Output("out_z", z_fir) + out = Output("out", x_fir + y_fir + z_fir) test = Modely(visualizer=None, seed=42) - test.addModel('out_all',[out, out_x, out_y, out_z]) + test.addModel("out_all", [out, out_x, out_y, out_z]) test.neuralizeModel(0.1) ## one sample prediction with state variables not initialized ## (they will have the last valid state) - result = test(inputs={'x':[1,2,3,4,5]}, connect={'y':'out_x','z':'out_x'}) - self.assertEqual(result['out'], [90.0]) - self.assertEqual(result['out_x'], [15.0]) - self.assertEqual(result['out_y'], [30.0]) - self.assertEqual(result['out_z'], [45.0]) + result = test( + inputs={"x": [1, 2, 3, 4, 5]}, connect={"y": "out_x", "z": "out_x"} + ) + self.assertEqual(result["out"], [90.0]) + self.assertEqual(result["out_x"], [15.0]) + self.assertEqual(result["out_y"], [30.0]) + self.assertEqual(result["out_z"], [45.0]) ## 1 sample prediction with state variables all initialized - result = test(inputs={'x':[1,2,3,4,5], 'y':[1,2,3,4,5], 'z':[1,2,3,4,5]}, connect={'y':'out_x','z':'out_x'}) - self.assertEqual(result['out_x'], [15.0]) - #(1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3 + result = test( + inputs={"x": [1, 2, 3, 4, 5], "y": [1, 2, 3, 4, 5], "z": [1, 2, 3, 4, 5]}, + connect={"y": "out_x", "z": "out_x"}, + ) + self.assertEqual(result["out_x"], [15.0]) + # (1+2+3+4+5)+(2+3+4+5+(1+2+3+4+5))*2+(2+3+4+5+(1+2+3+4+5))*3 # self.assertEqual(result['out'], [160.0]) # self.assertEqual(result['out_y'], [58.0]) # self.assertEqual(result['out_z'], [87.0]) # Replace instead of rolling - #(1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3 - self.assertEqual(result['out'], [140.0]) - self.assertEqual(result['out_y'], [50.0]) - self.assertEqual(result['out_z'], [75.0]) + # (1+2+3+4+5)+(1+2+3+4+(1+2+3+4+5))*2+(1+2+3+4+(1+2+3+4+5))*3 + self.assertEqual(result["out"], [140.0]) + self.assertEqual(result["out_y"], [50.0]) + self.assertEqual(result["out_z"], [75.0]) ## multi-sample prediction with states initialized as many times as they have values - result = test(inputs={'x':[1,2,3,4,5,6,7,8,9], 'y':[1,2,3,4,5,6,7], 'z':[1,2,3,4,5,6]}, connect={'y':'out_x','z':'out_x'}) - self.assertEqual(result['out_x'], [15.0, 20.0, 25.0, 30.0, 35.0]) + result = test( + inputs={ + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "y": [1, 2, 3, 4, 5, 6, 7], + "z": [1, 2, 3, 4, 5, 6], + }, + connect={"y": "out_x", "z": "out_x"}, + ) + self.assertEqual(result["out_x"], [15.0, 20.0, 25.0, 30.0, 35.0]) # self.assertEqual(result['out_y'], [2*(2+3+4+5+15), 2*(3+4+5+6+20), 2*(4+5+6+7+25), 2*(5+6+7+25+30), 2*(6+7+25+30+35)]) # self.assertEqual(result['out_z'], [3*(2+3+4+5+15), 3*(3+4+5+6+20), 3*(4+5+6+20+25), 3*(5+6+20+25+30), 3*(6+20+25+30+35)]) # Reaplce instead of rolling - self.assertEqual(result['out_y'], [2*(1+2+3+4+15), 2*(2+3+4+5+20), 2*(3+4+5+6+25), 2*(4+5+6+25+30), 2*(5+6+25+30+35)]) - self.assertEqual(result['out_z'], [3*(1+2+3+4+15), 3*(2+3+4+5+20), 3*(3+4+5+20+25), 3*(4+5+20+25+30), 3*(5+20+25+30+35)]) - self.assertEqual(result['out'], [sum(x) for x in zip(result['out_x'],result['out_y'],result['out_z'])]) + self.assertEqual( + result["out_y"], + [ + 2 * (1 + 2 + 3 + 4 + 15), + 2 * (2 + 3 + 4 + 5 + 20), + 2 * (3 + 4 + 5 + 6 + 25), + 2 * (4 + 5 + 6 + 25 + 30), + 2 * (5 + 6 + 25 + 30 + 35), + ], + ) + self.assertEqual( + result["out_z"], + [ + 3 * (1 + 2 + 3 + 4 + 15), + 3 * (2 + 3 + 4 + 5 + 20), + 3 * (3 + 4 + 5 + 20 + 25), + 3 * (4 + 5 + 20 + 25 + 30), + 3 * (5 + 20 + 25 + 30 + 35), + ], + ) + self.assertEqual( + result["out"], + [sum(x) for x in zip(result["out_x"], result["out_y"], result["out_z"])], + ) def test_predict_values_and_connect_variables_2models_more_window_connect(self): clearNames() ## Model1 - input1 = Input('in1') - a = Parameter('a', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - output1 = Output('out1', Fir(W=a)(input1.tw(0.05))) + input1 = Input("in1") + a = Parameter("a", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output1 = Output("out1", Fir(W=a)(input1.tw(0.05))) test = Modely(visualizer=None, seed=42) - test.addModel('model1', output1) - test.addMinimize('error1', input1.next(), output1) + test.addModel("model1", output1) + test.addMinimize("error1", input1.next(), output1) test.neuralizeModel(0.01) ## Model2 - input2 = Input('in2') - input3 = Input('in3') - b = Parameter('b', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - c = Parameter('c', dimensions=1, tw=0.03, values=[[1],[1],[1]]) - output2 = Output('out2', Fir(W=b)(input2.tw(0.05))+Fir(W=c)(input3.tw(0.03))) - - test.addModel('model2', output2) - test.addConnect(output1,input3) - test.addMinimize('error2', input2.next(), output2) + input2 = Input("in2") + input3 = Input("in3") + b = Parameter("b", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + c = Parameter("c", dimensions=1, tw=0.03, values=[[1], [1], [1]]) + output2 = Output("out2", Fir(W=b)(input2.tw(0.05)) + Fir(W=c)(input3.tw(0.03))) + + test.addModel("model2", output2) + test.addConnect(output1, input3) + test.addMinimize("error2", input2.next(), output2) test.neuralizeModel(0.01) ## Without connect - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=-1) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [21.0, 29.0, 37.0, 45.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in3": [[1], [2], [3], [4], [5], [6]], + }, + prediction_samples=-1, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [21.0, 29.0, 37.0, 45.0]) ## connect out1 to in3 for 4 samples test.resetStates() - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=3) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 105.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + }, + prediction_samples=3, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 105.0]) # self.assertEqual(test.states['in3'], [[[20.], [25.], [30.]]]) # Replace insead of rolling - self.assertEqual(test.states['in3'], [[[25.], [30.], [float('inf')]]]) + self.assertEqual(test.states["in3"], [[[25.0], [30.0], [float("inf")]]]) ## connect out1 to in3 for 3 samples test.resetStates() - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=2) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 60.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + }, + prediction_samples=2, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 60.0]) # self.assertEqual(test.states['in3'], [[[0.0], [0.], [30.]]]) # Replace insead of rolling - self.assertEqual(test.states['in3'], [[[0.], [30.], [float('inf')]]]) + self.assertEqual(test.states["in3"], [[[0.0], [30.0], [float("inf")]]]) ## connect out1 to in3 for 4 samples (initialize in3 with data) test.resetStates() - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=3) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - #(1+2+3+4+5)+(2+3+15) - #(2+3+4+5+6)+(3+15+20) - #self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 105.0]) - #self.assertEqual(test.states['in3'], [[[20.], [25.], [30.]]]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in3": [[1], [2], [3], [4], [5], [6]], + }, + prediction_samples=3, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + # (1+2+3+4+5)+(2+3+15) + # (2+3+4+5+6)+(3+15+20) + # self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 105.0]) + # self.assertEqual(test.states['in3'], [[[20.], [25.], [30.]]]) # Replace insead of rolling - #(1+2+3+4+5)+(1+2+15) - #(2+3+4+5+6)+(2+15+20) - self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 105.0]) - self.assertEqual(test.states['in3'], [[[25.], [30.], [float('inf')]]]) + # (1+2+3+4+5)+(1+2+15) + # (2+3+4+5+6)+(2+15+20) + self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 105.0]) + self.assertEqual(test.states["in3"], [[[25.0], [30.0], [float("inf")]]]) ## connect out1 to in3 for 3 samples (initialize in3 with data) test.resetStates() - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=2) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in3": [[1], [2], [3], [4], [5], [6]], + }, + prediction_samples=2, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) # (4+5+6+7+8)+(5+6+30) # self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 71.0]) # self.assertEqual(test.states['in3'], [[[5.], [6.], [30.]]]) # Replace insead of rolling # (4+5+6+7+8)+(4+5+30) - self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 69.0]) - self.assertEqual(test.states['in3'], [[[5.], [30.], [float('inf')]]]) + self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 69.0]) + self.assertEqual(test.states["in3"], [[[5.0], [30.0], [float("inf")]]]) ## Test remove connect - test.removeConnection('in3') + test.removeConnection("in3") test.neuralizeModel() - results = test(inputs={'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9]], - 'in2': [[1], [2], [3], [4], [5], [6], [7], [8], [9]], - 'in3': [[1], [2], [3], [4], [5], [6]]}, prediction_samples=-1) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [21.0, 29.0, 37.0, 45.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in3": [[1], [2], [3], [4], [5], [6]], + }, + prediction_samples=-1, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [21.0, 29.0, 37.0, 45.0]) ## Test connect via string - test.addConnect('out1', 'in3') + test.addConnect("out1", "in3") test.neuralizeModel() - results = test(inputs={'in1': [[1], [2], [3], [4], [5], [6], [7], [8], [9]], - 'in2': [[1], [2], [3], [4], [5], [6], [7], [8], [9]]}, prediction_samples=3) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 105.0]) - self.assertEqual(test.states['in3'], [[[25.], [30.], [float('inf')]]]) - - def test_predict_values_and_connect_variables_2models_more_window_connect_predict(self): + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + }, + prediction_samples=3, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 105.0]) + self.assertEqual(test.states["in3"], [[[25.0], [30.0], [float("inf")]]]) + + def test_predict_values_and_connect_variables_2models_more_window_connect_predict( + self, + ): NeuObj.clearNames() ## Model1 - input1 = Input('in1') - a = Parameter('a', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - output1 = Output('out1', Fir(W=a)(input1.tw(0.05))) + input1 = Input("in1") + a = Parameter("a", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output1 = Output("out1", Fir(W=a)(input1.tw(0.05))) test = Modely(visualizer=None, seed=42) - test.addModel('model1', output1) - test.addMinimize('error1', input1.next(), output1) + test.addModel("model1", output1) + test.addMinimize("error1", input1.next(), output1) test.neuralizeModel(0.01) ## Model2 - input2 = Input('in2') - input3 = Input('in3') - b = Parameter('b', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - c = Parameter('c', dimensions=1, tw=0.03, values=[[1],[1],[1]]) + input2 = Input("in2") + input3 = Input("in3") + b = Parameter("b", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + c = Parameter("c", dimensions=1, tw=0.03, values=[[1], [1], [1]]) input_connect = input3.tw(0.03) - output2 = Output('out2', Fir(W=b)(input2.tw(0.05))+Fir(W=c)(input_connect)) - output_connect = Output('out_connect', input_connect) + output2 = Output("out2", Fir(W=b)(input2.tw(0.05)) + Fir(W=c)(input_connect)) + output_connect = Output("out_connect", input_connect) - test.addModel('model2', [output2, output_connect]) - test.addMinimize('error2', input2.next(), output2) + test.addModel("model2", [output2, output_connect]) + test.addMinimize("error2", input2.next(), output2) test.neuralizeModel(0.01) ## Without connect - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [21.0, 29.0, 37.0, 45.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in3": [[1], [2], [3], [4], [5], [6]], + } + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [21.0, 29.0, 37.0, 45.0]) ## connect out1 to in3 for 4 samples - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=3, connect={'in3':'out1'}) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 105.0]) - self.assertEqual(results['out_connect'][-1], [20.0, 25.0, 30.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + }, + prediction_samples=3, + connect={"in3": "out1"}, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 105.0]) + self.assertEqual(results["out_connect"][-1], [20.0, 25.0, 30.0]) # Replace insead of rolling # self.assertEqual(test.model.connect_variables['in3'].detach().numpy().tolist(), [[[25.], [30.], [20.]]]) ## connect out1 to in3 for 3 samples - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]]}, prediction_samples=2, connect={'in3':'out1'}) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - self.assertEqual(results['out2'], [30.0, 55.0, 85.0, 60.0]) - self.assertEqual(results['out_connect'][-1], [0.0, 0., 30.]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + }, + prediction_samples=2, + connect={"in3": "out1"}, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + self.assertEqual(results["out2"], [30.0, 55.0, 85.0, 60.0]) + self.assertEqual(results["out_connect"][-1], [0.0, 0.0, 30.0]) # Replace insead of rolling # self.assertEqual(test.model.connect_variables['in3'].detach().numpy().tolist(), [[[0.], [30.], [0.]]]) ## connect out1 to in3 for 4 samples (initialize in3 with data) - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=3, connect={'in3':'out1'}) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) - #(1+2+3+4+5)+(2+3+15) - #(2+3+4+5+6)+(3+15+20) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in3": [[1], [2], [3], [4], [5], [6]], + }, + prediction_samples=3, + connect={"in3": "out1"}, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) + # (1+2+3+4+5)+(2+3+15) + # (2+3+4+5+6)+(3+15+20) # self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 105.0]) - self.assertEqual(results['out_connect'][-1], [20., 25., 30.]) + self.assertEqual(results["out_connect"][-1], [20.0, 25.0, 30.0]) # Replace insead of rolling - #(1+2+3+4+5)+(1+2+15) - #(2+3+4+5+6)+(2+15+20) - self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 105.0]) - #self.assertEqual(results['out_connect'][-1], [25.0, 30.0, 20.0]) + # (1+2+3+4+5)+(1+2+15) + # (2+3+4+5+6)+(2+15+20) + self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 105.0]) + # self.assertEqual(results['out_connect'][-1], [25.0, 30.0, 20.0]) ## connect out1 to in3 for 3 samples (initialize in3 with data) - results = test(inputs={'in1':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in2':[[1],[2],[3],[4],[5],[6],[7],[8],[9]], 'in3':[[1],[2],[3],[4],[5],[6]]}, prediction_samples=2, connect={'in3':'out1'}) - self.assertEqual(results['out1'], [15.0, 20.0, 25.0, 30.0]) + results = test( + inputs={ + "in1": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in2": [[1], [2], [3], [4], [5], [6], [7], [8], [9]], + "in3": [[1], [2], [3], [4], [5], [6]], + }, + prediction_samples=2, + connect={"in3": "out1"}, + ) + self.assertEqual(results["out1"], [15.0, 20.0, 25.0, 30.0]) # (4+5+6+7+8)+(5+6+30) # self.assertEqual(results['out2'], [35.0, 58.0, 85.0, 71.0]) # self.assertEqual(results['out_connect'][-1], [5., 6., 30.]) - self.assertEqual(results['out_connect'][-1], [4., 5., 30.]) + self.assertEqual(results["out_connect"][-1], [4.0, 5.0, 30.0]) # Replace insead of rolling # (4+5+6+7+8)+(4+5+30) - self.assertEqual(results['out2'], [33.0, 57.0, 85.0, 69.0]) + self.assertEqual(results["out2"], [33.0, 57.0, 85.0, 69.0]) # self.assertEqual(test.model.connect_variables['in3'].detach().numpy().tolist(), [[[5.], [30.], [4.]]]) - def test_predict_values_and_states_only_state_variables_more_window_closed_loop(self): + def test_predict_values_and_states_only_state_variables_more_window_closed_loop( + self, + ): NeuObj.clearNames() - x_state = Input('x_state') - p = Parameter('p', dimensions=1, tw=0.03, values=[[1.0], [1.0], [1.0]]) + x_state = Input("x_state") + p = Parameter("p", dimensions=1, tw=0.03, values=[[1.0], [1.0], [1.0]]) rel_x = Fir(W=p)(x_state.tw(0.03)) rel_x = ClosedLoop(rel_x, x_state) - out = Output('out', rel_x) + out = Output("out", rel_x) - test = Modely(visualizer = None, seed=42) - test.addModel('out',out) + test = Modely(visualizer=None, seed=42) + test.addModel("out", out) test.neuralizeModel(0.01) - result = test(inputs={'x_state':[1, 2, 3]}) - self.assertEqual(test.states['x_state'], [[[2.],[3.],[6.]]]) + result = test(inputs={"x_state": [1, 2, 3]}) + self.assertEqual(test.states["x_state"], [[[2.0], [3.0], [6.0]]]) result = test() - self.assertEqual(test.states['x_state'], [[[3.],[6.],[11.]]]) + self.assertEqual(test.states["x_state"], [[[3.0], [6.0], [11.0]]]) def test_predict_values_linear_and_fir_2models_same_window_connect(self): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=1) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=1) lin_out = Linear(W=W, b=b)(input1.sw(2)) - inout = Input('inout') - a = Parameter('a', sw = 2, values=[[4],[5]]) + inout = Input("inout") + a = Parameter("a", sw=2, values=[[4], [5]]) lin_out.connect(inout) - output1 = Output('out1', lin_out) - output2 = Output('out2', Fir(W=a)(inout.sw(2))) - output3 = Output('out3', Fir(W=a)(lin_out)) + output1 = Output("out1", lin_out) + output2 = Output("out2", Fir(W=a)(inout.sw(2))) + output3 = Output("out3", Fir(W=a)(lin_out)) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2,output3]) + test.addModel("model", [output1, output2, output3]) test.neuralizeModel() # [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------ - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[-10,-16]})) - self.assertEqual({'out1': [[-10.0,-16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0,2.0],[2.0,3.0]]})) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [-10, -16]}), + ) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]]}), + ) def test_predict_values_linear_and_fir_2models_same_window_connect_predict(self): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=[1]) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=[1]) lin_out = Linear(W=W, b=b)(input1.sw(2)) - output1 = Output('out1', lin_out) + output1 = Output("out1", lin_out) - inout = Input('inout') - a = Parameter('a', sw = 2, values=[[4],[5]]) - output2 = Output('out2', Fir(W=a)(inout.sw(2))) - output3 = Output('out3', Fir(W=a)(lin_out)) + inout = Input("inout") + a = Parameter("a", sw=2, values=[[4], [5]]) + output2 = Output("out2", Fir(W=a)(inout.sw(2))) + output3 = Output("out3", Fir(W=a)(lin_out)) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2,output3]) + test.addModel("model", [output1, output2, output3]) test.neuralizeModel() # [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------ - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[-10,-16]})) - self.assertEqual({'out1': [[-10.0,-16.0]], 'out2': [-120.0], 'out3':[-120.0]}, test({'in1': [[1.0,2.0],[2.0,3.0]]},connect={'inout': 'out1'})) - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[-30,-30]}, connect={'inout': 'out1'})) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [-10, -16]}), + ) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]]}, connect={"inout": "out1"}), + ) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [-120.0], "out3": [-120.0]}, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [-30, -30]}, + connect={"inout": "out1"}, + ), + ) def test_predict_values_linear_and_fir_2models_more_window_connect(self): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=1) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=1) lin_out = Linear(W=W, b=b)(input1.sw(2)) - inout = Input('inout') - a = Parameter('a', sw = 2, values=[[4], [5]]) - a_big = Parameter('ab', sw = 5, values=[[1], [2], [3], [4], [5]]) + inout = Input("inout") + a = Parameter("a", sw=2, values=[[4], [5]]) + a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]]) lin_out.connect(inout) - output1 = Output('out1', lin_out) - output2 = Output('out2', Fir(W=a)(inout.sw(2))) - output3 = Output('out3', Fir(W=a_big)(inout.sw(5))) - output4 = Output('out4', Fir(W=a)(lin_out)) + output1 = Output("out1", lin_out) + output2 = Output("out2", Fir(W=a)(inout.sw(2))) + output3 = Output("out3", Fir(W=a_big)(inout.sw(5))) + output4 = Output("out4", Fir(W=a)(lin_out)) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2,output3,output4]) + test.addModel("model", [output1, output2, output3, output4]) test.neuralizeModel() # [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------ - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]]})) + self.assertEqual( + { + "out1": [[-10.0, -16.0]], + "out2": [-120.0], + "out3": [-120.0], + "out4": [-120.0], + }, + test({"in1": [[1.0, 2.0], [2.0, 3.0]]}), + ) test.resetStates() # out2 # = [[-10,-16]] -> 1) [-10,-16]*[4,5] -> [-16*5+-10*4=-120] # out3 # = [[-10,-16]] -> 1) [0,0,-10,-10,-16]*[1,2,3,4,5] -> [-10*3+-16*5+-10*4=-150] # self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-150.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[0,0,0,-10,-16]})) # Replace instead of rolling - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'inout': [0, 0, 0, -10, -16]})) + self.assertEqual( + { + "out1": [[-10.0, -16.0]], + "out2": [-120.0], + "out3": [-120.0], + "out4": [-120.0], + }, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [0, 0, 0, -10, -16]}), + ) test.resetStates() # out2 # = [[-10,-16],[-16,-10]] -> 1) [-10,-16]*[4,5] -> [-16*5+-10*4=-120] 2) [-16,-10]*[4,5] -> [-16*4+-10*5=-114] -> [-120,-114] # out3 # = [[-10,-16],[-16,-10]] -> 1) [0,0,0,-10,-16]*[1,2,3,4,5] -> [-16*5+-10*4=-120] 2) [0,0,-10,-16,-10]*[1,2,3,4,5] -> [-10*3+-16*4+-10*5 = -144] -> [-120,-144] - self.assertEqual({'out1': [[-10.0, -16.0],[-16.0, -10.0]], 'out2': [-120.0,-114.0], 'out3': [-120.0,-144], 'out4': [-120.0,-114.0]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0], [1.0,2.0]]})) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, -10.0]], + "out2": [-120.0, -114.0], + "out3": [-120.0, -144], + "out4": [-120.0, -114.0], + }, + test({"in1": [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]}), + ) with self.assertRaises(ValueError): test.removeConnection(input1) test.removeConnection(inout) test.neuralizeModel() - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [0.0], 'out3': [0.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]]})) - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'inout': [0, 0, 0, -10, -16]})) - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -10.0]], 'out2': [0.0, 0.0], 'out3': [0.0, 0.0], 'out4': [-120.0, -114.0]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]})) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [0.0], "out3": [0.0], "out4": [-120.0]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]]}), + ) + self.assertEqual( + { + "out1": [[-10.0, -16.0]], + "out2": [-120.0], + "out3": [-120.0], + "out4": [-120.0], + }, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [0, 0, 0, -10, -16]}), + ) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, -10.0]], + "out2": [0.0, 0.0], + "out3": [0.0, 0.0], + "out4": [-120.0, -114.0], + }, + test({"in1": [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]}), + ) def test_predict_values_linear_and_fir_2models_more_window_connect_predict(self): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=[1]) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=[1]) lin_out = Linear(W=W, b=b)(input1.sw(2)) - output1 = Output('out1', lin_out) + output1 = Output("out1", lin_out) - inout = Input('inout') - a = Parameter('a', sw = 2, values=[[4], [5]]) - output2 = Output('out2', Fir(W=a)(inout.sw(2))) - a_big = Parameter('ab', sw = 5, values=[[1], [2], [3], [4], [5]]) - output3 = Output('out3', Fir(W=a_big)(inout.sw(5))) - output4 = Output('out4', Fir(W=a)(lin_out)) + inout = Input("inout") + a = Parameter("a", sw=2, values=[[4], [5]]) + output2 = Output("out2", Fir(W=a)(inout.sw(2))) + a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]]) + output3 = Output("out3", Fir(W=a_big)(inout.sw(5))) + output4 = Output("out4", Fir(W=a)(lin_out)) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2,output3,output4]) + test.addModel("model", [output1, output2, output3, output4]) test.neuralizeModel() # [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] = [-10,-16] -> [-10,-16]*[4,5] -> [-16*5+-10*4=-120] <------ - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]}, test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[0,0,0,-10,-16]})) - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-120.0], 'out4': [-120.0]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]]}, connect={'inout': 'out1'})) - #self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-150.0], 'out4': [-120.0]}, + self.assertEqual( + { + "out1": [[-10.0, -16.0]], + "out2": [-120.0], + "out3": [-120.0], + "out4": [-120.0], + }, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "inout": [0, 0, 0, -10, -16]}), + ) + self.assertEqual( + { + "out1": [[-10.0, -16.0]], + "out2": [-120.0], + "out3": [-120.0], + "out4": [-120.0], + }, + test({"in1": [[1.0, 2.0], [2.0, 3.0]]}, connect={"inout": "out1"}), + ) + # self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [-120.0], 'out3': [-150.0], 'out4': [-120.0]}, # test({'in1': [[1.0, 2.0], [2.0, 3.0]],'inout':[0,0,0,-10,-16]}, connect={'inout': 'out1'})) with self.assertRaises(StopIteration): self.assertEqual({}, test()) @@ -894,255 +1281,452 @@ def test_predict_values_linear_and_fir_2models_more_window_connect_predict(self) with self.assertRaises(StopIteration): self.assertEqual({}, test(prediction_samples=4)) - self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [9.0], 'out3': [9.0], 'out4': [9.0]}, - test(connect={'inout': 'out1'})) - self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [9.0], 'out3': [9.0], 'out4': [9.0]}, - test(connect={'inout': 'out1'}, prediction_samples=0)) - self.assertEqual({'out1': [[1.0, 1.0],[1.0, 1.0]], 'out2': [9.0,9.0], 'out3': [9.0,12.0], 'out4': [9.0,9.0]}, - test(connect={'inout': 'out1'}, prediction_samples=1, num_of_samples=2)) + self.assertEqual( + {"out1": [[1.0, 1.0]], "out2": [9.0], "out3": [9.0], "out4": [9.0]}, + test(connect={"inout": "out1"}), + ) + self.assertEqual( + {"out1": [[1.0, 1.0]], "out2": [9.0], "out3": [9.0], "out4": [9.0]}, + test(connect={"inout": "out1"}, prediction_samples=0), + ) + self.assertEqual( + { + "out1": [[1.0, 1.0], [1.0, 1.0]], + "out2": [9.0, 9.0], + "out3": [9.0, 12.0], + "out4": [9.0, 9.0], + }, + test(connect={"inout": "out1"}, prediction_samples=1, num_of_samples=2), + ) # [[1,2],[2,3]]*[-1,-5] = [[1*-1+2*-5=-11],[2*-1+3*-5=-17]]+[1] # [[2,3],[1,2]]*[-1,-5] = [[2*-1+3*-5=-17],[1*-1+2*-5=-11]]+[1] # out2 # = [[-10,-16],[-16,-10]] -> 1) [-10,-16]*[4,5] -> [-16*5+-10*4=-120] 2) [-16,-10]*[4,5] -> [-16*4+-10*5=-114] -> [-120,-114] # out3 # = [[-10,-16],[-16,-10]] -> 1) [0,0,0,-10,-16]*[1,2,3,4,5] -> [-16*5+-10*4=-120] 2) [0,0,-10,-16,-10]*[1,2,3,4,5] -> [-10*3+-16*4+-10*5 = -144] -> [-120,-144] - self.assertEqual({'out1': [[-10.0, -16.0],[-16.0, -10.0]], 'out2': [-120.0,-114.0], 'out3': [-120.0,-144], 'out4': [-120.0,-114.0]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0], [1.0,2.0]]}, - connect={'inout': 'out1'})) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, -10.0]], + "out2": [-120.0, -114.0], + "out3": [-120.0, -144], + "out4": [-120.0, -114.0], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]}, connect={"inout": "out1"} + ), + ) def test_predict_values_linear_and_fir_2models_more_window_closed_loop(self): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=1) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=1) relation1 = Linear(W=W, b=b)(input1.sw(2)) # input2 = Input('inout') #TODO loop forever # test.addConnect(output1, input1) # With this - input2 = Input('in2') - a = Parameter('a', sw=5, values=[[1,3],[2,4],[3,5],[4,6],[5,7]]) - relation2 = Fir(output_dimension=2,W=a)(input2.sw(5)) + input2 = Input("in2") + a = Parameter("a", sw=5, values=[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]) + relation2 = Fir(output_dimension=2, W=a)(input2.sw(5)) relation1.closedLoop(input2) relation2.closedLoop(input1) - output1 = Output('out1', relation1) - output2 = Output('out2', relation2) + output1 = Output("out1", relation1) + output2 = Output("out2", relation2) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) + test.addModel("model", [output1, output2]) test.neuralizeModel() - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]})) - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]},prediction_samples=0)) - - self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=1, num_of_samples=2)) - self.assertEqual({'out1': [[465.0,1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test()) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}), + ) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=0, + ), + ) + + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, 465.0]], + "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]]], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=1, + num_of_samples=2, + ), + ) + self.assertEqual( + {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, test() + ) test.resetStates() - self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0],[465.0,1291.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]],[[2230.0, 3102.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=2, num_of_samples=3)) - - def test_predict_values_linear_and_fir_2models_more_window_closed_loop_on_models(self): + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]], + "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=2, + num_of_samples=3, + ), + ) + + def test_predict_values_linear_and_fir_2models_more_window_closed_loop_on_models( + self, + ): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=1) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=1) relation1 = Linear(W=W, b=b)(input1.sw(2)) # input2 = Input('inout') #TODO loop forever # test.addConnect(output1, input1) # With this - input2 = Input('in2') - a = Parameter('a', sw=5, values=[[1,3],[2,4],[3,5],[4,6],[5,7]]) - relation2 = Fir(output_dimension=2,W=a)(input2.sw(5)) + input2 = Input("in2") + a = Parameter("a", sw=5, values=[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]) + relation2 = Fir(output_dimension=2, W=a)(input2.sw(5)) - output1 = Output('out1', relation1) - output2 = Output('out2', relation2) + output1 = Output("out1", relation1) + output2 = Output("out2", relation2) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) + test.addModel("model", [output1, output2]) test.addClosedLoop(output1, input2) test.addClosedLoop(output2, input1) test.neuralizeModel() - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]})) - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]},prediction_samples=0)) - - self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=1, num_of_samples=2)) - self.assertEqual({'out1': [[465.0,1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test()) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}), + ) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=0, + ), + ) + + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, 465.0]], + "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]]], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=1, + num_of_samples=2, + ), + ) + self.assertEqual( + {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, test() + ) test.resetStates() - self.assertEqual({'out1': [[-10.0, -16.0],[-16.0,465.0],[465.0,1291.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]],[[2230.0, 3102.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=2, num_of_samples=3)) - - test.removeConnection('in1') - test.removeConnection('in2') + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]], + "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=2, + num_of_samples=3, + ), + ) + + test.removeConnection("in1") + test.removeConnection("in2") test.neuralizeModel() - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]})) - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]})) - - test.addClosedLoop('out1', 'in2') - test.addClosedLoop('out2', 'in1') + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}), + ) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test({"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}), + ) + + test.addClosedLoop("out1", "in2") + test.addClosedLoop("out2", "in1") test.neuralizeModel() - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]], - 'out2': [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=2, - num_of_samples=3)) - - def test_predict_values_linear_and_fir_2models_more_window_closed_loop_predict(self): + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0]], + "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]], [[2230.0, 3102.0]]], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=2, + num_of_samples=3, + ), + ) + + def test_predict_values_linear_and_fir_2models_more_window_closed_loop_predict( + self, + ): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=[1]) - output1 = Output('out1', Linear(W=W, b=b)(input1.sw(2))) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=[1]) + output1 = Output("out1", Linear(W=W, b=b)(input1.sw(2))) # input2 = Input('inout') #TODO loop forever # test.addConnect(output1, input1) # With this - input2 = Input('in2') - a = Parameter('a', sw=5, values=[[1,3],[2,4],[3,5],[4,6],[5,7]]) - output2 = Output('out2', Fir(output_dimension=2,W=a)(input2.sw(5))) + input2 = Input("in2") + a = Parameter("a", sw=5, values=[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]]) + output2 = Output("out2", Fir(output_dimension=2, W=a)(input2.sw(5))) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) + test.addModel("model", [output1, output2]) test.neuralizeModel() # 1*-1+2*-5+1 = -10 2*-1+3*-5+1 = -16 -10*1+-16*2+-5*3+2*4+3*5 = -34 -16*2+-10*3+-5*4+2*5+3*6 = -86 - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, closed_loop={'in1':'out2', 'in2':'out1'})) - self.assertEqual({'out1': [[-10.0, -16.0]], 'out2': [[[-34.0, -86.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=0, closed_loop={'in1':'out2', 'in2':'out1'})) - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0,465.0]], 'out2': [[[-34.0, -86.0]],[[-140.0,-230.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, num_of_samples=2, prediction_samples=2, closed_loop={'in1':'out2', 'in2':'out1'})) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + closed_loop={"in1": "out2", "in2": "out1"}, + ), + ) + self.assertEqual( + {"out1": [[-10.0, -16.0]], "out2": [[[-34.0, -86.0]]]}, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=0, + closed_loop={"in1": "out2", "in2": "out1"}, + ), + ) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, 465.0]], + "out2": [[[-34.0, -86.0]], [[-140.0, -230.0]]], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + num_of_samples=2, + prediction_samples=2, + closed_loop={"in1": "out2", "in2": "out1"}, + ), + ) with self.assertRaises(StopIteration): - self.assertEqual({'out1': [[465.0, 1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test()) + self.assertEqual( + {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, test() + ) with self.assertRaises(StopIteration): - self.assertEqual({'out1': [[465.0, 1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test(prediction_samples=0)) + self.assertEqual( + {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, + test(prediction_samples=0), + ) with self.assertRaises(StopIteration): - self.assertEqual({'out1': [[465.0, 1291.0]], 'out2': [[[2230.0, 3102.0]]]}, test(prediction_samples=3)) - self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [[[0.0, 0.0]]]}, - test(closed_loop={'in1': 'out2', 'in2': 'out1'})) - self.assertEqual({'out1': [[1.0, 1.0]], 'out2': [[[0.0, 0.0]]]}, - test(closed_loop={'in1': 'out2', 'in2': 'out1'},prediction_samples=0)) - self.assertEqual({'out1': [[1.0, 1.0],[1.0,1.0]], 'out2': [[[0.0, 0.0]],[[9.0,13.0]]]}, - test(closed_loop={'in1': 'out2', 'in2': 'out1'},prediction_samples=1, num_of_samples=2)) - self.assertEqual({'out1': [[1.0, 1.0],[1.0,1.0],[1.0,-73.0]], 'out2': [[[0.0, 0.0]],[[9.0,13.0]],[[12.0,18.0]]]}, - test(closed_loop={'in1': 'out2', 'in2': 'out1'},prediction_samples=2, num_of_samples=3)) - - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0,1.0], [1.0,1.0], [1.0,1.0], [1.0,1.0]], - 'out2': [[[-34.0, -86.0]],[[-8.0,-40.0]],[[8.0,8.0]],[[8.0,18.0]],[[3.0,9.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, num_of_samples=5)) - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0,1.0], [1.0,1.0], [1.0,1.0], [1.0,1.0]], - 'out2': [[[-34.0, -86.0]],[[-8.0,-40.0]],[[8.0,8.0]],[[8.0,18.0]],[[3.0,9.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, prediction_samples=-1, num_of_samples=5)) - - #-34*-1+ -86*-5+1 = 465.0 - #-140*-1+ -230*-5+1 = 465.0 - #8*-1 + 18*-5+1 = -97.0 - #[[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]] * [465.0, 1291.0] - #-16*1+-5*2+2*3+-10.0*4-16.0*5 = 140 - #-5*1+2*2-10*3+-16*4+465*5 = 2230 , -5*3+2*4-10*5+-16*6+465*7 = 3102 - #2*1+3*2 = 8, 2*3+3*4 = 18 - #3*1+1*4+1*5 = 12.0, 3*3+1*6+1*7 = 22.0 - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, 465.0], [465.0, 1291.0], [1.0, 1.0], [1.0, -97.0]], - 'out2': [[[-34.0, -86.0]], [[-140.0, -230.0]],[[2230.0, 3102.0]], [[8.0, 18.0]], [[12.0, 22.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0]], 'in2': [-10, -16, -5, 2, 3]}, num_of_samples=5, - prediction_samples=2, closed_loop={'in1': 'out2', 'in2': 'out1'})) + self.assertEqual( + {"out1": [[465.0, 1291.0]], "out2": [[[2230.0, 3102.0]]]}, + test(prediction_samples=3), + ) + self.assertEqual( + {"out1": [[1.0, 1.0]], "out2": [[[0.0, 0.0]]]}, + test(closed_loop={"in1": "out2", "in2": "out1"}), + ) + self.assertEqual( + {"out1": [[1.0, 1.0]], "out2": [[[0.0, 0.0]]]}, + test(closed_loop={"in1": "out2", "in2": "out1"}, prediction_samples=0), + ) + self.assertEqual( + {"out1": [[1.0, 1.0], [1.0, 1.0]], "out2": [[[0.0, 0.0]], [[9.0, 13.0]]]}, + test( + closed_loop={"in1": "out2", "in2": "out1"}, + prediction_samples=1, + num_of_samples=2, + ), + ) + self.assertEqual( + { + "out1": [[1.0, 1.0], [1.0, 1.0], [1.0, -73.0]], + "out2": [[[0.0, 0.0]], [[9.0, 13.0]], [[12.0, 18.0]]], + }, + test( + closed_loop={"in1": "out2", "in2": "out1"}, + prediction_samples=2, + num_of_samples=3, + ), + ) + + self.assertEqual( + { + "out1": [ + [-10.0, -16.0], + [-16.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + ], + "out2": [ + [[-34.0, -86.0]], + [[-8.0, -40.0]], + [[8.0, 8.0]], + [[8.0, 18.0]], + [[3.0, 9.0]], + ], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + num_of_samples=5, + ), + ) + self.assertEqual( + { + "out1": [ + [-10.0, -16.0], + [-16.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + [1.0, 1.0], + ], + "out2": [ + [[-34.0, -86.0]], + [[-8.0, -40.0]], + [[8.0, 8.0]], + [[8.0, 18.0]], + [[3.0, 9.0]], + ], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + prediction_samples=-1, + num_of_samples=5, + ), + ) + + # -34*-1+ -86*-5+1 = 465.0 + # -140*-1+ -230*-5+1 = 465.0 + # 8*-1 + 18*-5+1 = -97.0 + # [[1, 3], [2, 4], [3, 5], [4, 6], [5, 7]] * [465.0, 1291.0] + # -16*1+-5*2+2*3+-10.0*4-16.0*5 = 140 + # -5*1+2*2-10*3+-16*4+465*5 = 2230 , -5*3+2*4-10*5+-16*6+465*7 = 3102 + # 2*1+3*2 = 8, 2*3+3*4 = 18 + # 3*1+1*4+1*5 = 12.0, 3*3+1*6+1*7 = 22.0 + self.assertEqual( + { + "out1": [ + [-10.0, -16.0], + [-16.0, 465.0], + [465.0, 1291.0], + [1.0, 1.0], + [1.0, -97.0], + ], + "out2": [ + [[-34.0, -86.0]], + [[-140.0, -230.0]], + [[2230.0, 3102.0]], + [[8.0, 18.0]], + [[12.0, 22.0]], + ], + }, + test( + {"in1": [[1.0, 2.0], [2.0, 3.0]], "in2": [-10, -16, -5, 2, 3]}, + num_of_samples=5, + prediction_samples=2, + closed_loop={"in1": "out2", "in2": "out1"}, + ), + ) def test_predict_parameters(self): NeuObj.clearNames() - input1 = Input('in1') - cl1 = Input('cl1') - co1 = Input('co1') - W = Parameter('W', values=[[1], [2], [3]]) + input1 = Input("in1") + cl1 = Input("cl1") + co1 = Input("co1") + W = Parameter("W", values=[[1], [2], [3]]) parfun = ParamFun(myfunsum, parameters_and_constants=[W]) matmulfun = ParamFun(matmul) parfun_out = parfun(input1.sw(3)) - output = Output('out', parfun_out) - matmul_outcl = matmulfun(parfun_out, cl1.sw(3))+1.0 + output = Output("out", parfun_out) + matmul_outcl = matmulfun(parfun_out, cl1.sw(3)) + 1.0 matmul_outcl.connect(co1) matmul_outcl.closedLoop(cl1) - outputCl = Output('outCl', matmul_outcl) - outputCo = Output('outCo', matmulfun(parfun_out, co1.sw(3))) + outputCl = Output("outCl", matmul_outcl) + outputCo = Output("outCo", matmulfun(parfun_out, co1.sw(3))) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output,outputCl,outputCo]) + test.addModel("model", [output, outputCl, outputCo]) test.neuralizeModel() # Test only one input - result = test({'in1':[1,2,3]}) - self.assertEqual((1,3), np.array(result['out']).shape) - self.assertEqual((1,), np.array(result['outCl']).shape) - self.assertEqual((1,), np.array(result['outCo']).shape) - self.assertEqual([[2.0,4.0,6.0]], result['out']) - self.assertEqual([1.0], result['outCl']) - self.assertEqual([6.0],result['outCo']) - self.assertEqual(test.states['cl1'], [[[0.], [0.], [1.]]]) + result = test({"in1": [1, 2, 3]}) + self.assertEqual((1, 3), np.array(result["out"]).shape) + self.assertEqual((1,), np.array(result["outCl"]).shape) + self.assertEqual((1,), np.array(result["outCo"]).shape) + self.assertEqual([[2.0, 4.0, 6.0]], result["out"]) + self.assertEqual([1.0], result["outCl"]) + self.assertEqual([6.0], result["outCo"]) + self.assertEqual(test.states["cl1"], [[[0.0], [0.0], [1.0]]]) # self.assertEqual(test.states['co1'], [[[0.], [0.], [1.]]]) - self.assertEqual(test.states['co1'], [[[0.], [1.], [float('inf')]]]) + self.assertEqual(test.states["co1"], [[[0.0], [1.0], [float("inf")]]]) # Test two input test.resetStates() - result = test({'in1':[1,2,3,4]}) - self.assertEqual((2,3), np.array(result['out']).shape) - self.assertEqual((2,), np.array(result['outCl']).shape) - self.assertEqual((2,), np.array(result['outCo']).shape) - self.assertEqual([[2.0,4.0,6.0],[3.0,5.0,7.0]], result['out']) - self.assertEqual([1.0,1*7+1], result['outCl']) - self.assertEqual([1 * 6.0, 1. * 5 + 7. * 8.], result['outCo']) + result = test({"in1": [1, 2, 3, 4]}) + self.assertEqual((2, 3), np.array(result["out"]).shape) + self.assertEqual((2,), np.array(result["outCl"]).shape) + self.assertEqual((2,), np.array(result["outCo"]).shape) + self.assertEqual([[2.0, 4.0, 6.0], [3.0, 5.0, 7.0]], result["out"]) + self.assertEqual([1.0, 1 * 7 + 1], result["outCl"]) + self.assertEqual([1 * 6.0, 1.0 * 5 + 7.0 * 8.0], result["outCo"]) # self.assertEqual(test.states['co1'], [[[0.], [1.], [8.]]]) - self.assertEqual(test.states['co1'], [[[1.], [8.], [float('inf')]]]) - self.assertEqual(test.states['cl1'], [[[0.], [1.], [8.]]]) + self.assertEqual(test.states["co1"], [[[1.0], [8.0], [float("inf")]]]) + self.assertEqual(test.states["cl1"], [[[0.0], [1.0], [8.0]]]) # Test two input test.resetStates() - result = test({'in1':[1,2,3,4], 'cl1':[2,2,2,2,2,2]}) - self.assertEqual((2,3), np.array(result['out']).shape) - self.assertEqual((2,), np.array(result['outCl']).shape) - self.assertEqual((2,), np.array(result['outCo']).shape) - self.assertEqual([[2.0,4.0,6.0],[3.0,5.0,7.0]], result['out']) + result = test({"in1": [1, 2, 3, 4], "cl1": [2, 2, 2, 2, 2, 2]}) + self.assertEqual((2, 3), np.array(result["out"]).shape) + self.assertEqual((2,), np.array(result["outCl"]).shape) + self.assertEqual((2,), np.array(result["outCo"]).shape) + self.assertEqual([[2.0, 4.0, 6.0], [3.0, 5.0, 7.0]], result["out"]) # 2*2+4*2+6*2+1, 2*3+5*2+7*2+1 - self.assertEqual([25.0, 31.], result['outCl']) - self.assertEqual([150.0, 342.0], result['outCo']) - self.assertEqual(test.states['cl1'], [[[2.], [2.], [31.]]]) - #self.assertEqual(test.states['co1'], [[[0.], [25.], [31.]]]) - self.assertEqual(test.states['co1'], [[[25.], [31.], [float('inf')]]]) + self.assertEqual([25.0, 31.0], result["outCl"]) + self.assertEqual([150.0, 342.0], result["outCo"]) + self.assertEqual(test.states["cl1"], [[[2.0], [2.0], [31.0]]]) + # self.assertEqual(test.states['co1'], [[[0.], [25.], [31.]]]) + self.assertEqual(test.states["co1"], [[[25.0], [31.0], [float("inf")]]]) # Test two input test.resetStates() - result = test({'in1':[1,2,3,4], 'co1':[2,2,2,2,2,2]}) - self.assertEqual((2,3), np.array(result['out']).shape) - self.assertEqual((2,), np.array(result['outCl']).shape) - self.assertEqual((2,), np.array(result['outCo']).shape) - self.assertEqual([[2.0,4.0,6.0],[3.0,5.0,7.0]], result['out']) + result = test({"in1": [1, 2, 3, 4], "co1": [2, 2, 2, 2, 2, 2]}) + self.assertEqual((2, 3), np.array(result["out"]).shape) + self.assertEqual((2,), np.array(result["outCl"]).shape) + self.assertEqual((2,), np.array(result["outCo"]).shape) + self.assertEqual([[2.0, 4.0, 6.0], [3.0, 5.0, 7.0]], result["out"]) # 2*0+4*0+6*0+1 - self.assertEqual([1.0, 7*1+1.], result['outCl']) + self.assertEqual([1.0, 7 * 1 + 1.0], result["outCl"]) # 2*2+4*2+6*1, 2*3+2*5+7*8 - self.assertEqual([18.0, 72.0], result['outCo']) - self.assertEqual(test.states['cl1'], [[[0.], [1.], [8.]]]) - #self.assertEqual(test.states['co1'], [[[2.], [2.], [8.]]]) - self.assertEqual(test.states['co1'], [[[2.], [8.], [float('inf')]]]) + self.assertEqual([18.0, 72.0], result["outCo"]) + self.assertEqual(test.states["cl1"], [[[0.0], [1.0], [8.0]]]) + # self.assertEqual(test.states['co1'], [[[2.], [2.], [8.]]]) + self.assertEqual(test.states["co1"], [[[2.0], [8.0], [float("inf")]]]) test.resetStates() - result = test({'co1':[2,2,2,2,2,2]}) - self.assertEqual((4,3), np.array(result['out']).shape) - self.assertEqual((4,), np.array(result['outCl']).shape) - self.assertEqual((4,), np.array(result['outCo']).shape) - self.assertEqual([[1.0,2.0,3.0],[1.0,2.0,3.0],[1.0,2.0,3.0],[1.0,2.0,3.0]], result['out']) - self.assertEqual(test.states['cl1'], [[[4.], [15.], [55.]]]) + result = test({"co1": [2, 2, 2, 2, 2, 2]}) + self.assertEqual((4, 3), np.array(result["out"]).shape) + self.assertEqual((4,), np.array(result["outCl"]).shape) + self.assertEqual((4,), np.array(result["outCo"]).shape) + self.assertEqual( + [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]], + result["out"], + ) + self.assertEqual(test.states["cl1"], [[[4.0], [15.0], [55.0]]]) # self.assertEqual(test.states['co1'], [[[2.], [2.], [55.]]]) - self.assertEqual(test.states['co1'], [[[2.], [55.], [float('inf')]]]) + self.assertEqual(test.states["co1"], [[[2.0], [55.0], [float("inf")]]]) test.resetStates() - result = test({'co1':[2,2,2,2,2,2]}, prediction_samples = 2) - self.assertEqual((4,3), np.array(result['out']).shape) - self.assertEqual((4,), np.array(result['outCl']).shape) - self.assertEqual((4,), np.array(result['outCo']).shape) - + result = test({"co1": [2, 2, 2, 2, 2, 2]}, prediction_samples=2) + self.assertEqual((4, 3), np.array(result["out"]).shape) + self.assertEqual((4,), np.array(result["outCl"]).shape) + self.assertEqual((4,), np.array(result["outCo"]).shape) # Test output recurrent @@ -1209,167 +1793,336 @@ def test_predict_parameters(self): def test_parameters_predict_closed_loop_perdict(self): NeuObj.clearNames() - input1 = Input('in1') - W = Parameter('W', sw=3, values=[[1], [2], [3]]) - out = Output('out',Fir(W=W)(input1.sw(3))) + input1 = Input("in1") + W = Parameter("W", sw=3, values=[[1], [2], [3]]) + out = Output("out", Fir(W=W)(input1.sw(3))) test = Modely(visualizer=None, seed=42) - test.addModel('model', [out]) + test.addModel("model", [out]) test.neuralizeModel() - result = test({'in1':[1,2,3]}) - self.assertEqual((1,), np.array(result['out']).shape) - self.assertEqual([14.0], result['out']) - - result = test({'in1': [1, 2, 3]},closed_loop={'in1':'out'}) - self.assertEqual((1,), np.array(result['out']).shape) - self.assertEqual([14.0], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]}, closed_loop = {'in1':'out'}, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0, 3*4+2*3+2*1, 5*3+4*2+3*1, 26*3+5*2+4*1, 92*3+26*2+1*5], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=-1) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=0) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=1) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=2) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=-1, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=0, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=1, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1,26*3+5*2+4*1,0*3+0*2+5*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=2, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,0*3+5*2+4*1,14*3+0*2+5*1], result['out']) - - result = test({'in1': [1, 2, 3, 4, 5]},closed_loop={'in1':'out'}, prediction_samples=3, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,181*3+50*2+14*1,0*3+0*2+5*1], result['out']) + result = test({"in1": [1, 2, 3]}) + self.assertEqual((1,), np.array(result["out"]).shape) + self.assertEqual([14.0], result["out"]) + + result = test({"in1": [1, 2, 3]}, closed_loop={"in1": "out"}) + self.assertEqual((1,), np.array(result["out"]).shape) + self.assertEqual([14.0], result["out"]) + + result = test({"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, num_of_samples=5 + ) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 4 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 26 * 3 + 5 * 2 + 4 * 1, + 92 * 3 + 26 * 2 + 1 * 5, + ], + result["out"], + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=-1 + ) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=0 + ) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=1 + ) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 14 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=2 + ) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"] + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, closed_loop={"in1": "out"}, prediction_samples=3 + ) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"] + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, + closed_loop={"in1": "out"}, + prediction_samples=-1, + num_of_samples=5, + ) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 4 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 0 * 3 + 5 * 2 + 4 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, + closed_loop={"in1": "out"}, + prediction_samples=0, + num_of_samples=5, + ) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 4 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 0 * 3 + 5 * 2 + 4 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, + closed_loop={"in1": "out"}, + prediction_samples=1, + num_of_samples=5, + ) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 14 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 26 * 3 + 5 * 2 + 4 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, + closed_loop={"in1": "out"}, + prediction_samples=2, + num_of_samples=5, + ) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 14 + 2 * 3 + 2 * 1, + 50 * 3 + 14 * 2 + 3 * 1, + 0 * 3 + 5 * 2 + 4 * 1, + 14 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) + + result = test( + {"in1": [1, 2, 3, 4, 5]}, + closed_loop={"in1": "out"}, + prediction_samples=3, + num_of_samples=5, + ) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 14 + 2 * 3 + 2 * 1, + 50 * 3 + 14 * 2 + 3 * 1, + 181 * 3 + 50 * 2 + 14 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) def test_parameters_predict_closed_loop(self): NeuObj.clearNames() - input1 = Input('in1') - W = Parameter('W', sw=3, values=[[1], [2], [3]]) + input1 = Input("in1") + W = Parameter("W", sw=3, values=[[1], [2], [3]]) relation = Fir(W=W)(input1.sw(3)) relation.closedLoop(input1) - out = Output('out',relation) + out = Output("out", relation) test = Modely(visualizer=None, seed=42) - test.addModel('model', [out]) + test.addModel("model", [out]) test.neuralizeModel() - result = test({'in1':[1,2,3]}) - self.assertEqual((1,), np.array(result['out']).shape) - self.assertEqual([14.0], result['out']) + result = test({"in1": [1, 2, 3]}) + self.assertEqual((1,), np.array(result["out"]).shape) + self.assertEqual([14.0], result["out"]) test.resetStates() - result = test({'in1': [1, 2, 3]}) - self.assertEqual((1,), np.array(result['out']).shape) - self.assertEqual([14.0], result['out']) + result = test({"in1": [1, 2, 3]}) + self.assertEqual((1,), np.array(result["out"]).shape) + self.assertEqual([14.0], result["out"]) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0, 3*4+2*3+2*1, 5*3+4*2+3*1, 26*3+5*2+4*1, 92*3+26*2+1*5], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, num_of_samples=5) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 4 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 26 * 3 + 5 * 2 + 4 * 1, + 92 * 3 + 26 * 2 + 1 * 5, + ], + result["out"], + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=-1) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=-1) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=0) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=0) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 4 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=1) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=1) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 14 + 2 * 3 + 2 * 1, 5 * 3 + 4 * 2 + 3 * 1], result["out"] + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=2) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=2) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"] + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=3) - self.assertEqual((3,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=3) + self.assertEqual((3,), np.array(result["out"]).shape) + self.assertEqual( + [14.0, 3 * 14 + 2 * 3 + 2 * 1, 50 * 3 + 14 * 2 + 3 * 1], result["out"] + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=-1, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=-1, num_of_samples=5) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 4 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 0 * 3 + 5 * 2 + 4 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=0, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*4+2*3+2*1,5*3+4*2+3*1,0*3+5*2+4*1,0*3+0*2+5*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=0, num_of_samples=5) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 4 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 0 * 3 + 5 * 2 + 4 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=1, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,5*3+4*2+3*1,26*3+5*2+4*1,0*3+0*2+5*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=1, num_of_samples=5) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 14 + 2 * 3 + 2 * 1, + 5 * 3 + 4 * 2 + 3 * 1, + 26 * 3 + 5 * 2 + 4 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=2, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,0*3+5*2+4*1,14*3+0*2+5*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=2, num_of_samples=5) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 14 + 2 * 3 + 2 * 1, + 50 * 3 + 14 * 2 + 3 * 1, + 0 * 3 + 5 * 2 + 4 * 1, + 14 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) test.resetStates() - result = test({'in1': [1, 2, 3, 4, 5]}, prediction_samples=3, num_of_samples = 5) - self.assertEqual((5,), np.array(result['out']).shape) - self.assertEqual([14.0,3*14+2*3+2*1,50*3+14*2+3*1,181*3+50*2+14*1,0*3+0*2+5*1], result['out']) + result = test({"in1": [1, 2, 3, 4, 5]}, prediction_samples=3, num_of_samples=5) + self.assertEqual((5,), np.array(result["out"]).shape) + self.assertEqual( + [ + 14.0, + 3 * 14 + 2 * 3 + 2 * 1, + 50 * 3 + 14 * 2 + 3 * 1, + 181 * 3 + 50 * 2 + 14 * 1, + 0 * 3 + 0 * 2 + 5 * 1, + ], + result["out"], + ) def test_derivate_wrt_input_closed_loop(self): NeuObj.clearNames() - x = Input('x') - y = Input('y') + x = Input("x") + y = Input("y") x_last = x.last() y_last = y.last() - p=Parameter('fir',sw=1,values=[[-0.5]]) + p = Parameter("fir", sw=1, values=[[-0.5]]) fun = Sin(x_last) + Fir(W=p)(x_last) + Cos(y_last) out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last) out_der.closedLoop(x) - out = Output('out', out_der) + out = Output("out", out_der) m = Modely(visualizer=None) - m.addModel('model', [out]) + m.addModel("model", [out]) m.neuralizeModel() K = -0.5 @@ -1385,36 +2138,46 @@ def fun_data(x, y, K): x_data.append(x) y_data.append(y) - result = m({'x': [-0.2], 'y': [0.5]}, closed_loop={'y':'out'}, num_of_samples=10, prediction_samples=10) - self.TestAlmostEqual([a.tolist() for a in x_data[0:10]],result['out']) - - result = m({'x': [-0.2], 'y': [0.5]}, closed_loop={'y':'out'}, num_of_samples=10, prediction_samples='auto') - self.TestAlmostEqual([a.tolist() for a in x_data[0:10]],result['out']) + result = m( + {"x": [-0.2], "y": [0.5]}, + closed_loop={"y": "out"}, + num_of_samples=10, + prediction_samples=10, + ) + self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out"]) + + result = m( + {"x": [-0.2], "y": [0.5]}, + closed_loop={"y": "out"}, + num_of_samples=10, + prediction_samples="auto", + ) + self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out"]) def test_derivate_wrt_input_connect(self): NeuObj.clearNames() - x = Input('x') - y = Input('y') + x = Input("x") + y = Input("y") x_last = x.last() y_last = y.last() - p1 = Parameter('p1', sw=1, values=[[-0.5]]) + p1 = Parameter("p1", sw=1, values=[[-0.5]]) fun = Sin(x_last) + Fir(W=p1)(x_last) + Cos(y_last) out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last) - x2 = Input('x2') - y2 = Input('y2') + x2 = Input("x2") + y2 = Input("y2") x2_last = x2.last() y2_last = y2.last() - p2 = Parameter('p2', sw=1, values=[[3]]) + p2 = Parameter("p2", sw=1, values=[[3]]) fun2 = Sin(x2_last) + Fir(W=p2)(x2_last) + Cos(y2_last) out_der2 = Differentiate(fun2, x2_last) + Differentiate(fun2, y2_last) out_der.connect(x2) - out1 = Output('out1', out_der) - out2 = Output('out2', out_der2) + out1 = Output("out1", out_der) + out2 = Output("out2", out_der2) m = Modely(visualizer=None) - m.addModel('model', [out1,out2]) + m.addModel("model", [out1, out2]) m.neuralizeModel() K1 = -0.5 @@ -1424,89 +2187,150 @@ def fun_data(x, y, K): return K + np.cos(x) - np.sin(y) def fun_data2(x, y, K1, K2): - return K2 + np.cos(fun_data(x,y,K1)) - np.sin(fun_data(x,y,K1)) + return K2 + np.cos(fun_data(x, y, K1)) - np.sin(fun_data(x, y, K1)) x_data, y_data = [], [] x = [-0.2, 0, 0, 0, 0, 0, 0, 0, 0, 0] y = [0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0] - for (xi,yi) in zip(x,y): + for xi, yi in zip(x, y): r = fun_data2(xi, yi, K1, K2) x_data.append(r) y_data.append(r) - result = m({'x': [-0.2], 'y': [0.5]}, connect={'y2':'out1'}, num_of_samples=10, prediction_samples=10) - self.TestAlmostEqual([a.tolist() for a in x_data[0:10]],result['out2']) - - result = m({'x': [-0.2], 'y': [0.5]}, connect={'y2': 'out1'}, num_of_samples=10, prediction_samples='auto') - self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result['out2']) + result = m( + {"x": [-0.2], "y": [0.5]}, + connect={"y2": "out1"}, + num_of_samples=10, + prediction_samples=10, + ) + self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out2"]) + + result = m( + {"x": [-0.2], "y": [0.5]}, + connect={"y2": "out1"}, + num_of_samples=10, + prediction_samples="auto", + ) + self.TestAlmostEqual([a.tolist() for a in x_data[0:10]], result["out2"]) def test_inference_sampled_data(self): NeuObj.clearNames() - data_folder = os.path.join(os.path.dirname(__file__), 'get_samples_data/') + data_folder = os.path.join(os.path.dirname(__file__), "get_samples_data/") ## the state is saved inside the model so the memory is shared between different calls - x = Input('x') - y_state = Input('y') - x_p = Parameter('x_p', sw=2, dimensions=1, values=[[1.0],[1.0]]) - y_p = Parameter('y_p', sw=3, dimensions=1, values=[[2.0],[2.0],[2.0]]) + x = Input("x") + y_state = Input("y") + x_p = Parameter("x_p", sw=2, dimensions=1, values=[[1.0], [1.0]]) + y_p = Parameter("y_p", sw=3, dimensions=1, values=[[2.0], [2.0], [2.0]]) x_fir = Fir(W=x_p)(x.sw([-2, 0])) y_fir = Fir(W=y_p)(y_state.sw([0, 3])) y_fir.closedLoop(y_state) - out_x = Output('out_x', x_fir) - out_y = Output('out_y', y_fir) - out = Output('out',x_fir+y_fir) + out_x = Output("out_x", x_fir) + out_y = Output("out_y", y_fir) + out = Output("out", x_fir + y_fir) test = Modely(visualizer=None, seed=42) - test.addModel('out_all',[out, out_x, out_y]) + test.addModel("out_all", [out, out_x, out_y]) test.neuralizeModel(0.1) - data_struct = ['x','y'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1) + data_struct = ["x", "y"] + test.loadData( + name="dataset", source=data_folder, format=data_struct, skiplines=1 + ) ## Using vectorized data - result, logs = test(inputs={'x':[1,2,3,4,5,6], 'y':[1,2,3,4,5,6,7]}, prediction_samples=3, log_internal=True) - self.assertEqual(result['out_x'], [3.0, 5.0, 7.0, 9.0, 11.0]) - self.assertEqual(result['out_y'], [12.0, 34.0, 98.0, 288.0, 36.0]) - self.assertEqual(result['out'], [x+y for x, y in zip(result['out_x'], result['out_y'])]) - self.assertDictEqual(logs['ingress'][0], {'x': [[[1.0], [2.0]]], 'y': [[[1.0], [2.0], [3.0]]]}) - self.assertDictEqual(logs['ingress'][1], {'x': [[[2.0], [3.0]]], 'y': [[[2.0], [3.0], [12.0]]]}) - self.assertDictEqual(logs['ingress'][2], {'x': [[[3.0], [4.0]]], 'y': [[[3.0], [12.0], [34.0]]]}) - self.assertDictEqual(logs['ingress'][3], {'x': [[[4.0], [5.0]]], 'y': [[[12.0], [34.0], [98.0]]]}) - self.assertDictEqual(logs['ingress'][4], {'x': [[[5.0], [6.0]]], 'y': [[[5.0], [6.0], [7.0]]]}) - self.assertDictEqual(logs['closedLoop'][0], {'y': torch.tensor([[[12.]]])}) - self.assertDictEqual(logs['closedLoop'][1], {'y': torch.tensor([[[34.]]])}) - self.assertDictEqual(logs['closedLoop'][2], {'y': torch.tensor([[[98.]]])}) + result, logs = test( + inputs={"x": [1, 2, 3, 4, 5, 6], "y": [1, 2, 3, 4, 5, 6, 7]}, + prediction_samples=3, + log_internal=True, + ) + self.assertEqual(result["out_x"], [3.0, 5.0, 7.0, 9.0, 11.0]) + self.assertEqual(result["out_y"], [12.0, 34.0, 98.0, 288.0, 36.0]) + self.assertEqual( + result["out"], [x + y for x, y in zip(result["out_x"], result["out_y"])] + ) + self.assertDictEqual( + logs["ingress"][0], {"x": [[[1.0], [2.0]]], "y": [[[1.0], [2.0], [3.0]]]} + ) + self.assertDictEqual( + logs["ingress"][1], {"x": [[[2.0], [3.0]]], "y": [[[2.0], [3.0], [12.0]]]} + ) + self.assertDictEqual( + logs["ingress"][2], {"x": [[[3.0], [4.0]]], "y": [[[3.0], [12.0], [34.0]]]} + ) + self.assertDictEqual( + logs["ingress"][3], {"x": [[[4.0], [5.0]]], "y": [[[12.0], [34.0], [98.0]]]} + ) + self.assertDictEqual( + logs["ingress"][4], {"x": [[[5.0], [6.0]]], "y": [[[5.0], [6.0], [7.0]]]} + ) + self.assertDictEqual(logs["closedLoop"][0], {"y": torch.tensor([[[12.0]]])}) + self.assertDictEqual(logs["closedLoop"][1], {"y": torch.tensor([[[34.0]]])}) + self.assertDictEqual(logs["closedLoop"][2], {"y": torch.tensor([[[98.0]]])}) ## Using sampled data test.resetStates() - result, logs = test(inputs={'x':[[1,2],[2,3],[3,4],[4,5],[5,6]], 'y':[[1,2,3],[2,3,4],[3,4,5],[4,5,6],[5,6,7]]}, sampled=True, prediction_samples=3, log_internal=True) - self.assertEqual(result['out_x'], [3.0, 5.0, 7.0, 9.0, 11.0]) - self.assertEqual(result['out_y'], [12.0, 34.0, 98.0, 288.0, 36.0]) - self.assertEqual(result['out'], [x+y for x, y in zip(result['out_x'], result['out_y'])]) - self.assertDictEqual(logs['ingress'][0], {'x': [[[1.0], [2.0]]], 'y': [[[1.0], [2.0], [3.0]]]}) - self.assertDictEqual(logs['ingress'][1], {'x': [[[2.0], [3.0]]], 'y': [[[2.0], [3.0], [12.0]]]}) - self.assertDictEqual(logs['ingress'][2], {'x': [[[3.0], [4.0]]], 'y': [[[3.0], [12.0], [34.0]]]}) - self.assertDictEqual(logs['ingress'][3], {'x': [[[4.0], [5.0]]], 'y': [[[12.0], [34.0], [98.0]]]}) - self.assertDictEqual(logs['ingress'][4], {'x': [[[5.0], [6.0]]], 'y': [[[5.0], [6.0], [7.0]]]}) - self.assertDictEqual(logs['closedLoop'][0], {'y': torch.tensor([[[12.]]])}) - self.assertDictEqual(logs['closedLoop'][1], {'y': torch.tensor([[[34.]]])}) - self.assertDictEqual(logs['closedLoop'][2], {'y': torch.tensor([[[98.]]])}) + result, logs = test( + inputs={ + "x": [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6]], + "y": [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7]], + }, + sampled=True, + prediction_samples=3, + log_internal=True, + ) + self.assertEqual(result["out_x"], [3.0, 5.0, 7.0, 9.0, 11.0]) + self.assertEqual(result["out_y"], [12.0, 34.0, 98.0, 288.0, 36.0]) + self.assertEqual( + result["out"], [x + y for x, y in zip(result["out_x"], result["out_y"])] + ) + self.assertDictEqual( + logs["ingress"][0], {"x": [[[1.0], [2.0]]], "y": [[[1.0], [2.0], [3.0]]]} + ) + self.assertDictEqual( + logs["ingress"][1], {"x": [[[2.0], [3.0]]], "y": [[[2.0], [3.0], [12.0]]]} + ) + self.assertDictEqual( + logs["ingress"][2], {"x": [[[3.0], [4.0]]], "y": [[[3.0], [12.0], [34.0]]]} + ) + self.assertDictEqual( + logs["ingress"][3], {"x": [[[4.0], [5.0]]], "y": [[[12.0], [34.0], [98.0]]]} + ) + self.assertDictEqual( + logs["ingress"][4], {"x": [[[5.0], [6.0]]], "y": [[[5.0], [6.0], [7.0]]]} + ) + self.assertDictEqual(logs["closedLoop"][0], {"y": torch.tensor([[[12.0]]])}) + self.assertDictEqual(logs["closedLoop"][1], {"y": torch.tensor([[[34.0]]])}) + self.assertDictEqual(logs["closedLoop"][2], {"y": torch.tensor([[[98.0]]])}) # ## Using get_sampled from a dataset test.resetStates() - inputs = test.getSamples('dataset', window=5) - result, logs = test(inputs=inputs, sampled=True, prediction_samples=3, log_internal=True) - self.assertEqual(result['out_x'], [3.0, 5.0, 7.0, 9.0, 11.0]) - self.assertEqual(result['out_y'], [12.0, 34.0, 98.0, 288.0, 36.0]) - self.assertEqual(result['out'], [x+y for x, y in zip(result['out_x'], result['out_y'])]) - self.assertDictEqual(logs['ingress'][0], {'x': [[[1.0], [2.0]]], 'y': [[[1.0], [2.0], [3.0]]]}) - self.assertDictEqual(logs['ingress'][1], {'x': [[[2.0], [3.0]]], 'y': [[[2.0], [3.0], [12.0]]]}) - self.assertDictEqual(logs['ingress'][2], {'x': [[[3.0], [4.0]]], 'y': [[[3.0], [12.0], [34.0]]]}) - self.assertDictEqual(logs['ingress'][3], {'x': [[[4.0], [5.0]]], 'y': [[[12.0], [34.0], [98.0]]]}) - self.assertDictEqual(logs['ingress'][4], {'x': [[[5.0], [6.0]]], 'y': [[[5.0], [6.0], [7.0]]]}) - self.assertDictEqual(logs['closedLoop'][0], {'y': torch.tensor([[[12.]]])}) - self.assertDictEqual(logs['closedLoop'][1], {'y': torch.tensor([[[34.]]])}) - self.assertDictEqual(logs['closedLoop'][2], {'y': torch.tensor([[[98.]]])}) - + inputs = test.getSamples("dataset", window=5) + result, logs = test( + inputs=inputs, sampled=True, prediction_samples=3, log_internal=True + ) + self.assertEqual(result["out_x"], [3.0, 5.0, 7.0, 9.0, 11.0]) + self.assertEqual(result["out_y"], [12.0, 34.0, 98.0, 288.0, 36.0]) + self.assertEqual( + result["out"], [x + y for x, y in zip(result["out_x"], result["out_y"])] + ) + self.assertDictEqual( + logs["ingress"][0], {"x": [[[1.0], [2.0]]], "y": [[[1.0], [2.0], [3.0]]]} + ) + self.assertDictEqual( + logs["ingress"][1], {"x": [[[2.0], [3.0]]], "y": [[[2.0], [3.0], [12.0]]]} + ) + self.assertDictEqual( + logs["ingress"][2], {"x": [[[3.0], [4.0]]], "y": [[[3.0], [12.0], [34.0]]]} + ) + self.assertDictEqual( + logs["ingress"][3], {"x": [[[4.0], [5.0]]], "y": [[[12.0], [34.0], [98.0]]]} + ) + self.assertDictEqual( + logs["ingress"][4], {"x": [[[5.0], [6.0]]], "y": [[[5.0], [6.0], [7.0]]]} + ) + self.assertDictEqual(logs["closedLoop"][0], {"y": torch.tensor([[[12.0]]])}) + self.assertDictEqual(logs["closedLoop"][1], {"y": torch.tensor([[[34.0]]])}) + self.assertDictEqual(logs["closedLoop"][2], {"y": torch.tensor([[[98.0]]])}) # def test_state_initialization_inference(self): # NeuObj.clearNames() @@ -1592,4 +2416,4 @@ def test_inference_sampled_data(self): # mass_dyn = Modely() # mass_dyn.addModel('', [mass_x, mass_y, mass_dx, mass_dy]) # mass_dyn.neuralizeModel(0.01) - # example = mass_dyn({'x0': [7], 'y0': [7], 'dx0': [5], 'dy0': [5], 'Fx': [100], 'Fy': [100]}, num_of_samples=200) \ No newline at end of file + # example = mass_dyn({'x0': [7], 'y0': [7], 'dx0': [5], 'dy0': [5], 'Fx': [100], 'Fy': [100]}, num_of_samples=200) diff --git a/tests/test_network_element.py b/tests/test_network_element.py index 150c5644..10720c1d 100644 --- a/tests/test_network_element.py +++ b/tests/test_network_element.py @@ -14,12 +14,17 @@ # 11 Tests # This file tests the dimensions and the of the element created in the pytorch environment -class ModelyNetworkBuildingTest(unittest.TestCase): +class ModelyNetworkBuildingTest(unittest.TestCase): def TestAlmostEqual(self, data1, data2, precision=4): - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2, dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) if type(data1) == type(data2) == list: - self.assertEqual(len(data1),len(data2)) + self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): self.TestAlmostEqual(pred, label, precision=precision) else: @@ -27,428 +32,648 @@ def TestAlmostEqual(self, data1, data2, precision=4): def test_network_building_very_simple(self): NeuObj.clearNames() - input1 = Input('in1').last() + input1 = Input("in1").last() rel1 = Fir(input1) - fun = Output('out', rel1) + fun = Output("out", rel1) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) list_of_dimensions = [[1, 1], [1, 1]] - for ind, (key, value) in enumerate({k: v for k, v in test._model.relation_forward.items() if 'Fir' in k}.items()): - self.assertEqual(list_of_dimensions[ind],list(value.weights.shape)) - + for ind, (key, value) in enumerate( + { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items() + ): + self.assertEqual(list_of_dimensions[ind], list(value.weights.shape)) + def test_network_building_simple(self): NeuObj.clearNames() Stream.resetCount() - input1 = Input('in1') + input1 = Input("in1") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input1.tw(0.01)) - fun = Output('out',rel1+rel2) + fun = Output("out", rel1 + rel2) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - - list_of_dimensions = {'Fir2':[5,1],'Fir5':[1,1]} - for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items(): - self.assertEqual(list_of_dimensions[key],list(value.weights.shape)) + + list_of_dimensions = {"Fir2": [5, 1], "Fir5": [1, 1]} + for key, value in { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items(): + self.assertEqual(list_of_dimensions[key], list(value.weights.shape)) def test_network_building_tw(self): NeuObj.clearNames() Stream.resetCount() - input1 = Input('in1') - input2 = Input('in2') + input1 = Input("in1") + input2 = Input("in2") rel1 = Fir(input1.tw(0.05)) rel2 = Fir(input1.tw(0.01)) rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.02,0.02])) - fun = Output('out',rel1+rel2+rel3+rel4) + rel4 = Fir(input2.tw([-0.02, 0.02])) + fun = Output("out", rel1 + rel2 + rel3 + rel4) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - - list_of_dimensions = {'Fir2':[5,1], 'Fir5':[1,1], 'Fir8':[5,1], 'Fir11':[4,1]} - for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items(): - self.assertEqual(list_of_dimensions[key],list(value.weights.shape)) - + + list_of_dimensions = { + "Fir2": [5, 1], + "Fir5": [1, 1], + "Fir8": [5, 1], + "Fir11": [4, 1], + } + for key, value in { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items(): + self.assertEqual(list_of_dimensions[key], list(value.weights.shape)) + def test_network_building_tw2(self): Stream.resetCount() NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.02,0.02])) - rel5 = Fir(input2.tw([-0.03,0.03])) + rel4 = Fir(input2.tw([-0.02, 0.02])) + rel5 = Fir(input2.tw([-0.03, 0.03])) rel6 = Fir(input2.tw([-0.03, 0])) rel7 = Fir(input2.tw(0.03)) - fun = Output('out',rel3+rel4+rel5+rel6+rel7) + fun = Output("out", rel3 + rel4 + rel5 + rel6 + rel7) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - self.assertEqual(test._max_n_samples, 8) # 5 samples + 3 samples of the horizon - self.assertEqual({'in2': 8} , test._input_n_samples) - - list_of_dimensions = {'Fir2':[5,1], 'Fir5':[4,1], 'Fir8':[6,1], 'Fir11':[3,1], 'Fir14':[3,1]} - for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items(): - self.assertEqual(list_of_dimensions[key],list(value.weights.shape)) + self.assertEqual(test._max_n_samples, 8) # 5 samples + 3 samples of the horizon + self.assertEqual({"in2": 8}, test._input_n_samples) + + list_of_dimensions = { + "Fir2": [5, 1], + "Fir5": [4, 1], + "Fir8": [6, 1], + "Fir11": [3, 1], + "Fir14": [3, 1], + } + for key, value in { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items(): + self.assertEqual(list_of_dimensions[key], list(value.weights.shape)) def test_network_building_tw3(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.01,0.03])) - rel5 = Fir(input2.tw([-0.04,0.01])) - fun = Output('out',rel3+rel4+rel5) + rel4 = Fir(input2.tw([-0.01, 0.03])) + rel5 = Fir(input2.tw([-0.04, 0.01])) + fun = Output("out", rel3 + rel4 + rel5) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - list_of_dimensions = [[5,1], [4,1], [5,1]] - for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()): - self.assertEqual(list_of_dimensions[ind],list(value.weights.shape)) + list_of_dimensions = [[5, 1], [4, 1], [5, 1]] + for ind, (key, value) in enumerate( + { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items() + ): + self.assertEqual(list_of_dimensions[ind], list(value.weights.shape)) def test_network_building_tw_with_offest(self): NeuObj.clearNames() Stream.resetCount() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.tw(0.05)) - rel4 = Fir(input2.tw([-0.04,0.02])) - rel5 = Fir(input2.tw([-0.04,0.02],offset=0)) - rel6 = Fir(input2.tw([-0.04,0.02],offset=0.01)) - fun = Output('out',rel3+rel4+rel5+rel6) - + rel4 = Fir(input2.tw([-0.04, 0.02])) + rel5 = Fir(input2.tw([-0.04, 0.02], offset=0)) + rel6 = Fir(input2.tw([-0.04, 0.02], offset=0.01)) + fun = Output("out", rel3 + rel4 + rel5 + rel6) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - list_of_dimensions = {'Fir2':[5,1], 'Fir5':[6,1], 'Fir8':[6,1], 'Fir11':[6,1]} - for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items(): - self.assertEqual(list_of_dimensions[key],list(value.weights.shape)) + list_of_dimensions = { + "Fir2": [5, 1], + "Fir5": [6, 1], + "Fir8": [6, 1], + "Fir11": [6, 1], + } + for key, value in { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items(): + self.assertEqual(list_of_dimensions[key], list(value.weights.shape)) def test_network_building_tw_negative(self): NeuObj.clearNames() - input2 = Input('in2') - rel1 = Fir(input2.tw([-0.04,-0.01])) - rel2 = Fir(input2.tw([-0.06,-0.03])) - fun = Output('out',rel1+rel2) + input2 = Input("in2") + rel1 = Fir(input2.tw([-0.04, -0.01])) + rel2 = Fir(input2.tw([-0.06, -0.03])) + fun = Output("out", rel1 + rel2) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - list_of_dimensions = [[3,1], [3,1]] - for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()): - self.assertEqual(list_of_dimensions[ind],list(value.weights.shape)) + list_of_dimensions = [[3, 1], [3, 1]] + for ind, (key, value) in enumerate( + { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items() + ): + self.assertEqual(list_of_dimensions[ind], list(value.weights.shape)) def test_network_building_tw_positive(self): NeuObj.clearNames() - input2 = Input('in2') - rel1 = Fir(input2.tw([0.01,0.04])) - rel2 = Fir(input2.tw([0.03,0.06])) - fun = Output('out',rel1+rel2) + input2 = Input("in2") + rel1 = Fir(input2.tw([0.01, 0.04])) + rel2 = Fir(input2.tw([0.03, 0.06])) + fun = Output("out", rel1 + rel2) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - list_of_dimensions = [[3,1], [3,1]] - for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()): - self.assertEqual(list_of_dimensions[ind],list(value.weights.shape)) + list_of_dimensions = [[3, 1], [3, 1]] + for ind, (key, value) in enumerate( + { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items() + ): + self.assertEqual(list_of_dimensions[ind], list(value.weights.shape)) def test_network_building_sw_with_offset(self): Stream.resetCount() NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") rel3 = Fir(input2.sw(5)) - rel4 = Fir(input2.sw([-4,2])) - rel5 = Fir(input2.sw([-4,2],offset=0)) - rel6 = Fir(input2.sw([-4,2],offset=1)) - fun = Output('out',rel3+rel4+rel5+rel6) + rel4 = Fir(input2.sw([-4, 2])) + rel5 = Fir(input2.sw([-4, 2], offset=0)) + rel6 = Fir(input2.sw([-4, 2], offset=1)) + fun = Output("out", rel3 + rel4 + rel5 + rel6) test = Modely(visualizer=None, seed=1) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - list_of_dimensions = {'Fir2':[5,1], 'Fir5':[6,1], 'Fir8':[6,1], 'Fir11':[6,1]} - for key, value in {k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items(): - self.assertEqual(list_of_dimensions[key],list(value.weights.shape)) + list_of_dimensions = { + "Fir2": [5, 1], + "Fir5": [6, 1], + "Fir8": [6, 1], + "Fir11": [6, 1], + } + for key, value in { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items(): + self.assertEqual(list_of_dimensions[key], list(value.weights.shape)) def test_network_building_sw_and_tw(self): NeuObj.clearNames() - input2 = Input('in2') + input2 = Input("in2") with self.assertRaises(TypeError): - input2.sw(5)+input2.tw(0.05) + input2.sw(5) + input2.tw(0.05) - rel1 = Fir(input2.sw([-4,2]))+Fir(input2.tw([-0.01,0])) - fun = Output('out',rel1) + rel1 = Fir(input2.sw([-4, 2])) + Fir(input2.tw([-0.01, 0])) + fun = Output("out", rel1) test = Modely(visualizer=None) - test.addModel('fun',fun) + test.addModel("fun", fun) test.neuralizeModel(0.01) - list_of_dimensions = [[6,1], [1,1]] - for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Fir' in k}.items()): - self.assertEqual(list_of_dimensions[ind],list(value.weights.shape)) + list_of_dimensions = [[6, 1], [1, 1]] + for ind, (key, value) in enumerate( + { + k: v for k, v in test._model.relation_forward.items() if "Fir" in k + }.items() + ): + self.assertEqual(list_of_dimensions[ind], list(value.weights.shape)) def test_network_linear(self): NeuObj.clearNames() - input = Input('in1') - rel1 = Linear(input.sw([-4,2])) + input = Input("in1") + rel1 = Linear(input.sw([-4, 2])) rel2 = Linear(5)(input.sw([-1, 2])) - fun1 = Output('out1',rel1) - fun2 = Output('out2', rel2) + fun1 = Output("out1", rel1) + fun2 = Output("out2", rel2) - input5 = Input('in5', dimensions=3) - rel15 = Linear(input5.sw([-4,2])) + input5 = Input("in5", dimensions=3) + rel15 = Linear(input5.sw([-4, 2])) rel25 = Linear(5)(input5.last()) - fun15 = Output('out51',rel15) - fun25 = Output('out52', rel25) + fun15 = Output("out51", rel15) + fun25 = Output("out52", rel25) - test = Modely(seed =1, visualizer=None) - test.addModel('fun',[fun1,fun2,fun15,fun25]) + test = Modely(seed=1, visualizer=None) + test.addModel("fun", [fun1, fun2, fun15, fun25]) test.neuralizeModel(0.01) - list_of_dimensions = [[1,1],[1,5],[3,1],[3,5]] - for ind, (key, value) in enumerate({k:v for k,v in test._model.relation_forward.items() if 'Linear' in k}.items()): - self.assertEqual(list_of_dimensions[ind],list(value.weights.shape)) + list_of_dimensions = [[1, 1], [1, 5], [3, 1], [3, 5]] + for ind, (key, value) in enumerate( + { + k: v for k, v in test._model.relation_forward.items() if "Linear" in k + }.items() + ): + self.assertEqual(list_of_dimensions[ind], list(value.weights.shape)) def test_network_linear_interpolation_train(self): NeuObj.clearNames() - x = Input('x') - param = Parameter(name='a', sw=1) - rel1 = Fir(W=param)(Interpolation(x_points=[1.0, 2.0, 3.0, 4.0],y_points=[2.0, 4.0, 6.0, 8.0], mode='linear')(x.last())) - out = Output('out',rel1) - - test = Modely(seed = 1, visualizer=None) - test.addModel('fun',[out]) - test.addMinimize('error', out, x.last()) + x = Input("x") + param = Parameter(name="a", sw=1) + rel1 = Fir(W=param)( + Interpolation( + x_points=[1.0, 2.0, 3.0, 4.0], + y_points=[2.0, 4.0, 6.0, 8.0], + mode="linear", + )(x.last()) + ) + out = Output("out", rel1) + + test = Modely(seed=1, visualizer=None) + test.addModel("fun", [out]) + test.addMinimize("error", out, x.last()) test.neuralizeModel(0.01) - dataset = {'x':np.random.uniform(1,4,100)} - test.loadData(name='dataset', source=dataset) + dataset = {"x": np.random.uniform(1, 4, 100)} + test.loadData(name="dataset", source=dataset) test.trainModel(num_of_epochs=100, train_batch_size=10) - self.assertAlmostEqual(test.parameters['a'][0][0], 0.5, places=2) + self.assertAlmostEqual(test.parameters["a"][0][0], 0.5, places=2) def test_network_linear_interpolation(self): NeuObj.clearNames() - x = Input('x') - rel1 = Interpolation(x_points=[1.0, 2.0, 3.0, 4.0],y_points=[1.0, 4.0, 9.0, 16.0], mode='linear')(x.last()) - out = Output('out',rel1) + x = Input("x") + rel1 = Interpolation( + x_points=[1.0, 2.0, 3.0, 4.0], y_points=[1.0, 4.0, 9.0, 16.0], mode="linear" + )(x.last()) + out = Output("out", rel1) - test = Modely(seed=1,visualizer=None) - test.addModel('fun',[out]) + test = Modely(seed=1, visualizer=None) + test.addModel("fun", [out]) test.neuralizeModel(0.01) - inference = test(inputs={'x':[1.5,2.5,3.5]}) - self.assertEqual(inference['out'],[2.5,6.5,12.5]) + inference = test(inputs={"x": [1.5, 2.5, 3.5]}) + self.assertEqual(inference["out"], [2.5, 6.5, 12.5]) - x1 = Input('x1') - rel1 = Interpolation(x_points=[1.0, 4.0, 3.0, 2.0],y_points=[1.0, 16.0, 9.0, 4.0], mode='linear')(x1.last()) - out = Output('out1',rel1) + x1 = Input("x1") + rel1 = Interpolation( + x_points=[1.0, 4.0, 3.0, 2.0], y_points=[1.0, 16.0, 9.0, 4.0], mode="linear" + )(x1.last()) + out = Output("out1", rel1) test = Modely(visualizer=None) - test.addModel('fun',[out]) + test.addModel("fun", [out]) test.neuralizeModel(0.01) - inference = test(inputs={'x1':[1.5,2.5,3.5]}) - self.assertEqual(inference['out1'],[2.5,6.5,12.5]) + inference = test(inputs={"x1": [1.5, 2.5, 3.5]}) + self.assertEqual(inference["out1"], [2.5, 6.5, 12.5]) def test_softmax_and_sigmoid(self): NeuObj.clearNames() - x = Input('x') - y = Input('y', dimensions=3) + x = Input("x") + y = Input("y", dimensions=3) softmax = Softmax(y.last()) sigmoid = Sigmoid(x.last()) - out = Output('softmax',softmax) - out2 = Output('sigmoid',sigmoid) + out = Output("softmax", softmax) + out2 = Output("sigmoid", sigmoid) test = Modely(visualizer=None) - test.addModel('model',[out,out2]) + test.addModel("model", [out, out2]) test.neuralizeModel(0.01) - inference = test(inputs={'x':[-1000.0, 0.0, 1000.0], 'y':[[-1.0,0.0,1.0],[-1000.0,0.0,1000.0],[1.0,2.0,3.0]]}) - self.assertEqual(inference['sigmoid'],[0.0, 0.5, 1.0]) - self.assertEqual(inference['softmax'],[[[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]], - [[0.0, 0.0, 1.0]], - [[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]]]) + inference = test( + inputs={ + "x": [-1000.0, 0.0, 1000.0], + "y": [[-1.0, 0.0, 1.0], [-1000.0, 0.0, 1000.0], [1.0, 2.0, 3.0]], + } + ) + self.assertEqual(inference["sigmoid"], [0.0, 0.5, 1.0]) + self.assertEqual( + inference["softmax"], + [ + [[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]], + [[0.0, 0.0, 1.0]], + [[0.09003057330846786, 0.2447284758090973, 0.6652409434318542]], + ], + ) def test_sech_cosh_function(self): torch.manual_seed(1) - input = Input('in1') + input = Input("in1") sech_rel = Sech(input.last()) sech_rel_2 = Sech(input.sw(2)) cosh_rel = Cosh(input.last()) cosh_rel_2 = Cosh(input.sw(2)) - input5 = Input('in5', dimensions=5) + input5 = Input("in5", dimensions=5) sech_rel_5 = Sech(input5.last()) cosh_rel_5 = Cosh(input5.last()) - out1 = Output('sech_out_1', sech_rel) - out2 = Output('sech_out_2', sech_rel_2) - out3 = Output('sech_out_3', sech_rel_5) - out4 = Output('cosh_out_1', cosh_rel) - out5 = Output('cosh_out_2', cosh_rel_2) - out6 = Output('cosh_out_3', cosh_rel_5) + out1 = Output("sech_out_1", sech_rel) + out2 = Output("sech_out_2", sech_rel_2) + out3 = Output("sech_out_3", sech_rel_5) + out4 = Output("cosh_out_1", cosh_rel) + out5 = Output("cosh_out_2", cosh_rel_2) + out6 = Output("cosh_out_3", cosh_rel_5) test = Modely(visualizer=None) - test.addModel('model',[out1,out2,out3,out4,out5,out6]) + test.addModel("model", [out1, out2, out3, out4, out5, out6]) test.neuralizeModel(0.01) - result = test(inputs={'in1':[[3.0],[-2.0]], 'in5':[[4.0,1.0,0.0,-6.0,2.0]]}) - self.TestAlmostEqual([0.2658022344112396], result['sech_out_1']) - self.TestAlmostEqual([[0.0993279218673706, 0.2658022344112396]], result['sech_out_2']) - self.TestAlmostEqual([[[0.03661899268627167, 0.6480542421340942, 1.0, 0.004957473836839199, 0.2658022344112396]]], result['sech_out_3']) - self.TestAlmostEqual([3.762195587158203], result['cosh_out_1']) - self.TestAlmostEqual([[10.067662239074707, 3.762195587158203]], result['cosh_out_2']) - self.TestAlmostEqual([[[27.3082332611084, 1.5430806875228882, 1.0, 201.71563720703125, 3.762195587158203]]], result['cosh_out_3']) + result = test( + inputs={"in1": [[3.0], [-2.0]], "in5": [[4.0, 1.0, 0.0, -6.0, 2.0]]} + ) + self.TestAlmostEqual([0.2658022344112396], result["sech_out_1"]) + self.TestAlmostEqual( + [[0.0993279218673706, 0.2658022344112396]], result["sech_out_2"] + ) + self.TestAlmostEqual( + [ + [ + [ + 0.03661899268627167, + 0.6480542421340942, + 1.0, + 0.004957473836839199, + 0.2658022344112396, + ] + ] + ], + result["sech_out_3"], + ) + self.TestAlmostEqual([3.762195587158203], result["cosh_out_1"]) + self.TestAlmostEqual( + [[10.067662239074707, 3.762195587158203]], result["cosh_out_2"] + ) + self.TestAlmostEqual( + [ + [ + [ + 27.3082332611084, + 1.5430806875228882, + 1.0, + 201.71563720703125, + 3.762195587158203, + ] + ] + ], + result["cosh_out_3"], + ) def test_concatenate_time_concatenate(self): NeuObj.clearNames() - input = Input('in1') - input2 = Input('in2') - concatenate_rel = Concatenate(input.last(),input2.last()) - timeconcatenate_rel = TimeConcatenate(input.last(),input2.last()) - concatenate_tw_rel = Concatenate(input.tw(3),input2.tw(3)) - timeconcatenate_tw_rel = TimeConcatenate(input.tw(3),input2.tw(3)) - - input3 = Input('in3', dimensions=5) - input4 = Input('in4', dimensions=5) - - concatenate_rel_5 = Concatenate(input3.last(),input4.last()) - timeconcatenate_rel_5 = TimeConcatenate(input3.last(),input4.last()) - concatenate_tw_rel_5 = Concatenate(input3.tw(3),input4.tw(3)) - timeconcatenate_tw_rel_5 = TimeConcatenate(input3.tw(3),input4.tw(3)) - - out1 = Output('concatenate', concatenate_rel) - out2 = Output('time_concatenate', timeconcatenate_rel) - out3 = Output('concatenate_tw', concatenate_tw_rel) - out4 = Output('time_concatenate_tw', timeconcatenate_tw_rel) - out5 = Output('concatenate_5', concatenate_rel_5) - out6 = Output('time_concatenate_5', timeconcatenate_rel_5) - out7 = Output('concatenate_tw_5', concatenate_tw_rel_5) - out8 = Output('time_concatenate_tw_5', timeconcatenate_tw_rel_5) - - test = Modely(seed=1,visualizer=None) - test.addModel('model',[out1,out2,out3,out4,out5,out6,out7,out8]) + input = Input("in1") + input2 = Input("in2") + concatenate_rel = Concatenate(input.last(), input2.last()) + timeconcatenate_rel = TimeConcatenate(input.last(), input2.last()) + concatenate_tw_rel = Concatenate(input.tw(3), input2.tw(3)) + timeconcatenate_tw_rel = TimeConcatenate(input.tw(3), input2.tw(3)) + + input3 = Input("in3", dimensions=5) + input4 = Input("in4", dimensions=5) + + concatenate_rel_5 = Concatenate(input3.last(), input4.last()) + timeconcatenate_rel_5 = TimeConcatenate(input3.last(), input4.last()) + concatenate_tw_rel_5 = Concatenate(input3.tw(3), input4.tw(3)) + timeconcatenate_tw_rel_5 = TimeConcatenate(input3.tw(3), input4.tw(3)) + + out1 = Output("concatenate", concatenate_rel) + out2 = Output("time_concatenate", timeconcatenate_rel) + out3 = Output("concatenate_tw", concatenate_tw_rel) + out4 = Output("time_concatenate_tw", timeconcatenate_tw_rel) + out5 = Output("concatenate_5", concatenate_rel_5) + out6 = Output("time_concatenate_5", timeconcatenate_rel_5) + out7 = Output("concatenate_tw_5", concatenate_tw_rel_5) + out8 = Output("time_concatenate_tw_5", timeconcatenate_tw_rel_5) + + test = Modely(seed=1, visualizer=None) + test.addModel("model", [out1, out2, out3, out4, out5, out6, out7, out8]) test.neuralizeModel(1) - result = test(inputs={'in1':[[1.0],[2.0],[3.0]], 'in2':[[4.0],[5.0],[6.0]], - 'in3':[[7.0,8.0,9.0,10.0,11.0],[12.0,13.0,14.0,15.0,16.0],[17.0,18.0,19.0,20.0,21.0]], - 'in4':[[22.0,23.0,24.0,25.0,26.0],[27.0,28.0,29.0,30.0,31.0],[32.0,33.0,34.0,35.0,36.0]]}) - self.assertEqual((1,1,2), np.array(result['concatenate']).shape) - self.assertEqual([[[3.0, 6.0]]], result['concatenate']) - self.assertEqual((1,2), np.array(result['time_concatenate']).shape) - self.assertEqual([[3.0, 6.0]], result['time_concatenate']) - self.assertEqual((1,3,2), np.array(result['concatenate_tw']).shape) - self.assertEqual([[[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]], result['concatenate_tw']) - self.assertEqual((1,6), np.array(result['time_concatenate_tw']).shape) - self.assertEqual([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]], result['time_concatenate_tw']) - self.assertEqual((1,1,10), np.array(result['concatenate_5']).shape) - self.assertEqual([[[17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0]]], result['concatenate_5']) - self.assertEqual((1,2,5), np.array(result['time_concatenate_5']).shape) - self.assertEqual([[[17.0, 18.0, 19.0, 20.0, 21.0], [32.0, 33.0, 34.0, 35.0, 36.0]]], result['time_concatenate_5']) - self.assertEqual((1,3,10), np.array(result['concatenate_tw_5']).shape) - self.assertEqual([[[7.0, 8.0, 9.0, 10.0, 11.0, 22.0, 23.0, 24.0, 25.0, 26.0], - [12.0, 13.0, 14.0, 15.0, 16.0, 27.0, 28.0, 29.0, 30.0, 31.0], - [17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0]]], result['concatenate_tw_5']) - self.assertEqual((1,6,5), np.array(result['time_concatenate_tw_5']).shape) - self.assertEqual([[[7.0, 8.0, 9.0, 10.0, 11.0], - [12.0, 13.0, 14.0, 15.0, 16.0], - [17.0, 18.0, 19.0, 20.0, 21.0], - [22.0, 23.0, 24.0, 25.0, 26.0], - [27.0, 28.0, 29.0, 30.0, 31.0], - [32.0, 33.0, 34.0, 35.0, 36.0]]], result['time_concatenate_tw_5']) + result = test( + inputs={ + "in1": [[1.0], [2.0], [3.0]], + "in2": [[4.0], [5.0], [6.0]], + "in3": [ + [7.0, 8.0, 9.0, 10.0, 11.0], + [12.0, 13.0, 14.0, 15.0, 16.0], + [17.0, 18.0, 19.0, 20.0, 21.0], + ], + "in4": [ + [22.0, 23.0, 24.0, 25.0, 26.0], + [27.0, 28.0, 29.0, 30.0, 31.0], + [32.0, 33.0, 34.0, 35.0, 36.0], + ], + } + ) + self.assertEqual((1, 1, 2), np.array(result["concatenate"]).shape) + self.assertEqual([[[3.0, 6.0]]], result["concatenate"]) + self.assertEqual((1, 2), np.array(result["time_concatenate"]).shape) + self.assertEqual([[3.0, 6.0]], result["time_concatenate"]) + self.assertEqual((1, 3, 2), np.array(result["concatenate_tw"]).shape) + self.assertEqual( + [[[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]]], result["concatenate_tw"] + ) + self.assertEqual((1, 6), np.array(result["time_concatenate_tw"]).shape) + self.assertEqual( + [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]], result["time_concatenate_tw"] + ) + self.assertEqual((1, 1, 10), np.array(result["concatenate_5"]).shape) + self.assertEqual( + [[[17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0]]], + result["concatenate_5"], + ) + self.assertEqual((1, 2, 5), np.array(result["time_concatenate_5"]).shape) + self.assertEqual( + [[[17.0, 18.0, 19.0, 20.0, 21.0], [32.0, 33.0, 34.0, 35.0, 36.0]]], + result["time_concatenate_5"], + ) + self.assertEqual((1, 3, 10), np.array(result["concatenate_tw_5"]).shape) + self.assertEqual( + [ + [ + [7.0, 8.0, 9.0, 10.0, 11.0, 22.0, 23.0, 24.0, 25.0, 26.0], + [12.0, 13.0, 14.0, 15.0, 16.0, 27.0, 28.0, 29.0, 30.0, 31.0], + [17.0, 18.0, 19.0, 20.0, 21.0, 32.0, 33.0, 34.0, 35.0, 36.0], + ] + ], + result["concatenate_tw_5"], + ) + self.assertEqual((1, 6, 5), np.array(result["time_concatenate_tw_5"]).shape) + self.assertEqual( + [ + [ + [7.0, 8.0, 9.0, 10.0, 11.0], + [12.0, 13.0, 14.0, 15.0, 16.0], + [17.0, 18.0, 19.0, 20.0, 21.0], + [22.0, 23.0, 24.0, 25.0, 26.0], + [27.0, 28.0, 29.0, 30.0, 31.0], + [32.0, 33.0, 34.0, 35.0, 36.0], + ] + ], + result["time_concatenate_tw_5"], + ) def test_equation_learner(self): NeuObj.clearNames() - x = Input('x') - F = Input('F') - - def myFun(p1,p2,k1,k2): - return k1*p1+k2*p2 - - K1 = Parameter('k1', dimensions = 1, sw = 1,values=[[2.0]]) - K2 = Parameter('k2', dimensions = 1, sw = 1,values=[[3.0]]) - parfun = ParamFun(myFun, parameters_and_constants=[K1,K2]) - parfun2 = ParamFun(myFun, parameters_and_constants=[K1,K2]) - parfun3 = ParamFun(myFun, parameters_and_constants=[K1,K2]) - fuzzi = Fuzzify(centers=[0,1,2,3]) - - linear_layer_in = Linear(output_dimension=3, W_init=init_constant, W_init_params={'value':0}, b_init=init_constant, b_init_params={'value':0}, b=False) - linear_layer_in_dim_2 = Linear(output_dimension=3, W_init=init_constant, W_init_params={'value':0}, b_init=init_constant, b_init_params={'value':0}, b=False) - linear_layer_out = Linear(output_dimension=1, W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0}, b=False) - - equation_learner = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in)(x.last()) - equation_learner_out = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in, linear_out=linear_layer_out)(x.last()) - equation_learner_tw = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in)(x.sw(2)) - equation_learner_multi_tw = EquationLearner(functions=[Tan, Sin, Cos], linear_in=linear_layer_in_dim_2)((x.sw(2),F.sw(2))) - - linear_layer_in_2 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1}) - linear_layer_in_2_dim_2 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1}) - - equation_learner_2 = EquationLearner(functions=[Add, Mul, Identity], linear_in=linear_layer_in_2)(x.last()) - equation_learner_2_tw = EquationLearner(functions=[Add, Mul, Identity], linear_in=linear_layer_in_2)(x.sw(2)) - equation_learner_2_multi_tw = EquationLearner(functions=[Add, Mul, Identity], linear_in=linear_layer_in_2_dim_2)((x.sw(2),F.sw(2))) - - linear_layer_in_3 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0}, b=False) - linear_layer_in_3_dim_2 = Linear(output_dimension=5, W_init=init_constant, W_init_params={'value':1}, b_init=init_constant, b_init_params={'value':0}, b=False) - - equation_learner_3 = EquationLearner(functions=[parfun, Add, fuzzi], linear_in=linear_layer_in_3)(x.last()) - equation_learner_3_tw = EquationLearner(functions=[parfun2, Add, fuzzi], linear_in=linear_layer_in_3)(x.sw(2)) - equation_learner_3_multi_tw = EquationLearner(functions=[parfun3, Add, fuzzi], linear_in=linear_layer_in_3_dim_2)((x.sw(2),F.sw(2))) - - out = Output('el',equation_learner) - out2 = Output('el_out',equation_learner_out) - out3 = Output('el_tw',equation_learner_tw) - out4 = Output('el_multi_tw',equation_learner_multi_tw) - out5 = Output('el2',equation_learner_2) - out6 = Output('el2_tw',equation_learner_2_tw) - out7 = Output('el2_multi_tw',equation_learner_2_multi_tw) - out8 = Output('el3',equation_learner_3) - out9 = Output('el3_tw',equation_learner_3_tw) - out10 = Output('el3_multi_tw',equation_learner_3_multi_tw) + x = Input("x") + F = Input("F") + + def myFun(p1, p2, k1, k2): + return k1 * p1 + k2 * p2 + + K1 = Parameter("k1", dimensions=1, sw=1, values=[[2.0]]) + K2 = Parameter("k2", dimensions=1, sw=1, values=[[3.0]]) + parfun = ParamFun(myFun, parameters_and_constants=[K1, K2]) + parfun2 = ParamFun(myFun, parameters_and_constants=[K1, K2]) + parfun3 = ParamFun(myFun, parameters_and_constants=[K1, K2]) + fuzzi = Fuzzify(centers=[0, 1, 2, 3]) + + linear_layer_in = Linear( + output_dimension=3, + W_init=init_constant, + W_init_params={"value": 0}, + b_init=init_constant, + b_init_params={"value": 0}, + b=False, + ) + linear_layer_in_dim_2 = Linear( + output_dimension=3, + W_init=init_constant, + W_init_params={"value": 0}, + b_init=init_constant, + b_init_params={"value": 0}, + b=False, + ) + linear_layer_out = Linear( + output_dimension=1, + W_init=init_constant, + W_init_params={"value": 1}, + b_init=init_constant, + b_init_params={"value": 0}, + b=False, + ) + + equation_learner = EquationLearner( + functions=[Tan, Sin, Cos], linear_in=linear_layer_in + )(x.last()) + equation_learner_out = EquationLearner( + functions=[Tan, Sin, Cos], + linear_in=linear_layer_in, + linear_out=linear_layer_out, + )(x.last()) + equation_learner_tw = EquationLearner( + functions=[Tan, Sin, Cos], linear_in=linear_layer_in + )(x.sw(2)) + equation_learner_multi_tw = EquationLearner( + functions=[Tan, Sin, Cos], linear_in=linear_layer_in_dim_2 + )((x.sw(2), F.sw(2))) + + linear_layer_in_2 = Linear( + output_dimension=5, W_init=init_constant, W_init_params={"value": 1} + ) + linear_layer_in_2_dim_2 = Linear( + output_dimension=5, W_init=init_constant, W_init_params={"value": 1} + ) + + equation_learner_2 = EquationLearner( + functions=[Add, Mul, Identity], linear_in=linear_layer_in_2 + )(x.last()) + equation_learner_2_tw = EquationLearner( + functions=[Add, Mul, Identity], linear_in=linear_layer_in_2 + )(x.sw(2)) + equation_learner_2_multi_tw = EquationLearner( + functions=[Add, Mul, Identity], linear_in=linear_layer_in_2_dim_2 + )((x.sw(2), F.sw(2))) + + linear_layer_in_3 = Linear( + output_dimension=5, + W_init=init_constant, + W_init_params={"value": 1}, + b_init=init_constant, + b_init_params={"value": 0}, + b=False, + ) + linear_layer_in_3_dim_2 = Linear( + output_dimension=5, + W_init=init_constant, + W_init_params={"value": 1}, + b_init=init_constant, + b_init_params={"value": 0}, + b=False, + ) + + equation_learner_3 = EquationLearner( + functions=[parfun, Add, fuzzi], linear_in=linear_layer_in_3 + )(x.last()) + equation_learner_3_tw = EquationLearner( + functions=[parfun2, Add, fuzzi], linear_in=linear_layer_in_3 + )(x.sw(2)) + equation_learner_3_multi_tw = EquationLearner( + functions=[parfun3, Add, fuzzi], linear_in=linear_layer_in_3_dim_2 + )((x.sw(2), F.sw(2))) + + out = Output("el", equation_learner) + out2 = Output("el_out", equation_learner_out) + out3 = Output("el_tw", equation_learner_tw) + out4 = Output("el_multi_tw", equation_learner_multi_tw) + out5 = Output("el2", equation_learner_2) + out6 = Output("el2_tw", equation_learner_2_tw) + out7 = Output("el2_multi_tw", equation_learner_2_multi_tw) + out8 = Output("el3", equation_learner_3) + out9 = Output("el3_tw", equation_learner_3_tw) + out10 = Output("el3_multi_tw", equation_learner_3_multi_tw) example = Modely(visualizer=None) - example.addModel('model',[out,out2,out3,out4,out5,out6,out7,out8,out9,out10]) + example.addModel( + "model", [out, out2, out3, out4, out5, out6, out7, out8, out9, out10] + ) example.neuralizeModel() - result = example({'x':[[1.0],[2.0]], 'F':[[3.0],[4.0]]}) - self.assertEqual(result['el'], [[[0.0, 0.0, 1.0]]]) - self.assertEqual(result['el_out'], [1.0]) - self.assertEqual(result['el_tw'], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]]) - self.assertEqual(result['el_multi_tw'], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]]) - self.assertEqual(result['el2'], [[[4.0, 4.0, 2.0]]]) - self.assertEqual(result['el2_tw'], [[[2.0, 1.0, 1.0], [4.0, 4.0, 2.0]]]) - self.assertEqual(result['el2_multi_tw'], [[[8.0, 16.0, 4.0], [12.0, 36.0, 6.0]]]) - self.assertEqual(result['el3'], [[[10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]]) - self.assertEqual(result['el3_tw'], [[[5.0, 2.0, 0.0, 1.0, 0.0, 0.0], [10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]]) - self.assertEqual(result['el3_multi_tw'], [[[20.0, 8.0, 0.0, 0.0, 0.0, 1.0], [30.0, 12.0, 0.0, 0.0, 0.0, 1.0]]]) + result = example({"x": [[1.0], [2.0]], "F": [[3.0], [4.0]]}) + self.assertEqual(result["el"], [[[0.0, 0.0, 1.0]]]) + self.assertEqual(result["el_out"], [1.0]) + self.assertEqual(result["el_tw"], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]]) + self.assertEqual(result["el_multi_tw"], [[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]]) + self.assertEqual(result["el2"], [[[4.0, 4.0, 2.0]]]) + self.assertEqual(result["el2_tw"], [[[2.0, 1.0, 1.0], [4.0, 4.0, 2.0]]]) + self.assertEqual( + result["el2_multi_tw"], [[[8.0, 16.0, 4.0], [12.0, 36.0, 6.0]]] + ) + self.assertEqual(result["el3"], [[[10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]]) + self.assertEqual( + result["el3_tw"], + [[[5.0, 2.0, 0.0, 1.0, 0.0, 0.0], [10.0, 4.0, 0.0, 0.0, 1.0, 0.0]]], + ) + self.assertEqual( + result["el3_multi_tw"], + [[[20.0, 8.0, 0.0, 0.0, 0.0, 1.0], [30.0, 12.0, 0.0, 0.0, 0.0, 1.0]]], + ) def test_localmodel(self): NeuObj.clearNames() - x = Input('x') - activationA = Fuzzify(2, [0, 1], functions='Triangular')(x.last()) + x = Input("x") + activationA = Fuzzify(2, [0, 1], functions="Triangular")(x.last()) loc = LocalModel(input_function=Fir())(x.tw(1), activationA) - out = Output('out', loc) - example = Modely(visualizer=None,seed=5) - example.addModel('out', out) + out = Output("out", loc) + example = Modely(visualizer=None, seed=5) + example.addModel("out", out) example.neuralizeModel(0.25) # The output is 2 samples - self.assertEqual({'out': [1.7170718908309937, 1.9410502910614014]}, example({'x': [-1, 0, 1, 2, 0]})) - self.assertEqual({'out': [1.7170718908309937, 1.9410502910614014]}, example({'x': [[-1, 0, 1, 2], [0, 1, 2, 0]]}, sampled=True)) + self.assertEqual( + {"out": [1.7170718908309937, 1.9410502910614014]}, + example({"x": [-1, 0, 1, 2, 0]}), + ) + self.assertEqual( + {"out": [1.7170718908309937, 1.9410502910614014]}, + example({"x": [[-1, 0, 1, 2], [0, 1, 2, 0]]}, sampled=True), + ) def test_arithmetic(self): NeuObj.clearNames() - y = Input('y', dimensions=10) + y = Input("y", dimensions=10) - k = Parameter('k', dimensions=1) + k = Parameter("k", dimensions=1) rel1 = y.last() + (5 * y.last()) rel2 = y.last() - (5 * y.last()) rel3 = y.last() + (k * y.last()) @@ -459,32 +684,32 @@ def test_arithmetic(self): rel8 = y.last() / (k * y.last()) rel9 = Sum(y.last()) - out = Output('out', rel1) - out2 = Output('out2', rel2) - out3 = Output('out3', rel3) - out4 = Output('out4', rel4) - out5 = Output('out5', rel5) - out6 = Output('out6', rel6) - out7 = Output('out7', rel7) - out8 = Output('out8', rel8) - out9 = Output('out9', rel9) + out = Output("out", rel1) + out2 = Output("out2", rel2) + out3 = Output("out3", rel3) + out4 = Output("out4", rel4) + out5 = Output("out5", rel5) + out6 = Output("out6", rel6) + out7 = Output("out7", rel7) + out8 = Output("out8", rel8) + out9 = Output("out9", rel9) example = Modely(visualizer=None, seed=42) - example.addModel('out', [out,out2,out3,out4,out5,out6,out7,out8,out9]) + example.addModel("out", [out, out2, out3, out4, out5, out6, out7, out8, out9]) example.neuralizeModel(0.25) - self.assertEqual(rel1.dim['dim'], 10) - self.assertEqual(rel2.dim['dim'], 10) - self.assertEqual(rel3.dim['dim'], 10) - self.assertEqual(rel4.dim['dim'], 10) - self.assertEqual(rel5.dim['dim'], 10) - self.assertEqual(rel6.dim['dim'], 10) - self.assertEqual(rel7.dim['dim'], 10) - self.assertEqual(rel8.dim['dim'], 10) - self.assertEqual(rel9.dim['dim'], 1) + self.assertEqual(rel1.dim["dim"], 10) + self.assertEqual(rel2.dim["dim"], 10) + self.assertEqual(rel3.dim["dim"], 10) + self.assertEqual(rel4.dim["dim"], 10) + self.assertEqual(rel5.dim["dim"], 10) + self.assertEqual(rel6.dim["dim"], 10) + self.assertEqual(rel7.dim["dim"], 10) + self.assertEqual(rel8.dim["dim"], 10) + self.assertEqual(rel9.dim["dim"], 1) def test_rungekutta(self): NeuObj.clearNames() - x = Input('x') + x = Input("x") def fun(x): return x @@ -493,24 +718,24 @@ def fun(x): rk2_rel = RK2(f=fun)(x.last()) rk4_rel = RK4(f=fun)(x.last()) - out_fe = Output('fe', fe_rel) - out_rk2 = Output('rk2', rk2_rel) - out_rk4 = Output('rk4', rk4_rel) + out_fe = Output("fe", fe_rel) + out_rk2 = Output("rk2", rk2_rel) + out_rk4 = Output("rk4", rk4_rel) model = Modely(visualizer=None) - model.addModel('model', [out_fe, out_rk2, out_rk4]) + model.addModel("model", [out_fe, out_rk2, out_rk4]) model.neuralizeModel(1) - inputs = {'x': [[1.0], [2.0]]} + inputs = {"x": [[1.0], [2.0]]} result = model(inputs=inputs) # expected: fe -> [2.0, 4.0], rk2 -> [2.5, 5.0] - self.TestAlmostEqual([2.0, 4.0], result['fe']) - self.TestAlmostEqual([2.5, 5.0], result['rk2']) - self.TestAlmostEqual([2.708333, 5.41666], result['rk4']) + self.TestAlmostEqual([2.0, 4.0], result["fe"]) + self.TestAlmostEqual([2.5, 5.0], result["rk2"]) + self.TestAlmostEqual([2.708333, 5.41666], result["rk4"]) # now test with step h = 0.1 model.neuralizeModel(0.1, clear_model=True) result = model(inputs=inputs) - self.TestAlmostEqual([1.1, 2.2], result['fe']) - self.TestAlmostEqual([1.105, 2.21], result['rk2']) - self.TestAlmostEqual([1.10517, 2.21034], result['rk4']) \ No newline at end of file + self.TestAlmostEqual([1.1, 2.2], result["fe"]) + self.TestAlmostEqual([1.105, 2.21], result["rk2"]) + self.TestAlmostEqual([1.10517, 2.21034], result["rk4"]) diff --git a/tests/test_parameters_of_train.py b/tests/test_parameters_of_train.py index ab73f05b..dd0947d6 100644 --- a/tests/test_parameters_of_train.py +++ b/tests/test_parameters_of_train.py @@ -13,239 +13,324 @@ # 13 Tests # Test the train parameter and the optimizer options -data_folder = os.path.join(os.path.dirname(__file__), 'data/') +data_folder = os.path.join(os.path.dirname(__file__), "data/") + def funIn(x, w): return x * w + def funOut(x, w): return x / w -def linear_fun(x,a,b): - return x*a+b + +def linear_fun(x, a, b): + return x * a + b + class ModelyTrainingTestParameter(unittest.TestCase): def test_network_mass_spring_damper(self): NeuObj.clearNames() - x = Input('x') # Position - F = Input('F') # Force + x = Input("x") # Position + F = Input("F") # Force # List the output of the model - x_z = Output('x_z', Fir(x.tw(0.3)) + Fir(F.last())) + x_z = Output("x_z", Fir(x.tw(0.3)) + Fir(F.last())) # Add the neural model to the nnodely structure and neuralization of the model test = Modely(visualizer=None) - test.addModel('x_z',x_z) - test.addMinimize('next-pos', x.z(-1), x_z, 'mse') + test.addModel("x_z", x_z) + test.addMinimize("next-pos", x.z(-1), x_z, "mse") # Create the neural network - test.neuralizeModel(sample_time=0.05) # The sampling time depends to the dataset + test.neuralizeModel( + sample_time=0.05 + ) # The sampling time depends to the dataset # Data load - data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.trainModel(splits=[80,10,10]) + data_struct = [ + "x", + "F", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.trainModel(splits=[80, 10, 10]) tp = test.getTrainingInfo() - self.assertEqual((15-6), test._num_of_samples['dataset']) - self.assertEqual(round((15-6)*80/100),tp['n_samples_train']) - self.assertEqual(round((15-6)*10/100),tp['n_samples_val']) - self.assertEqual(round((15-6)*10/100),tp['n_samples_test']) - self.assertEqual(round((15-6)*80/100),tp['train_batch_size']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) - self.assertEqual(1,tp['val_batch_size']) - self.assertEqual(100,tp['num_of_epochs']) - self.assertEqual(0.001,tp['optimizer_defaults']['lr']) + self.assertEqual((15 - 6), test._num_of_samples["dataset"]) + self.assertEqual(round((15 - 6) * 80 / 100), tp["n_samples_train"]) + self.assertEqual(round((15 - 6) * 10 / 100), tp["n_samples_val"]) + self.assertEqual(round((15 - 6) * 10 / 100), tp["n_samples_test"]) + self.assertEqual(round((15 - 6) * 80 / 100), tp["train_batch_size"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) + self.assertEqual(1, tp["val_batch_size"]) + self.assertEqual(100, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) def test_build_dataset_batch_connect(self): NeuObj.clearNames() data_x = np.random.rand(500) * 20 - 10 data_a = 2 data_b = -3 - dataset = {'in1': data_x, 'out': linear_fun(data_x, data_a, data_b)} + dataset = {"in1": data_x, "out": linear_fun(data_x, data_a, data_b)} - input1 = Input('in1') - out = Input('out') + input1 = Input("in1") + out = Input("out") rel1 = Fir(input1.tw(0.05)) - y = Output('y', rel1) + y = Output("y", rel1) test = Modely(visualizer=None, seed=42) - test.addModel('y',y) - test.addMinimize('pos', out.next(), y) + test.addModel("y", y) + test.addMinimize("pos", out.next(), y) test.neuralizeModel(0.01) - test.loadData(name='dataset',source=dataset) + test.loadData(name="dataset", source=dataset) training_params = {} - training_params['train_batch_size'] = 4 - training_params['val_batch_size'] = 4 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 5 - test.trainModel(splits=[70,20,10], closed_loop={'in1':'y'}, prediction_samples=5, training_params = training_params) + training_params["train_batch_size"] = 4 + training_params["val_batch_size"] = 4 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 5 + test.trainModel( + splits=[70, 20, 10], + closed_loop={"in1": "y"}, + prediction_samples=5, + training_params=training_params, + ) tp = test.getTrainingInfo() - self.assertEqual(346,tp['n_samples_train']) ## ((500 - 5) * 0.7) = 346 - self.assertEqual(99,tp['n_samples_val']) ## ((500 - 5) * 0.2) = 99 - self.assertEqual(50,tp['n_samples_test']) ## ((500 - 5) * 0.1) = 50 - self.assertEqual(495, test._num_of_samples['dataset']) ## 500 - 5 = 495 - self.assertEqual(4,tp['train_batch_size']) - self.assertEqual(4,tp['val_batch_size']) - self.assertEqual(5,tp['num_of_epochs']) - self.assertEqual(5, tp['prediction_samples']) - self.assertEqual(0, tp['step']) - self.assertEqual({'in1':'y'}, tp['closed_loop']) - self.assertEqual(0.1,tp['optimizer_defaults']['lr']) - self.assertEqual(((494*0.7)-tp['prediction_samples'])//4, tp['update_per_epochs']) - #self.assertEqual(1, tp['unused_samples']) + self.assertEqual(346, tp["n_samples_train"]) ## ((500 - 5) * 0.7) = 346 + self.assertEqual(99, tp["n_samples_val"]) ## ((500 - 5) * 0.2) = 99 + self.assertEqual(50, tp["n_samples_test"]) ## ((500 - 5) * 0.1) = 50 + self.assertEqual(495, test._num_of_samples["dataset"]) ## 500 - 5 = 495 + self.assertEqual(4, tp["train_batch_size"]) + self.assertEqual(4, tp["val_batch_size"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(5, tp["prediction_samples"]) + self.assertEqual(0, tp["step"]) + self.assertEqual({"in1": "y"}, tp["closed_loop"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + self.assertEqual( + ((494 * 0.7) - tp["prediction_samples"]) // 4, tp["update_per_epochs"] + ) + # self.assertEqual(1, tp['unused_samples']) def test_recurrent_train_closed_loop(self): NeuObj.clearNames() data_x = np.random.rand(500) * 20 - 10 data_a = 2 data_b = -3 - dataset = {'in1': data_x, 'out': linear_fun(data_x, data_a, data_b)} + dataset = {"in1": data_x, "out": linear_fun(data_x, data_a, data_b)} - x = Input('in1') - p = Parameter('p', dimensions=1, sw=1, values=[[1.0]]) + x = Input("in1") + p = Parameter("p", dimensions=1, sw=1, values=[[1.0]]) fir = Fir(W=p)(x.last()) - out = Output('out', fir) + out = Output("out", fir) test = Modely(visualizer=None, seed=42) - test.addModel('out',out) - test.addMinimize('pos', x.next(), out) + test.addModel("out", out) + test.addMinimize("pos", x.next(), out) test.neuralizeModel(0.01) - test.loadData(name='dataset',source=dataset) + test.loadData(name="dataset", source=dataset) training_params = {} - training_params['train_batch_size'] = 4 - training_params['val_batch_size'] = 4 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 50 - - test.trainModel(splits=[100,0,0], closed_loop={'in1':'out'}, prediction_samples=3, step=1, training_params = training_params) + training_params["train_batch_size"] = 4 + training_params["val_batch_size"] = 4 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 50 + + test.trainModel( + splits=[100, 0, 0], + closed_loop={"in1": "out"}, + prediction_samples=3, + step=1, + training_params=training_params, + ) tp = test.getTrainingInfo() - self.assertEqual((len(data_x)-1)*100/100,tp['n_samples_train']) ## ((500 - 1) * 1) = 499 - self.assertEqual(0,tp['n_samples_val']) ## ((500 - 5) * 0) = 0 - self.assertEqual(0,tp['n_samples_test']) ## ((500 - 5) * 0) = 0 - self.assertEqual((len(data_x)-1) * 100 / 100, test._num_of_samples['dataset']) - self.assertEqual(4,tp['train_batch_size']) - self.assertEqual(4,tp['val_batch_size']) - self.assertEqual(50,tp['num_of_epochs']) - self.assertEqual(3, tp['prediction_samples']) - self.assertEqual(1, tp['step']) - self.assertEqual({'in1':'out'}, tp['closed_loop']) - self.assertEqual(0.1,tp['optimizer_defaults']['lr']) - - self.assertEqual(99, tp['update_per_epochs']) ## 499 // (4+1) = 99 - #self.assertEqual(100, tp['unused_samples']) ## 99 * step + 1 + self.assertEqual( + (len(data_x) - 1) * 100 / 100, tp["n_samples_train"] + ) ## ((500 - 1) * 1) = 499 + self.assertEqual(0, tp["n_samples_val"]) ## ((500 - 5) * 0) = 0 + self.assertEqual(0, tp["n_samples_test"]) ## ((500 - 5) * 0) = 0 + self.assertEqual((len(data_x) - 1) * 100 / 100, test._num_of_samples["dataset"]) + self.assertEqual(4, tp["train_batch_size"]) + self.assertEqual(4, tp["val_batch_size"]) + self.assertEqual(50, tp["num_of_epochs"]) + self.assertEqual(3, tp["prediction_samples"]) + self.assertEqual(1, tp["step"]) + self.assertEqual({"in1": "out"}, tp["closed_loop"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + + self.assertEqual(99, tp["update_per_epochs"]) ## 499 // (4+1) = 99 + # self.assertEqual(100, tp['unused_samples']) ## 99 * step + 1 def test_recurrent_train_single_close_loop(self): NeuObj.clearNames() data_x = np.array(list(range(1, 101, 1)), dtype=np.float32) - dataset = {'x': data_x, 'y': 2 * data_x} + dataset = {"x": data_x, "y": 2 * data_x} - x = Input('x') - y = Input('y') - out = Output('out', Fir(x.last())) + x = Input("x") + y = Input("y") + out = Output("out", Fir(x.last())) test = Modely(visualizer=None, seed=42) - test.addModel('out', out) - test.addMinimize('pos', y.last(), out) + test.addModel("out", out) + test.addMinimize("pos", y.last(), out) test.neuralizeModel(0.01) - test.loadData(name='dataset', source=dataset) + test.loadData(name="dataset", source=dataset) training_params = {} - training_params['train_batch_size'] = 4 - training_params['val_batch_size'] = 4 - training_params['lr'] = 0.01 - training_params['num_of_epochs'] = 50 - test.trainModel(splits=[80, 20, 0], closed_loop={'x': 'out'}, prediction_samples=3, step=2, training_params=training_params) + training_params["train_batch_size"] = 4 + training_params["val_batch_size"] = 4 + training_params["lr"] = 0.01 + training_params["num_of_epochs"] = 50 + test.trainModel( + splits=[80, 20, 0], + closed_loop={"x": "out"}, + prediction_samples=3, + step=2, + training_params=training_params, + ) tp = test.getTrainingInfo() - self.assertEqual(round((len(data_x) - 0) * 80 / 100), tp['n_samples_train']) - self.assertEqual((len(data_x) - 0) * 20 / 100, tp['n_samples_val']) - self.assertEqual(0, tp['n_samples_test']) - self.assertEqual((len(data_x) - 0) * 100 / 100, test._num_of_samples['dataset']) - self.assertEqual(4, tp['train_batch_size']) - self.assertEqual(4, tp['val_batch_size']) - self.assertEqual(50, tp['num_of_epochs']) - self.assertEqual(3, tp['prediction_samples']) - self.assertEqual(2, tp['step']) - self.assertEqual({'x': 'out'}, tp['closed_loop']) - self.assertEqual(0.01, tp['optimizer_defaults']['lr']) - self.assertEqual(((100*0.8)-3)//(4+2), tp['update_per_epochs']) - #self.assertEqual(100*0.8 - (tp['update_per_epochs']*4) - 3, tp['unused_samples']) + self.assertEqual(round((len(data_x) - 0) * 80 / 100), tp["n_samples_train"]) + self.assertEqual((len(data_x) - 0) * 20 / 100, tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual((len(data_x) - 0) * 100 / 100, test._num_of_samples["dataset"]) + self.assertEqual(4, tp["train_batch_size"]) + self.assertEqual(4, tp["val_batch_size"]) + self.assertEqual(50, tp["num_of_epochs"]) + self.assertEqual(3, tp["prediction_samples"]) + self.assertEqual(2, tp["step"]) + self.assertEqual({"x": "out"}, tp["closed_loop"]) + self.assertEqual(0.01, tp["optimizer_defaults"]["lr"]) + self.assertEqual(((100 * 0.8) - 3) // (4 + 2), tp["update_per_epochs"]) + # self.assertEqual(100*0.8 - (tp['update_per_epochs']*4) - 3, tp['unused_samples']) def test_recurrent_train_multiple_close_loop(self): NeuObj.clearNames() data_x = np.array(list(range(1, 101, 1)), dtype=np.float32) - dataset = {'x': data_x, 'y': 2 * data_x} + dataset = {"x": data_x, "y": 2 * data_x} - x = Input('x') - y = Input('y') - out_x = Output('out_x', Fir(x.last())) - out_y = Output('out_y', Fir(y.last())) + x = Input("x") + y = Input("y") + out_x = Output("out_x", Fir(x.last())) + out_y = Output("out_y", Fir(y.last())) test = Modely(visualizer=None, seed=42) - test.addModel('out_x', out_x) - test.addModel('out_y', out_y) - test.addMinimize('pos_x', x.next(), out_x) - test.addMinimize('pos_y', y.next(), out_y) + test.addModel("out_x", out_x) + test.addModel("out_y", out_y) + test.addMinimize("pos_x", x.next(), out_x) + test.addMinimize("pos_y", y.next(), out_y) test.neuralizeModel(0.01) - test.loadData(name='dataset', source=dataset) + test.loadData(name="dataset", source=dataset) training_params = {} - training_params['train_batch_size'] = 4 - training_params['val_batch_size'] = 4 - training_params['lr'] = 0.01 - training_params['num_of_epochs'] = 32 - - test.trainModel(splits=[80, 20, 0], closed_loop={'x': 'out_x', 'y': 'out_y'}, prediction_samples=3, - training_params=training_params) + training_params["train_batch_size"] = 4 + training_params["val_batch_size"] = 4 + training_params["lr"] = 0.01 + training_params["num_of_epochs"] = 32 + + test.trainModel( + splits=[80, 20, 0], + closed_loop={"x": "out_x", "y": "out_y"}, + prediction_samples=3, + training_params=training_params, + ) tp = test.getTrainingInfo() - self.assertEqual(round((len(data_x) - 1) * 80 / 100), tp['n_samples_train']) - self.assertEqual(round((len(data_x) - 1) * 20 / 100), tp['n_samples_val']) - self.assertEqual(0, tp['n_samples_test']) - self.assertEqual((len(data_x) - 1) * 100 / 100, test._num_of_samples['dataset']) - self.assertEqual(4, tp['train_batch_size']) - self.assertEqual(4, tp['val_batch_size']) - self.assertEqual(32, tp['num_of_epochs']) - self.assertEqual(3, tp['prediction_samples']) - self.assertEqual(0, tp['step']) - self.assertEqual({'x': 'out_x', 'y': 'out_y'}, tp['closed_loop']) - self.assertEqual(0.01, tp['optimizer_defaults']['lr']) - self.assertEqual(19, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(round((len(data_x) - 1) * 80 / 100), tp["n_samples_train"]) + self.assertEqual(round((len(data_x) - 1) * 20 / 100), tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual((len(data_x) - 1) * 100 / 100, test._num_of_samples["dataset"]) + self.assertEqual(4, tp["train_batch_size"]) + self.assertEqual(4, tp["val_batch_size"]) + self.assertEqual(32, tp["num_of_epochs"]) + self.assertEqual(3, tp["prediction_samples"]) + self.assertEqual(0, tp["step"]) + self.assertEqual({"x": "out_x", "y": "out_y"}, tp["closed_loop"]) + self.assertEqual(0.01, tp["optimizer_defaults"]["lr"]) + self.assertEqual(19, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) def test_build_dataset_batch(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') - rel1 = Output('out1',Fir(input1.tw(0.05))) + input1 = Input("in1") + output = Input("out") + rel1 = Output("out1", Fir(input1.tw(0.05))) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1) + test.addMinimize("out", output.z(-1), rel1) test.neuralizeModel(0.01) - data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10,5,1), test._data['dataset']['in1'].shape) + data_struct = [ + "x", + "F", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape) training_params = {} - training_params['train_batch_size'] = 1 - training_params['val_batch_size'] = 1 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 5 + training_params["train_batch_size"] = 1 + training_params["val_batch_size"] = 1 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 5 with self.assertRaises(RuntimeError): - test.trainModel(splits=[70,20,10],training_params = training_params) - test.addModel('out',rel1) + test.trainModel(splits=[70, 20, 10], training_params=training_params) + test.addModel("out", rel1) test.neuralizeModel(0.01) - test.trainModel(splits=[70,20,10],training_params = training_params) + test.trainModel(splits=[70, 20, 10], training_params=training_params) tp = test.getTrainingInfo() # 15 lines in the dataset @@ -254,47 +339,76 @@ def test_build_dataset_batch(self): # 10 / 1 * 0.2 = 2 for validation # 10 / 1 * 0.1 = 1 for test - self.assertEqual(7,tp['n_samples_train']) - self.assertEqual(2,tp['n_samples_val']) - self.assertEqual(1,tp['n_samples_test']) - self.assertEqual(10, test._num_of_samples['dataset']) - self.assertEqual(1,tp['train_batch_size']) - self.assertEqual(1,tp['val_batch_size']) - self.assertEqual(5,tp['num_of_epochs']) - self.assertEqual(0.1,tp['optimizer_defaults']['lr']) - - n_samples = tp['n_samples_train'] - batch_size = tp['train_batch_size'] + self.assertEqual(7, tp["n_samples_train"]) + self.assertEqual(2, tp["n_samples_val"]) + self.assertEqual(1, tp["n_samples_test"]) + self.assertEqual(10, test._num_of_samples["dataset"]) + self.assertEqual(1, tp["train_batch_size"]) + self.assertEqual(1, tp["val_batch_size"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + + n_samples = tp["n_samples_train"] + batch_size = tp["train_batch_size"] list_of_batch_indexes = range(0, n_samples - batch_size + 1, batch_size) - self.assertEqual(len(list_of_batch_indexes), tp['update_per_epochs']) - #self.assertEqual(n_samples - list_of_batch_indexes[-1] - batch_size, tp['unused_samples']) + self.assertEqual(len(list_of_batch_indexes), tp["update_per_epochs"]) + # self.assertEqual(n_samples - list_of_batch_indexes[-1] - batch_size, tp['unused_samples']) - test.trainModel(splits=[70, 20, 10], training_params=training_params, num_of_epochs=100) + test.trainModel( + splits=[70, 20, 10], training_params=training_params, num_of_epochs=100 + ) tp = test.getTrainingInfo() - self.assertEqual(100, tp['num_of_epochs']) + self.assertEqual(100, tp["num_of_epochs"]) def test_build_dataset_batch2(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') - rel1 = Output('out1',Fir(input1.tw(0.05))) + input1 = Input("in1") + output = Input("out") + rel1 = Output("out1", Fir(input1.tw(0.05))) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1) - test.addModel('model', rel1) + test.addMinimize("out", output.z(-1), rel1) + test.addModel("model", rel1) test.neuralizeModel(0.01) - data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset',source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10,5,1), test._data['dataset']['in1'].shape) + data_struct = [ + "x", + "F", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape) training_params = {} - training_params['train_batch_size'] = 25 - training_params['val_batch_size'] = 25 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 5 - test.trainModel(splits=[50,0,50],training_params = training_params) + training_params["train_batch_size"] = 25 + training_params["val_batch_size"] = 25 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 5 + test.trainModel(splits=[50, 0, 50], training_params=training_params) tp = test.getTrainingInfo() # 15 lines in the dataset @@ -303,38 +417,64 @@ def test_build_dataset_batch2(self): # 10 / 1 * 0.5 = 5 for training # 10 / 1 * 0.0 = 0 for validation # 10 / 1 * 0.5 = 5 for test - self.assertEqual((15 - 5), test._num_of_samples['dataset']) - self.assertEqual(round((15 - 5) * 50 / 100), tp['n_samples_train']) - self.assertEqual(round((15 - 5) * 0 / 100), tp['n_samples_val']) - self.assertEqual(round((15 - 5) * 50 / 100), tp['n_samples_test']) - self.assertEqual(round((15 - 5) * 50 / 100), tp['train_batch_size']) - self.assertEqual(25, tp['val_batch_size']) - self.assertEqual(5, tp['num_of_epochs']) - self.assertEqual(0.1, tp['optimizer_defaults']['lr']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual((15 - 5), test._num_of_samples["dataset"]) + self.assertEqual(round((15 - 5) * 50 / 100), tp["n_samples_train"]) + self.assertEqual(round((15 - 5) * 0 / 100), tp["n_samples_val"]) + self.assertEqual(round((15 - 5) * 50 / 100), tp["n_samples_test"]) + self.assertEqual(round((15 - 5) * 50 / 100), tp["train_batch_size"]) + self.assertEqual(25, tp["val_batch_size"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) def test_build_dataset_batch3(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') - rel1 = Output('out1',Fir(input1.tw(0.05))) + input1 = Input("in1") + output = Input("out") + rel1 = Output("out1", Fir(input1.tw(0.05))) - test = Modely(workspace='results', visualizer=None) - test.addMinimize('out', output.next(), rel1) - test.addModel('model', rel1) + test = Modely(workspace="results", visualizer=None) + test.addMinimize("out", output.next(), rel1) + test.addModel("model", rel1) test.neuralizeModel(0.01) - data_struct = ['x', 'F', 'x2', 'y2', '', 'A1x', 'A1y', 'B1x', 'B1y', '', 'A2x', 'A2y', 'B2x', 'out', '', 'x3', - 'in1', 'in2', 'time'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10, 5, 1), test._data['dataset']['in1'].shape) + data_struct = [ + "x", + "F", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape) training_params = {} - training_params['train_batch_size'] = 2 - training_params['val_batch_size'] = 2 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 5 + training_params["train_batch_size"] = 2 + training_params["val_batch_size"] = 2 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 5 test.trainModel(splits=[40, 30, 30], training_params=training_params) tp = test.getTrainingInfo() # 15 lines in the dataset @@ -345,38 +485,64 @@ def test_build_dataset_batch3(self): # 10 * 0.4 = 2 for training # 10 * 0.3 = 1 for validation # 10 * 0.3 = 1 for test - self.assertEqual((15 - 5), test._num_of_samples['dataset']) - self.assertEqual(round((15 - 5) * 40 / 100), tp['n_samples_train']) - self.assertEqual(round((15 - 5) * 30 / 100), tp['n_samples_val']) - self.assertEqual(round((15 - 5) * 30 / 100), tp['n_samples_test']) - self.assertEqual(2, tp['train_batch_size']) - self.assertEqual(2, tp['val_batch_size']) - self.assertEqual(5, tp['num_of_epochs']) - self.assertEqual(0.1, tp['optimizer_defaults']['lr']) - self.assertEqual(2, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual((15 - 5), test._num_of_samples["dataset"]) + self.assertEqual(round((15 - 5) * 40 / 100), tp["n_samples_train"]) + self.assertEqual(round((15 - 5) * 30 / 100), tp["n_samples_val"]) + self.assertEqual(round((15 - 5) * 30 / 100), tp["n_samples_test"]) + self.assertEqual(2, tp["train_batch_size"]) + self.assertEqual(2, tp["val_batch_size"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + self.assertEqual(2, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) def test_build_dataset_batch4(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') - rel1 = Output('out1',Fir(input1.tw(0.05))) + input1 = Input("in1") + output = Input("out") + rel1 = Output("out1", Fir(input1.tw(0.05))) test = Modely(visualizer=None) - test.addMinimize('out', output.z(-1), rel1) - test.addModel('model', rel1) + test.addMinimize("out", output.z(-1), rel1) + test.addModel("model", rel1) test.neuralizeModel(0.01) - data_struct = ['x', 'F', 'x2', 'y2', '', 'A1x', 'A1y', 'B1x', 'B1y', '', 'A2x', 'A2y', 'B2x', 'out', '', 'x3', - 'in1', 'in2', 'time'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - self.assertEqual((10, 5, 1), test._data['dataset']['in1'].shape) + data_struct = [ + "x", + "F", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + self.assertEqual((10, 5, 1), test._data["dataset"]["in1"].shape) training_params = {} - training_params['train_batch_size'] = 2 - training_params['val_batch_size'] = 2 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 5 + training_params["train_batch_size"] = 2 + training_params["val_batch_size"] = 2 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 5 test.trainModel(splits=[80, 10, 10], training_params=training_params) tp = test.getTrainingInfo() @@ -388,42 +554,44 @@ def test_build_dataset_batch4(self): # 10 * 0.8 = 8 for training # 10 * 0.1 = 1 for validation # 10 * 0.1 = 1 for test - self.assertEqual((15 - 5), test._num_of_samples['dataset']) - self.assertEqual(round((15 - 5) * 80 / 100), tp['n_samples_train']) - self.assertEqual(round((15 - 5) * 10 / 100), tp['n_samples_val']) - self.assertEqual(round((15 - 5) * 10 / 100), tp['n_samples_test']) - self.assertEqual(2, tp['train_batch_size']) - self.assertEqual(1, tp['val_batch_size']) - self.assertEqual(5, tp['num_of_epochs']) - self.assertEqual(0.1, tp['optimizer_defaults']['lr']) - self.assertEqual(4, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual((15 - 5), test._num_of_samples["dataset"]) + self.assertEqual(round((15 - 5) * 80 / 100), tp["n_samples_train"]) + self.assertEqual(round((15 - 5) * 10 / 100), tp["n_samples_val"]) + self.assertEqual(round((15 - 5) * 10 / 100), tp["n_samples_test"]) + self.assertEqual(2, tp["train_batch_size"]) + self.assertEqual(1, tp["val_batch_size"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + self.assertEqual(4, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) def test_build_dataset_from_code(self): NeuObj.clearNames() - input1 = Input('in1') - output = Input('out') - rel1 = Output('out1',Fir(input1.tw(0.05))) + input1 = Input("in1") + output = Input("out") + rel1 = Output("out1", Fir(input1.tw(0.05))) test = Modely(visualizer=None) - test.addMinimize('out', output.next(), rel1) - test.addModel('model', rel1) + test.addMinimize("out", output.next(), rel1) + test.addModel("model", rel1) test.neuralizeModel(0.01) x_size = 20 data_x = np.random.rand(x_size) * 20 - 10 data_a = 2 data_b = -3 - dataset = {'in1': data_x, 'out': data_x * data_a + data_b} + dataset = {"in1": data_x, "out": data_x * data_a + data_b} - test.loadData(name='dataset', source=dataset, skiplines=0) - self.assertEqual((15, 5, 1), test._data['dataset']['in1'].shape) ## 20 data - 5 tw = 15 sample | 0.05/0.01 = 5 in1 + test.loadData(name="dataset", source=dataset, skiplines=0) + self.assertEqual( + (15, 5, 1), test._data["dataset"]["in1"].shape + ) ## 20 data - 5 tw = 15 sample | 0.05/0.01 = 5 in1 training_params = {} - training_params['train_batch_size'] = 2 - training_params['val_batch_size'] = 2 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 5 + training_params["train_batch_size"] = 2 + training_params["val_batch_size"] = 2 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 5 test.trainModel(splits=[80, 20, 0], training_params=training_params) tp = test.getTrainingInfo() @@ -435,146 +603,200 @@ def test_build_dataset_from_code(self): # 15 * 0.8 = 12 for training # 15 * 0.2 = 3 for validation # 15 * 0.0 = 0 for test - self.assertEqual((20 - 5), test._num_of_samples['dataset']) - self.assertEqual(round((20 - 5) * 80 / 100), tp['n_samples_train']) - self.assertEqual(round((20 - 5) * 20 / 100), tp['n_samples_val']) - self.assertEqual(round((20 - 5) * 0 / 100), tp['n_samples_test']) - self.assertEqual(2, tp['train_batch_size']) - self.assertEqual(2, tp['val_batch_size']) - self.assertEqual(5, tp['num_of_epochs']) - self.assertEqual(0.1, tp['optimizer_defaults']['lr']) - self.assertEqual(6, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual((20 - 5), test._num_of_samples["dataset"]) + self.assertEqual(round((20 - 5) * 80 / 100), tp["n_samples_train"]) + self.assertEqual(round((20 - 5) * 20 / 100), tp["n_samples_val"]) + self.assertEqual(round((20 - 5) * 0 / 100), tp["n_samples_test"]) + self.assertEqual(2, tp["train_batch_size"]) + self.assertEqual(2, tp["val_batch_size"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + self.assertEqual(6, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) def test_network_multi_dataset(self): NeuObj.clearNames() - train_folder = os.path.join(os.path.dirname(__file__), 'data/') - val_folder = os.path.join(os.path.dirname(__file__), 'val_data/') - test_folder = os.path.join(os.path.dirname(__file__), 'test_data/') + train_folder = os.path.join(os.path.dirname(__file__), "data/") + val_folder = os.path.join(os.path.dirname(__file__), "val_data/") + test_folder = os.path.join(os.path.dirname(__file__), "test_data/") - x = Input('x') # Position - F = Input('F') # Force + x = Input("x") # Position + F = Input("F") # Force # List the output of the model - x_z = Output('x_z', Fir(x.tw(0.3)) + Fir(F.last())) + x_z = Output("x_z", Fir(x.tw(0.3)) + Fir(F.last())) # Add the neural model to the nnodely structure and neuralization of the model test = Modely(visualizer=None) - test.addModel('x_z', x_z) - test.addMinimize('next-pos', x.z(-1), x_z, 'mse') + test.addModel("x_z", x_z) + test.addMinimize("next-pos", x.z(-1), x_z, "mse") # Create the neural network - test.neuralizeModel(sample_time=0.05) # The sampling time depends to the dataset + test.neuralizeModel( + sample_time=0.05 + ) # The sampling time depends to the dataset # Data load - data_struct = ['x', 'F', 'x2', 'y2', '', 'A1x', 'A1y', 'B1x', 'B1y', '', 'A2x', 'A2y', 'B2x', 'out', '', 'x3', - 'in1', 'in2', 'time'] - test.loadData(name='train_dataset', source=train_folder, format=data_struct, skiplines=4, delimiter='\t', - header=None) - test.loadData(name='validation_dataset', source=val_folder, format=data_struct, skiplines=4, delimiter='\t', - header=None) - test.loadData(name='test_dataset', source=test_folder, format=data_struct, skiplines=4, delimiter='\t', - header=None) + data_struct = [ + "x", + "F", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="train_dataset", + source=train_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="validation_dataset", + source=val_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.loadData( + name="test_dataset", + source=test_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) training_params = {} - training_params['train_batch_size'] = 3 - training_params['val_batch_size'] = 2 - training_params['lr'] = 0.1 - training_params['num_of_epochs'] = 5 - #test.trainModel(train_dataset='train_dataset', validation_dataset='validation_dataset', test_dataset='test_dataset', training_params=training_params) - test.trainModel(train_dataset='train_dataset', validation_dataset='validation_dataset', training_params=training_params) + training_params["train_batch_size"] = 3 + training_params["val_batch_size"] = 2 + training_params["lr"] = 0.1 + training_params["num_of_epochs"] = 5 + # test.trainModel(train_dataset='train_dataset', validation_dataset='validation_dataset', test_dataset='test_dataset', training_params=training_params) + test.trainModel( + train_dataset="train_dataset", + validation_dataset="validation_dataset", + training_params=training_params, + ) tp = test.getTrainingInfo() - self.assertEqual(9, test._num_of_samples['train_dataset']) - self.assertEqual(5, test._num_of_samples['validation_dataset']) - #self.assertEqual(7, test._num_of_samples['test_dataset']) - self.assertEqual(9, tp['n_samples_train']) - self.assertEqual(5, tp['n_samples_val']) - self.assertEqual(3, tp['train_batch_size']) - self.assertEqual(2, tp['val_batch_size']) - self.assertEqual(5, tp['num_of_epochs']) - self.assertEqual(0.1, tp['optimizer_defaults']['lr']) - self.assertEqual(3, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(9, test._num_of_samples["train_dataset"]) + self.assertEqual(5, test._num_of_samples["validation_dataset"]) + # self.assertEqual(7, test._num_of_samples['test_dataset']) + self.assertEqual(9, tp["n_samples_train"]) + self.assertEqual(5, tp["n_samples_val"]) + self.assertEqual(3, tp["train_batch_size"]) + self.assertEqual(2, tp["val_batch_size"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + self.assertEqual(3, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) def test_train_vector_input(self): NeuObj.clearNames() - x = Input('x', dimensions=4) - y = Input('y', dimensions=3) - k = Input('k', dimensions=2) - w = Input('w') + x = Input("x", dimensions=4) + y = Input("y", dimensions=3) + k = Input("k", dimensions=2) + w = Input("w") - out = Output('out', Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) - out2 = Output('out2', Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02))))) + out = Output("out", Fir(Linear(Linear(3)(x.tw(0.02)) + y.tw(0.02)))) + out2 = Output("out2", Fir(Linear(k.last() + Fir(2)(w.tw(0.05, offset=-0.02))))) test = Modely(visualizer=None) - test.addMinimize('out', out, out2) - test.addModel('model', out) + test.addMinimize("out", out, out2) + test.addModel("model", out) test.neuralizeModel(0.01) - data_folder = os.path.join(os.path.dirname(__file__), 'vector_data/') - data_struct = ['x', 'y', '', '', '', '', 'k', '', '', '', 'w'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1, delimiter='\t', header=None) + data_folder = os.path.join(os.path.dirname(__file__), "vector_data/") + data_struct = ["x", "y", "", "", "", "", "k", "", "", "", "w"] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=1, + delimiter="\t", + header=None, + ) training_params = {} - training_params['train_batch_size'] = 1 - training_params['val_batch_size'] = 1 - training_params['lr'] = 0.01 - training_params['num_of_epochs'] = 7 + training_params["train_batch_size"] = 1 + training_params["val_batch_size"] = 1 + training_params["lr"] = 0.01 + training_params["num_of_epochs"] = 7 test.trainModel(splits=[80, 10, 10], training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(22, test._num_of_samples['dataset']) - self.assertEqual(18, tp['n_samples_train']) - self.assertEqual(2, tp['n_samples_val']) - self.assertEqual(2, tp['n_samples_test']) - self.assertEqual(1, tp['train_batch_size']) - self.assertEqual(1, tp['val_batch_size']) - self.assertEqual(7, tp['num_of_epochs']) - self.assertEqual(0.01, tp['optimizer_defaults']['lr']) - self.assertEqual(18, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(22, test._num_of_samples["dataset"]) + self.assertEqual(18, tp["n_samples_train"]) + self.assertEqual(2, tp["n_samples_val"]) + self.assertEqual(2, tp["n_samples_test"]) + self.assertEqual(1, tp["train_batch_size"]) + self.assertEqual(1, tp["val_batch_size"]) + self.assertEqual(7, tp["num_of_epochs"]) + self.assertEqual(0.01, tp["optimizer_defaults"]["lr"]) + self.assertEqual(18, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) training_params = {} - training_params['train_batch_size'] = 6 - training_params['val_batch_size'] = 2 + training_params["train_batch_size"] = 6 + training_params["val_batch_size"] = 2 test.trainModel(splits=[80, 10, 10], training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(22, test._num_of_samples['dataset']) - self.assertEqual(18, tp['n_samples_train']) - self.assertEqual(2, tp['n_samples_val']) - self.assertEqual(2, tp['n_samples_test']) - self.assertEqual(6, tp['train_batch_size']) - self.assertEqual(2, tp['val_batch_size']) - self.assertEqual(100, tp['num_of_epochs']) - self.assertEqual(0.001, tp['optimizer_defaults']['lr']) - self.assertEqual(3, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(22, test._num_of_samples["dataset"]) + self.assertEqual(18, tp["n_samples_train"]) + self.assertEqual(2, tp["n_samples_val"]) + self.assertEqual(2, tp["n_samples_test"]) + self.assertEqual(6, tp["train_batch_size"]) + self.assertEqual(2, tp["val_batch_size"]) + self.assertEqual(100, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) + self.assertEqual(3, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) def test_optimizer_configuration(self): NeuObj.clearNames() ## Model1 - input1 = Input('in1') - a = Parameter('a', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) - shared_w = Parameter('w', values=[[5]]) - output1 = Output('out1', - Fir(W=a)(input1.tw(0.05)) + ParamFun(funIn, parameters_and_constants={'w': shared_w})( - input1.last())) + input1 = Input("in1") + a = Parameter("a", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + shared_w = Parameter("w", values=[[5]]) + output1 = Output( + "out1", + Fir(W=a)(input1.tw(0.05)) + + ParamFun(funIn, parameters_and_constants={"w": shared_w})(input1.last()), + ) test = Modely(visualizer=None, seed=42) - test.addModel('model1', output1) - test.addMinimize('error1', input1.last(), output1) + test.addModel("model1", output1) + test.addMinimize("error1", input1.last(), output1) ## Model2 - input2 = Input('in2') - b = Parameter('b', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) - output2 = Output('out2', - Fir(W=b)(input2.tw(0.05)) + ParamFun(funOut, parameters_and_constants={'w': shared_w})( - input2.last())) - - test.addModel('model2', output2) - test.addMinimize('error2', input2.last(), output2) + input2 = Input("in2") + b = Parameter("b", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output2 = Output( + "out2", + Fir(W=b)(input2.tw(0.05)) + + ParamFun(funOut, parameters_and_constants={"w": shared_w})(input2.last()), + ) + + test.addModel("model2", output2) + test.addMinimize("error2", input2.last(), output2) test.neuralizeModel(0.01) # Dataset for train @@ -582,15 +804,25 @@ def test_optimizer_configuration(self): data_in2 = np.linspace(10, 15, 60) data_out1 = 2 data_out2 = -3 - dataset = {'in1': data_in1, 'in2': data_in2, 'out1': data_in1 * data_out1, 'out2': data_in2 * data_out2} - test.loadData(name='dataset1', source=dataset) + dataset = { + "in1": data_in1, + "in2": data_in2, + "out1": data_in1 * data_out1, + "out2": data_in2 * data_out2, + } + test.loadData(name="dataset1", source=dataset) data_in1 = np.linspace(0, 5, 100) data_in2 = np.linspace(10, 15, 100) data_out1 = 2 data_out2 = -3 - dataset = {'in1': data_in1, 'in2': data_in2, 'out1': data_in1 * data_out1, 'out2': data_in2 * data_out2} - test.loadData(name='dataset2', source=dataset) + dataset = { + "in1": data_in1, + "in2": data_in2, + "out1": data_in1 * data_out1, + "out2": data_in2 * data_out2, + } + test.loadData(name="dataset2", source=dataset) # Optimizer # Basic usage @@ -598,83 +830,93 @@ def test_optimizer_configuration(self): # We train all the models with split [100,0,0], lr =0.01 and epochs = 100 test.trainModel() tp = test.getTrainingInfo() - self.assertEqual(['model1', 'model2'], tp['models']) - self.assertEqual(152, tp['n_samples_train']) - self.assertEqual(0, tp['n_samples_val']) - self.assertEqual(0, tp['n_samples_test']) - self.assertEqual(100, tp['num_of_epochs']) - self.assertEqual(0.001, tp['optimizer_defaults']['lr']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual(["model1", "model2"], tp["models"]) + self.assertEqual(152, tp["n_samples_train"]) + self.assertEqual(0, tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual(100, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) # We train only model1 with split [100,0,0] # TODO Learning rate automoatically optimized based on the mean and variance of the output # TODO num_of_epochs automatically defined # now is 0.001 for learning rate and 100 for the epochs and optimizer Adam - test.trainModel(models='model1', splits=[100, 0, 0]) + test.trainModel(models="model1", splits=[100, 0, 0]) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual(100, tp['num_of_epochs']) - self.assertEqual(152, tp['n_samples_train']) - self.assertEqual(0, tp['n_samples_val']) - self.assertEqual(0, tp['n_samples_test']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual(100, tp["num_of_epochs"]) + self.assertEqual(152, tp["n_samples_train"]) + self.assertEqual(0, tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) # Set number of epoch and learning rate via parameters it works only for standard parameters - test.trainModel(models='model1', splits=[100, 0, 0], lr=0.5, num_of_epochs=5) + test.trainModel(models="model1", splits=[100, 0, 0], lr=0.5, num_of_epochs=5) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual(5, tp['num_of_epochs']) - self.assertEqual(152, tp['n_samples_train']) - self.assertEqual(0, tp['n_samples_val']) - self.assertEqual(0, tp['n_samples_test']) - self.assertEqual(0.5, tp['optimizer_defaults']['lr']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual(5, tp["num_of_epochs"]) + self.assertEqual(152, tp["n_samples_train"]) + self.assertEqual(0, tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual(0.5, tp["optimizer_defaults"]["lr"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) # Set number of epoch and learning rate via parameters it works only for standard parameters and use two different dataset one for train and one for validation - test.trainModel(models='model1', train_dataset='dataset1', validation_dataset='dataset2', lr=0.6, num_of_epochs=10) + test.trainModel( + models="model1", + train_dataset="dataset1", + validation_dataset="dataset2", + lr=0.6, + num_of_epochs=10, + ) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual(10, tp['num_of_epochs']) - self.assertEqual(56, tp['n_samples_train']) - self.assertEqual(96, tp['n_samples_val']) - self.assertEqual(0, tp['n_samples_test']) - self.assertEqual(0.6,tp['optimizer_defaults']['lr']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual(10, tp["num_of_epochs"]) + self.assertEqual(56, tp["n_samples_train"]) + self.assertEqual(96, tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual(0.6, tp["optimizer_defaults"]["lr"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) # Use dictionary for set number of epoch, learning rate, etc.. This configuration works only standard parameters (all the parameters that are input of the trainModel). training_params = { - 'models': ['model2'], - 'splits': [55, 40, 5], - 'num_of_epochs': 20, - 'lr': 0.7 + "models": ["model2"], + "splits": [55, 40, 5], + "num_of_epochs": 20, + "lr": 0.7, } test.trainModel(training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(['model2'], tp['models']) - self.assertEqual(20, tp['num_of_epochs']) - self.assertEqual(round(152 * 55 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 40 / 100), tp['n_samples_val']) - self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test']) - self.assertEqual(0.7, tp['optimizer_defaults']['lr']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(["model2"], tp["models"]) + self.assertEqual(20, tp["num_of_epochs"]) + self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"]) + self.assertEqual( + 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"] + ) + self.assertEqual(0.7, tp["optimizer_defaults"]["lr"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) # If I add a function parameter it has the priority # In this case apply train parameter but on a different model - test.trainModel(models='model1', training_params=training_params) + test.trainModel(models="model1", training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual(20, tp['num_of_epochs']) - self.assertEqual(round(152 * 55 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 40 / 100), tp['n_samples_val']) - self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test']) - self.assertEqual(0.7, tp['optimizer_defaults']['lr']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual(20, tp["num_of_epochs"]) + self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"]) + self.assertEqual( + 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"] + ) + self.assertEqual(0.7, tp["optimizer_defaults"]["lr"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) ################################## # Modify additional parameters in the optimizer that are not present in the standard parameter @@ -683,85 +925,102 @@ def test_optimizer_configuration(self): # max priority to the function parameter ('lr' : 0.2) # then the standard_optimizer_parameters ('lr' : 0.1) # finally the standard_train_parameters ('lr' : 0.5) - optimizer_defaults = { - 'lr': 0.1, - 'betas': (0.5, 0.99) - } - test.trainModel(training_params=training_params, optimizer_defaults=optimizer_defaults, lr=0.2) + optimizer_defaults = {"lr": 0.1, "betas": (0.5, 0.99)} + test.trainModel( + training_params=training_params, + optimizer_defaults=optimizer_defaults, + lr=0.2, + ) tp = test.getTrainingInfo() - self.assertEqual(['model2'], tp['models']) - self.assertEqual(20, tp['num_of_epochs']) - self.assertEqual(round(152 * 55 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 40 / 100), tp['n_samples_val']) - self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test']) - self.assertEqual(0.2, tp['optimizer_defaults']['lr']) - self.assertEqual((0.5, 0.99), tp['optimizer_defaults']['betas']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) - - test.trainModel(training_params=training_params, optimizer_defaults=optimizer_defaults) + self.assertEqual(["model2"], tp["models"]) + self.assertEqual(20, tp["num_of_epochs"]) + self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"]) + self.assertEqual( + 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"] + ) + self.assertEqual(0.2, tp["optimizer_defaults"]["lr"]) + self.assertEqual((0.5, 0.99), tp["optimizer_defaults"]["betas"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) + + test.trainModel( + training_params=training_params, optimizer_defaults=optimizer_defaults + ) tp = test.getTrainingInfo() - self.assertEqual(['model2'], tp['models']) - self.assertEqual(20, tp['num_of_epochs']) - self.assertEqual(round(152 * 55 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 40 / 100), tp['n_samples_val']) - self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test']) - self.assertEqual(0.1, tp['optimizer_defaults']['lr']) - self.assertEqual((0.5, 0.99), tp['optimizer_defaults']['betas']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(["model2"], tp["models"]) + self.assertEqual(20, tp["num_of_epochs"]) + self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"]) + self.assertEqual( + 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"] + ) + self.assertEqual(0.1, tp["optimizer_defaults"]["lr"]) + self.assertEqual((0.5, 0.99), tp["optimizer_defaults"]["betas"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) test.trainModel(training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(['model2'], tp['models']) - self.assertEqual(20, tp['num_of_epochs']) - self.assertEqual(round(152 * 55 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 40 / 100), tp['n_samples_val']) - self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test']) - self.assertEqual(0.7, tp['optimizer_defaults']['lr']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(["model2"], tp["models"]) + self.assertEqual(20, tp["num_of_epochs"]) + self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"]) + self.assertEqual( + 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"] + ) + self.assertEqual(0.7, tp["optimizer_defaults"]["lr"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) ################################## # Modify the non standard args of the optimizer using the optimizer_defaults # In this case use the SGD with 0.2 of momentum - optimizer_defaults = { - 'momentum': 0.002 - } - test.trainModel(optimizer='SGD', training_params=training_params, optimizer_defaults=optimizer_defaults, lr=0.2) + optimizer_defaults = {"momentum": 0.002} + test.trainModel( + optimizer="SGD", + training_params=training_params, + optimizer_defaults=optimizer_defaults, + lr=0.2, + ) tp = test.getTrainingInfo() - self.assertEqual(['model2'], tp['models']) - self.assertEqual('SGD', tp['optimizer']) - self.assertEqual(20, tp['num_of_epochs']) - self.assertEqual(round(152 * 55 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 40 / 100), tp['n_samples_val']) - self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test']) - self.assertEqual(0.2, tp['optimizer_defaults']['lr']) - self.assertEqual(0.002, tp['optimizer_defaults']['momentum']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) + self.assertEqual(["model2"], tp["models"]) + self.assertEqual("SGD", tp["optimizer"]) + self.assertEqual(20, tp["num_of_epochs"]) + self.assertEqual(round(152 * 55 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 40 / 100), tp["n_samples_val"]) + self.assertEqual( + 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"] + ) + self.assertEqual(0.2, tp["optimizer_defaults"]["lr"]) + self.assertEqual(0.002, tp["optimizer_defaults"]["momentum"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) # Modify standard optimizer parameter for each training parameter training_params = { - 'models': ['model1'], - 'splits': [100, 0, 0], - 'num_of_epochs': 30, - 'lr': 0.5, - 'lr_param': {'a': 0.1} + "models": ["model1"], + "splits": [100, 0, 0], + "num_of_epochs": 30, + "lr": 0.5, + "lr_param": {"a": 0.1}, } test.trainModel(training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual(30, tp['num_of_epochs']) - self.assertEqual(round(152 * 100 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_val']) - self.assertEqual(152 - tp['n_samples_train'] - tp['n_samples_val'], tp['n_samples_test']) - self.assertEqual(0.5, tp['optimizer_defaults']['lr']) - self.assertEqual([{'lr': 0.1, 'params': 'a'}, - {'lr': 0.0, 'params': 'b'}, - {'params': 'w'}], tp['optimizer_params']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual(30, tp["num_of_epochs"]) + self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"]) + self.assertEqual( + 152 - tp["n_samples_train"] - tp["n_samples_val"], tp["n_samples_test"] + ) + self.assertEqual(0.5, tp["optimizer_defaults"]["lr"]) + self.assertEqual( + [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}], + tp["optimizer_params"], + ) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) ################################## # Modify standard optimizer parameter for each training parameter using optimizer_params @@ -771,71 +1030,77 @@ def test_optimizer_configuration(self): # then the optimizer_params inside the train_parameters ( {'params':['a'],'lr':0.7} ) # finally the train_parameters ( 'lr_param'={'a': 0.1}) training_params = { - 'models': ['model1'], - 'splits': [100, 0, 0], - 'num_of_epochs': 40, - 'lr': 0.5, - 'lr_param': {'a': 0.1}, - 'optimizer_params': [{'params': ['a'], 'lr': 0.7}], - 'optimizer_defaults': {'lr': 0.12} - } - optimizer_params = [ - {'params': ['a'], 'lr': 0.6} - ] - optimizer_defaults = { - 'lr': 0.2 + "models": ["model1"], + "splits": [100, 0, 0], + "num_of_epochs": 40, + "lr": 0.5, + "lr_param": {"a": 0.1}, + "optimizer_params": [{"params": ["a"], "lr": 0.7}], + "optimizer_defaults": {"lr": 0.12}, } - test.trainModel(training_params=training_params, optimizer_params=optimizer_params, - optimizer_defaults=optimizer_defaults, lr_param={'a': 0.4}) + optimizer_params = [{"params": ["a"], "lr": 0.6}] + optimizer_defaults = {"lr": 0.2} + test.trainModel( + training_params=training_params, + optimizer_params=optimizer_params, + optimizer_defaults=optimizer_defaults, + lr_param={"a": 0.4}, + ) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual(40, tp['num_of_epochs']) - self.assertEqual(round(152 * 100 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_val']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_test']) - self.assertEqual(0.2, tp['optimizer_defaults']['lr']) - self.assertEqual([{'lr': 0.4, 'params': 'a'}], tp['optimizer_params']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) - - test.trainModel(training_params=training_params, optimizer_params=optimizer_params, - optimizer_defaults=optimizer_defaults) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual(40, tp["num_of_epochs"]) + self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"]) + self.assertEqual(0.2, tp["optimizer_defaults"]["lr"]) + self.assertEqual([{"lr": 0.4, "params": "a"}], tp["optimizer_params"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) + + test.trainModel( + training_params=training_params, + optimizer_params=optimizer_params, + optimizer_defaults=optimizer_defaults, + ) tp = test.getTrainingInfo() - self.assertEqual(0.2, tp['optimizer_defaults']['lr']) - self.assertEqual([{'lr': 0.6, 'params': 'a'}], tp['optimizer_params']) + self.assertEqual(0.2, tp["optimizer_defaults"]["lr"]) + self.assertEqual([{"lr": 0.6, "params": "a"}], tp["optimizer_params"]) - test.trainModel(training_params=training_params, optimizer_params=optimizer_params) + test.trainModel( + training_params=training_params, optimizer_params=optimizer_params + ) tp = test.getTrainingInfo() - self.assertEqual(0.12, tp['optimizer_defaults']['lr']) - self.assertEqual([{'lr': 0.6, 'params': 'a'}], tp['optimizer_params']) + self.assertEqual(0.12, tp["optimizer_defaults"]["lr"]) + self.assertEqual([{"lr": 0.6, "params": "a"}], tp["optimizer_params"]) test.trainModel(training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(0.12, tp['optimizer_defaults']['lr']) - self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params']) + self.assertEqual(0.12, tp["optimizer_defaults"]["lr"]) + self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"]) - del training_params['optimizer_defaults'] + del training_params["optimizer_defaults"] test.trainModel(training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(0.5, tp['optimizer_defaults']['lr']) - self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params']) + self.assertEqual(0.5, tp["optimizer_defaults"]["lr"]) + self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"]) - del training_params['optimizer_params'] + del training_params["optimizer_params"] test.trainModel(training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual(0.5, tp['optimizer_defaults']['lr']) - self.assertEqual([{'lr': 0.1, 'params': 'a'}, - {'lr': 0.0, 'params': 'b'}, - {'params': 'w'}], tp['optimizer_params']) + self.assertEqual(0.5, tp["optimizer_defaults"]["lr"]) + self.assertEqual( + [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}], + tp["optimizer_params"], + ) test.trainModel() tp = test.getTrainingInfo() - self.assertEqual(0.001, tp['optimizer_defaults']['lr']) - self.assertEqual([{'params': 'a'}, - {'params': 'b'}, - {'params': 'w'}], tp['optimizer_params']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) + self.assertEqual( + [{"params": "a"}, {"params": "b"}, {"params": "w"}], tp["optimizer_params"] + ) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) ################################## @@ -848,65 +1113,82 @@ def test_optimizer_configuration(self): # finally the train_parameters ('lr'= 0.5) class RMSprop(Optimizer): def __init__(self, optimizer_defaults={}, optimizer_params=[]): - super(RMSprop, self).__init__('RMSprop', optimizer_defaults, optimizer_params) + super(RMSprop, self).__init__( + "RMSprop", optimizer_defaults, optimizer_params + ) def get_torch_optimizer(self): import torch - return torch.optim.RMSprop(self.replace_key_with_params(), **self.optimizer_defaults) + + return torch.optim.RMSprop( + self.replace_key_with_params(), **self.optimizer_defaults + ) training_params = { - 'models': ['model1'], - 'splits': [100, 0, 0], - 'num_of_epochs': 40, - 'lr': 0.5, - 'lr_param': {'a': 0.1}, - 'optimizer_params': [{'params': ['a'], 'lr': 0.7}], - 'optimizer_defaults': {'lr': 0.12} - } - optimizer_defaults = { - 'alpha': 0.8 + "models": ["model1"], + "splits": [100, 0, 0], + "num_of_epochs": 40, + "lr": 0.5, + "lr_param": {"a": 0.1}, + "optimizer_params": [{"params": ["a"], "lr": 0.7}], + "optimizer_defaults": {"lr": 0.12}, } + optimizer_defaults = {"alpha": 0.8} optimizer = RMSprop(optimizer_defaults) - test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3}, lr=0.4) + test.trainModel( + optimizer=optimizer, + training_params=training_params, + optimizer_defaults={"lr": 0.3}, + lr=0.4, + ) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual('RMSprop', tp['optimizer']) - self.assertEqual(40, tp['num_of_epochs']) - self.assertEqual(round(152 * 100 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_val']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_test']) - self.assertEqual({'lr': 0.4}, tp['optimizer_defaults']) - self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params']) - - test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.1}) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual("RMSprop", tp["optimizer"]) + self.assertEqual(40, tp["num_of_epochs"]) + self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"]) + self.assertEqual({"lr": 0.4}, tp["optimizer_defaults"]) + self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"]) + + test.trainModel( + optimizer=optimizer, + training_params=training_params, + optimizer_defaults={"lr": 0.1}, + ) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.1}, tp['optimizer_defaults']) - self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params']) + self.assertEqual({"lr": 0.1}, tp["optimizer_defaults"]) + self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"]) test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.12}, tp['optimizer_defaults']) - self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params']) + self.assertEqual({"lr": 0.12}, tp["optimizer_defaults"]) + self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"]) - del training_params['optimizer_defaults'] + del training_params["optimizer_defaults"] test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults']) - self.assertEqual([{'lr': 0.7, 'params': 'a'}], tp['optimizer_params']) + self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"]) + self.assertEqual([{"lr": 0.7, "params": "a"}], tp["optimizer_params"]) - del training_params['optimizer_params'] + del training_params["optimizer_params"] test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults']) - self.assertEqual([{'lr': 0.1, 'params': 'a'}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"]) + self.assertEqual( + [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}], + tp["optimizer_params"], + ) test.trainModel(optimizer=optimizer) tp = test.getTrainingInfo() - self.assertEqual({'alpha': 0.8, 'lr': 0.001}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a'}, {'params': 'b'}, {'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"alpha": 0.8, "lr": 0.001}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a"}, {"params": "b"}, {"params": "w"}], tp["optimizer_params"] + ) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) ################################## ################################## @@ -918,69 +1200,97 @@ def get_torch_optimizer(self): # then the train_parameters ( 'lr_param'={'a': 0.1} ) # finnaly the optimizer_paramsat the time of the optimizer initialization [{'params':['a'],'lr':0.6}] training_params = { - 'models': ['model1'], - 'splits': [100, 0, 0], - 'num_of_epochs': 40, - 'lr': 0.5, - 'lr_param': {'a': 0.1}, - 'optimizer_params': [{'params': ['a'], 'lr': 0.7}], - 'optimizer_defaults': {'lr': 0.12} - } - optimizer_defaults = { - 'alpha': 0.8 + "models": ["model1"], + "splits": [100, 0, 0], + "num_of_epochs": 40, + "lr": 0.5, + "lr_param": {"a": 0.1}, + "optimizer_params": [{"params": ["a"], "lr": 0.7}], + "optimizer_defaults": {"lr": 0.12}, } + optimizer_defaults = {"alpha": 0.8} optimizer_params = [ - {'params': ['a'], 'lr': 0.6}, {'params': 'w', 'lr': 0.12, 'alpha': 0.02} + {"params": ["a"], "lr": 0.6}, + {"params": "w", "lr": 0.12, "alpha": 0.02}, ] optimizer = RMSprop(optimizer_defaults, optimizer_params) - test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3}, - optimizer_params=[{'params': ['a'], 'lr': 1.0}, {'params': ['b'], 'lr': 1.2}], - lr_param={'a': 0.2}) + test.trainModel( + optimizer=optimizer, + training_params=training_params, + optimizer_defaults={"lr": 0.3}, + optimizer_params=[ + {"params": ["a"], "lr": 1.0}, + {"params": ["b"], "lr": 1.2}, + ], + lr_param={"a": 0.2}, + ) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual('RMSprop', tp['optimizer']) - self.assertEqual(40, tp['num_of_epochs']) - self.assertEqual(round(152 * 100 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_val']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_test']) - self.assertEqual({'lr': 0.3}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.2}, {'params': 'b', 'lr': 1.2}], tp['optimizer_params']) - - test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3}, - optimizer_params=[{'params': ['a'], 'lr': 0.1}, {'params': ['b'], 'lr': 0.2}]) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual("RMSprop", tp["optimizer"]) + self.assertEqual(40, tp["num_of_epochs"]) + self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"]) + self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.2}, {"params": "b", "lr": 1.2}], + tp["optimizer_params"], + ) + + test.trainModel( + optimizer=optimizer, + training_params=training_params, + optimizer_defaults={"lr": 0.3}, + optimizer_params=[ + {"params": ["a"], "lr": 0.1}, + {"params": ["b"], "lr": 0.2}, + ], + ) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.3}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.1}, {'params': 'b', 'lr': 0.2}], tp['optimizer_params']) - - test.trainModel(optimizer=optimizer, training_params=training_params, optimizer_defaults={'lr': 0.3}) + self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.1}, {"params": "b", "lr": 0.2}], + tp["optimizer_params"], + ) + + test.trainModel( + optimizer=optimizer, + training_params=training_params, + optimizer_defaults={"lr": 0.3}, + ) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.3}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params']) + self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"]) + self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"]) test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.12}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params']) + self.assertEqual({"lr": 0.12}, tp["optimizer_defaults"]) + self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"]) - del training_params['optimizer_defaults'] + del training_params["optimizer_defaults"] test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.7}], tp['optimizer_params']) + self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"]) + self.assertEqual([{"params": "a", "lr": 0.7}], tp["optimizer_params"]) - del training_params['optimizer_params'] + del training_params["optimizer_params"] test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'alpha': 0.8, 'lr': 0.5}, tp['optimizer_defaults']) - self.assertEqual([{'lr': 0.1, 'params': 'a'}, {'alpha': 0.02, 'lr': 0.12, 'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"alpha": 0.8, "lr": 0.5}, tp["optimizer_defaults"]) + self.assertEqual( + [{"lr": 0.1, "params": "a"}, {"alpha": 0.02, "lr": 0.12, "params": "w"}], + tp["optimizer_params"], + ) test.trainModel(optimizer=optimizer) tp = test.getTrainingInfo() - self.assertEqual({'alpha': 0.8, 'lr': 0.001}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.6}, {'params': 'w', 'lr': 0.12, 'alpha': 0.02}], - tp['optimizer_params']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual({"alpha": 0.8, "lr": 0.001}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.6}, {"params": "w", "lr": 0.12, "alpha": 0.02}], + tp["optimizer_params"], + ) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) ################################## ################################## @@ -992,122 +1302,252 @@ def get_torch_optimizer(self): # then the train_parameters ( 'lr_param'={'a': 0.1} ) # The other parameters are the defaults training_params = { - 'models': ['model1'], - 'splits': [100, 0, 0], - 'num_of_epochs': 40, - 'lr': 0.5, - 'lr_param': {'a': 0.1}, - 'add_optimizer_params': [{'params': ['a'], 'lr': 0.7}], - 'add_optimizer_defaults': {'lr': 0.12} + "models": ["model1"], + "splits": [100, 0, 0], + "num_of_epochs": 40, + "lr": 0.5, + "lr_param": {"a": 0.1}, + "add_optimizer_params": [{"params": ["a"], "lr": 0.7}], + "add_optimizer_defaults": {"lr": 0.12}, } optimizer = RMSprop() - test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3}, - add_optimizer_params=[{'params': ['a'], 'lr': 1.0}, {'params': ['b'], 'lr': 1.2}], - lr_param={'a': 0.2}) + test.trainModel( + optimizer=optimizer, + training_params=training_params, + add_optimizer_defaults={"lr": 0.3}, + add_optimizer_params=[ + {"params": ["a"], "lr": 1.0}, + {"params": ["b"], "lr": 1.2}, + ], + lr_param={"a": 0.2}, + ) tp = test.getTrainingInfo() - self.assertEqual(['model1'], tp['models']) - self.assertEqual('RMSprop', tp['optimizer']) - self.assertEqual(40, tp['num_of_epochs']) - self.assertEqual(round(152 * 100 / 100), tp['n_samples_train']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_val']) - self.assertEqual(round(152 * 0 / 100), tp['n_samples_test']) - self.assertEqual({'lr': 0.3}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.2}, {'params': 'b', 'lr': 1.2}, {'params': 'w'}], tp['optimizer_params']) - - test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3}, - add_optimizer_params=[{'params': ['a'], 'lr': 0.23}, {'params': ['b'], 'lr': 0.2}]) + self.assertEqual(["model1"], tp["models"]) + self.assertEqual("RMSprop", tp["optimizer"]) + self.assertEqual(40, tp["num_of_epochs"]) + self.assertEqual(round(152 * 100 / 100), tp["n_samples_train"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_val"]) + self.assertEqual(round(152 * 0 / 100), tp["n_samples_test"]) + self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.2}, {"params": "b", "lr": 1.2}, {"params": "w"}], + tp["optimizer_params"], + ) + + test.trainModel( + optimizer=optimizer, + training_params=training_params, + add_optimizer_defaults={"lr": 0.3}, + add_optimizer_params=[ + {"params": ["a"], "lr": 0.23}, + {"params": ["b"], "lr": 0.2}, + ], + ) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.3}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.23}, {'params': 'b', 'lr': 0.2}, {'params': 'w'}], tp['optimizer_params']) - - test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3}, - add_optimizer_params=[{'params': ['b'], 'lr': 0.2}]) + self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.23}, {"params": "b", "lr": 0.2}, {"params": "w"}], + tp["optimizer_params"], + ) + + test.trainModel( + optimizer=optimizer, + training_params=training_params, + add_optimizer_defaults={"lr": 0.3}, + add_optimizer_params=[{"params": ["b"], "lr": 0.2}], + ) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.3}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.1}, {'params': 'b', 'lr': 0.2}, {'params': 'w'}], tp['optimizer_params']) - - test.trainModel(optimizer=optimizer, training_params=training_params, add_optimizer_defaults={'lr': 0.3}) + self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.1}, {"params": "b", "lr": 0.2}, {"params": "w"}], + tp["optimizer_params"], + ) + + test.trainModel( + optimizer=optimizer, + training_params=training_params, + add_optimizer_defaults={"lr": 0.3}, + ) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.3}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.7}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"lr": 0.3}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.7}, {"lr": 0.0, "params": "b"}, {"params": "w"}], + tp["optimizer_params"], + ) test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.12}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.7}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"lr": 0.12}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.7}, {"lr": 0.0, "params": "b"}, {"params": "w"}], + tp["optimizer_params"], + ) - del training_params['add_optimizer_defaults'] + del training_params["add_optimizer_defaults"] test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.5}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a', 'lr': 0.7}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"lr": 0.5}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a", "lr": 0.7}, {"lr": 0.0, "params": "b"}, {"params": "w"}], + tp["optimizer_params"], + ) - del training_params['add_optimizer_params'] + del training_params["add_optimizer_params"] test.trainModel(optimizer=optimizer, training_params=training_params) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.5}, tp['optimizer_defaults']) - self.assertEqual([{'lr': 0.1, 'params': 'a'}, {'lr': 0.0, 'params': 'b'}, {'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"lr": 0.5}, tp["optimizer_defaults"]) + self.assertEqual( + [{"lr": 0.1, "params": "a"}, {"lr": 0.0, "params": "b"}, {"params": "w"}], + tp["optimizer_params"], + ) test.trainModel(optimizer=optimizer) tp = test.getTrainingInfo() - self.assertEqual({'lr': 0.001}, tp['optimizer_defaults']) - self.assertEqual([{'params': 'a'}, {'params': 'b'}, {'params': 'w'}], tp['optimizer_params']) + self.assertEqual({"lr": 0.001}, tp["optimizer_defaults"]) + self.assertEqual( + [{"params": "a"}, {"params": "b"}, {"params": "w"}], tp["optimizer_params"] + ) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(24, tp['unused_samples']) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(24, tp['unused_samples']) def test_train_sampled_datasets(self): NeuObj.clearNames() - x = Input('x') # Position - F = Input('F') # Force + x = Input("x") # Position + F = Input("F") # Force # List the output of the model - x_z = Output('x_z', Fir(x.tw(0.3)) + Fir(F.last())) + x_z = Output("x_z", Fir(x.tw(0.3)) + Fir(F.last())) # Add the neural model to the nnodely structure and neuralization of the model test = Modely(visualizer=None) - test.addModel('x_z',x_z) - test.addMinimize('next-pos', x.z(-1), x_z, 'mse') + test.addModel("x_z", x_z) + test.addMinimize("next-pos", x.z(-1), x_z, "mse") # Create the neural network - test.neuralizeModel(sample_time=0.05) # The sampling time depends to the dataset + test.neuralizeModel( + sample_time=0.05 + ) # The sampling time depends to the dataset # Data load - data_struct = ['x','F','x2','y2','','A1x','A1y','B1x','B1y','','A2x','A2y','B2x','out','','x3','in1','in2','time'] - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=4, delimiter='\t', header=None) - test.trainModel(train_dataset='dataset', num_of_epochs=3) + data_struct = [ + "x", + "F", + "x2", + "y2", + "", + "A1x", + "A1y", + "B1x", + "B1y", + "", + "A2x", + "A2y", + "B2x", + "out", + "", + "x3", + "in1", + "in2", + "time", + ] + test.loadData( + name="dataset", + source=data_folder, + format=data_struct, + skiplines=4, + delimiter="\t", + header=None, + ) + test.trainModel(train_dataset="dataset", num_of_epochs=3) tp = test.getTrainingInfo() - self.assertEqual((15-6), test._num_of_samples['dataset']) - self.assertEqual(round(15-6),tp['n_samples_train']) - self.assertEqual(round(0),tp['n_samples_val']) - self.assertEqual(round(0),tp['n_samples_test']) - self.assertEqual(round(15-6),tp['train_batch_size']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) - self.assertEqual(128,tp['val_batch_size']) - self.assertEqual(3,tp['num_of_epochs']) - self.assertEqual(0.001,tp['optimizer_defaults']['lr']) + self.assertEqual((15 - 6), test._num_of_samples["dataset"]) + self.assertEqual(round(15 - 6), tp["n_samples_train"]) + self.assertEqual(round(0), tp["n_samples_val"]) + self.assertEqual(round(0), tp["n_samples_test"]) + self.assertEqual(round(15 - 6), tp["train_batch_size"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) + self.assertEqual(128, tp["val_batch_size"]) + self.assertEqual(3, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) ## Passing a sampled dataset import torch - train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]], - [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]], - [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]], - [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]), - 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]],[[0.8190]],[[0.8180]],[[0.8160]],[[0.8140]],[[0.8130]]])} - ## Not the same number of samples + train_data = { + "x": torch.tensor( + [ + [ + [0.8030], + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + ], + [ + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + ], + [ + [0.8080], + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + [0.8150], + ], + [ + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + [0.8150], + [0.8160], + ], + ] + ), + "F": torch.tensor( + [ + [[0.8240]], + [[0.8230]], + [[0.8220]], + [[0.8200]], + [[0.8190]], + [[0.8180]], + [[0.8160]], + [[0.8140]], + [[0.8130]], + ] + ), + } + ## Not the same number of samples with self.assertRaises(ValueError): test.trainModel(train_dataset=train_data) with self.assertRaises(ValueError): test.trainAndAnalyze(test_dataset=train_data) - train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060]], - [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]], - [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140]], - [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]]]), - 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]])} + train_data = { + "x": torch.tensor( + [ + [[0.8030], [0.8030], [0.8040], [0.8040], [0.8050], [0.8060]], + [[0.8030], [0.8040], [0.8040], [0.8050], [0.8060], [0.8070]], + [[0.8080], [0.8090], [0.8100], [0.8120], [0.8130], [0.8140]], + [[0.8090], [0.8100], [0.8120], [0.8130], [0.8140], [0.8150]], + ] + ), + "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]]), + } ## Not the correct number of dimensions with self.assertRaises(ValueError): test.trainModel(train_dataset=train_data) @@ -1117,92 +1557,327 @@ def test_train_sampled_datasets(self): test.trainAndAnalyze(test_dataset=train_data) with self.assertRaises(ValueError): test.trainAndAnalyze(train_dataset=train_data, test_dataset=train_data) - - train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]], - [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]], - [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]], - [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]), - 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]]), - 't': torch.tensor([[[0.0]],[[0.05]],[[0.1]],[[0.15]]])} + + train_data = { + "x": torch.tensor( + [ + [ + [0.8030], + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + ], + [ + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + ], + [ + [0.8080], + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + [0.8150], + ], + [ + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + [0.8150], + [0.8160], + ], + ] + ), + "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]]), + "t": torch.tensor([[[0.0]], [[0.05]], [[0.1]], [[0.15]]]), + } ## The extra sample is ignored - test.trainModel(train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3) + test.trainModel( + train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3 + ) - train_data = {'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]])} + train_data = { + "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]]) + } ## If there is a missing input the training fails with self.assertRaises(KeyError): - test.trainModel(train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3) - - train_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]], - [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]], - [[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090]], - [[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100]], - [[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100],[0.8120]], - [[0.8060],[0.8070],[0.8080],[0.8090],[0.8100],[0.8120],[0.8130]], - [[0.8070],[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140]], - [[0.8080],[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150]], - [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]), - 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]],[[0.8190]],[[0.8180]],[[0.8160]],[[0.8140]],[[0.8130]]])} - val_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]], - [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]], - [[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090]], - [[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100]], - [[0.8090],[0.8100],[0.8120],[0.8130],[0.8140],[0.8150],[0.8160]]]), - 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]],[[0.8130]]])} - + test.trainModel( + train_dataset=train_data, validation_dataset=train_data, num_of_epochs=3 + ) + + train_data = { + "x": torch.tensor( + [ + [ + [0.8030], + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + ], + [ + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + ], + [ + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + [0.8090], + ], + [ + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + [0.8090], + [0.8100], + ], + [ + [0.8050], + [0.8060], + [0.8070], + [0.8080], + [0.8090], + [0.8100], + [0.8120], + ], + [ + [0.8060], + [0.8070], + [0.8080], + [0.8090], + [0.8100], + [0.8120], + [0.8130], + ], + [ + [0.8070], + [0.8080], + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + ], + [ + [0.8080], + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + [0.8150], + ], + [ + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + [0.8150], + [0.8160], + ], + ] + ), + "F": torch.tensor( + [ + [[0.8240]], + [[0.8230]], + [[0.8220]], + [[0.8200]], + [[0.8190]], + [[0.8180]], + [[0.8160]], + [[0.8140]], + [[0.8130]], + ] + ), + } + val_data = { + "x": torch.tensor( + [ + [ + [0.8030], + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + ], + [ + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + ], + [ + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + [0.8090], + ], + [ + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + [0.8090], + [0.8100], + ], + [ + [0.8090], + [0.8100], + [0.8120], + [0.8130], + [0.8140], + [0.8150], + [0.8160], + ], + ] + ), + "F": torch.tensor( + [[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]], [[0.8130]]] + ), + } + test.trainModel(train_dataset=train_data, num_of_epochs=3) tp = test.getTrainingInfo() - self.assertEqual(9, test._num_of_samples['dataset']) - self.assertEqual(9,tp['n_samples_train']) - self.assertEqual(0,tp['n_samples_val']) - self.assertEqual(0,tp['n_samples_test']) - self.assertEqual(9,tp['train_batch_size']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) - self.assertEqual(128,tp['val_batch_size']) - self.assertEqual(3,tp['num_of_epochs']) - self.assertEqual(0.001,tp['optimizer_defaults']['lr']) - - test.trainModel(train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3) + self.assertEqual(9, test._num_of_samples["dataset"]) + self.assertEqual(9, tp["n_samples_train"]) + self.assertEqual(0, tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual(9, tp["train_batch_size"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) + self.assertEqual(128, tp["val_batch_size"]) + self.assertEqual(3, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) + + test.trainModel( + train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3 + ) tp = test.getTrainingInfo() - self.assertEqual(9, test._num_of_samples['dataset']) - self.assertEqual(9,tp['n_samples_train']) - self.assertEqual(5,tp['n_samples_val']) - self.assertEqual(0,tp['n_samples_test']) - self.assertEqual(9,tp['train_batch_size']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) - self.assertEqual(5,tp['val_batch_size']) - self.assertEqual(3,tp['num_of_epochs']) - self.assertEqual(0.001,tp['optimizer_defaults']['lr']) - - test_data = {'x': torch.tensor([[[0.8030],[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070]], - [[0.8030],[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080]], - [[0.8040],[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090]], - [[0.8040],[0.8050],[0.8060],[0.8070],[0.8080],[0.8090],[0.8100]]]), - 'F': torch.tensor([[[0.8240]],[[0.8230]],[[0.8220]],[[0.8200]]])} - - test.trainAndAnalyze(test_dataset=test_data, test_batch_size=2, train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3) + self.assertEqual(9, test._num_of_samples["dataset"]) + self.assertEqual(9, tp["n_samples_train"]) + self.assertEqual(5, tp["n_samples_val"]) + self.assertEqual(0, tp["n_samples_test"]) + self.assertEqual(9, tp["train_batch_size"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) + self.assertEqual(5, tp["val_batch_size"]) + self.assertEqual(3, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) + + test_data = { + "x": torch.tensor( + [ + [ + [0.8030], + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + ], + [ + [0.8030], + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + ], + [ + [0.8040], + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + [0.8090], + ], + [ + [0.8040], + [0.8050], + [0.8060], + [0.8070], + [0.8080], + [0.8090], + [0.8100], + ], + ] + ), + "F": torch.tensor([[[0.8240]], [[0.8230]], [[0.8220]], [[0.8200]]]), + } + + test.trainAndAnalyze( + test_dataset=test_data, + test_batch_size=2, + train_dataset=train_data, + validation_dataset=val_data, + num_of_epochs=3, + ) tp = test.getTrainingInfo() - self.assertEqual(9, test._num_of_samples['dataset']) - self.assertEqual(9,tp['n_samples_train']) - self.assertEqual(5,tp['n_samples_val']) - self.assertEqual(4,tp['n_samples_test']) - self.assertEqual(9,tp['train_batch_size']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) - self.assertEqual(5,tp['val_batch_size']) - self.assertEqual(3,tp['num_of_epochs']) - self.assertEqual(0.001,tp['optimizer_defaults']['lr']) - - test.trainAndAnalyze(test_dataset=test_data, test_batch_size=2, train_dataset=train_data, validation_dataset=val_data, num_of_epochs=3, prediction_samples=2) + self.assertEqual(9, test._num_of_samples["dataset"]) + self.assertEqual(9, tp["n_samples_train"]) + self.assertEqual(5, tp["n_samples_val"]) + self.assertEqual(4, tp["n_samples_test"]) + self.assertEqual(9, tp["train_batch_size"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) + self.assertEqual(5, tp["val_batch_size"]) + self.assertEqual(3, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) + + test.trainAndAnalyze( + test_dataset=test_data, + test_batch_size=2, + train_dataset=train_data, + validation_dataset=val_data, + num_of_epochs=3, + prediction_samples=2, + ) tp = test.getTrainingInfo() - self.assertEqual(9, test._num_of_samples['dataset']) - self.assertEqual(9,tp['n_samples_train']) - self.assertEqual(5,tp['n_samples_val']) - self.assertEqual(4,tp['n_samples_test']) - self.assertEqual(9,tp['train_batch_size']) - self.assertEqual(1, tp['update_per_epochs']) - #self.assertEqual(0, tp['unused_samples']) - self.assertEqual(5,tp['val_batch_size']) - self.assertEqual(3,tp['num_of_epochs']) - self.assertEqual(0.001,tp['optimizer_defaults']['lr']) \ No newline at end of file + self.assertEqual(9, test._num_of_samples["dataset"]) + self.assertEqual(9, tp["n_samples_train"]) + self.assertEqual(5, tp["n_samples_val"]) + self.assertEqual(4, tp["n_samples_test"]) + self.assertEqual(9, tp["train_batch_size"]) + self.assertEqual(1, tp["update_per_epochs"]) + # self.assertEqual(0, tp['unused_samples']) + self.assertEqual(5, tp["val_batch_size"]) + self.assertEqual(3, tp["num_of_epochs"]) + self.assertEqual(0.001, tp["optimizer_defaults"]["lr"]) diff --git a/tests/test_results.py b/tests/test_results.py index 62426625..a7a89629 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -15,303 +15,631 @@ # in closed loop and states cases # in connect and states cases -data_folder = os.path.join(os.path.dirname(__file__), '_data/') +data_folder = os.path.join(os.path.dirname(__file__), "_data/") + class ModelyTrainingTest(unittest.TestCase): def test_analysis_results(self): NeuObj.clearNames() - input1 = Input('in1') - target1 = Input('out1') - target2 = Input('out2') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out', Fir(W=a)(input1.last())) - - test = Modely(visualizer=None,seed=42) - test.addModel('model', output1) - test.addMinimize('error1', target1.last(), output1) - test.addMinimize('error2', target2.last(), output1) + input1 = Input("in1") + target1 = Input("out1") + target2 = Input("out2") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out", Fir(W=a)(input1.last())) + + test = Modely(visualizer=None, seed=42) + test.addModel("model", output1) + test.addMinimize("error1", target1.last(), output1) + test.addMinimize("error2", target2.last(), output1) test.neuralizeModel() - dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]} - test.loadData(name='dataset', source=dataset) + dataset = { + "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + } + test.loadData(name="dataset", source=dataset) # Test prediction - test.analyzeModel('dataset') - self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]], - 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]}, - test.prediction['dataset']['error1']) - self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse']) - self.assertEqual((2.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse']) - self.assertEqual((1+4)/2.0, test.performance['dataset']['total']['mean_error']) - - test.analyzeModel('dataset', batch_size=5) - self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]], - 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]}, - test.prediction['dataset']['error1']) - self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse']) - self.assertEqual((2.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse']) - self.assertEqual((1+4)/2.0, test.performance['dataset']['total']['mean_error']) - - test.analyzeModel('dataset', batch_size=6) - self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]], - 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]}, - test.prediction['dataset']['error1']) - self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse']) - self.assertEqual((2.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse']) - self.assertEqual((1+4)/2.0, test.performance['dataset']['total']['mean_error']) - - dataset = {'in1': [1,1,1,1,1,1,2,2,3,3], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]} - test.loadData(name='dataset2', source=dataset) - - test.analyzeModel('dataset2') - self.assertAlmostEqual((1.0 ** 2.0) * 8.0 / 10.0, test.performance['dataset2']['error1']['mse'], places=6) - self.assertAlmostEqual(((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0, test.performance['dataset2']['error2']['mse'], places=6) - self.assertAlmostEqual(((1.0 ** 2) * 8.0 / 10.0 + ((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0 )/2.0, test.performance['dataset2']['total']['mean_error'], places=6) - - test.analyzeModel('dataset2', batch_size=5) - self.assertAlmostEqual((1.0 ** 2.0) * 8.0 / 10.0, test.performance['dataset2']['error1']['mse'], places=6) - self.assertAlmostEqual(((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0, test.performance['dataset2']['error2']['mse'], places=6) - self.assertAlmostEqual(((1.0 ** 2) * 8.0 / 10.0 + ((2.0 ** 2) * 6.0 + (1.0 ** 2) * 2.0) / 10.0 )/2.0, test.performance['dataset2']['total']['mean_error'], places=6) - - test.analyzeModel('dataset2', batch_size=6) - self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]], - 'B': [[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]],[[1.0]]]}, - test.prediction['dataset2']['error1']) - self.assertEqual((1.0 ** 2) * 6.0 / 6.0, test.performance['dataset2']['error1']['mse']) - self.assertEqual((2.0 ** 2) * 6.0 / 6.0, test.performance['dataset2']['error2']['mse']) - self.assertEqual((1+4)/2.0, test.performance['dataset2']['total']['mean_error']) - - test.analyzeModel('dataset2', minimize_gain={'error1': 0.5, 'error2': 0.0}) - self.assertAlmostEqual((1.0 ** 2.0) * 8.0 / 10.0 * 0.5, test.performance['dataset2']['error1']['mse'], places=6) - self.assertAlmostEqual(0.0, test.performance['dataset2']['error2']['mse'], places=6) - self.assertAlmostEqual(((1.0 ** 2) * 8.0 / 10.0 * 0.5 + 0.0)/2.0, test.performance['dataset2']['total']['mean_error'], places=6) + test.analyzeModel("dataset") + self.assertEqual( + { + "A": [ + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + ], + "B": [ + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + ], + }, + test.prediction["dataset"]["error1"], + ) + self.assertEqual( + (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"] + ) + self.assertEqual( + (2.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"] + ) + self.assertEqual( + (1 + 4) / 2.0, test.performance["dataset"]["total"]["mean_error"] + ) + + test.analyzeModel("dataset", batch_size=5) + self.assertEqual( + { + "A": [ + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + ], + "B": [ + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + [[1.0]], + ], + }, + test.prediction["dataset"]["error1"], + ) + self.assertEqual( + (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"] + ) + self.assertEqual( + (2.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"] + ) + self.assertEqual( + (1 + 4) / 2.0, test.performance["dataset"]["total"]["mean_error"] + ) + + test.analyzeModel("dataset", batch_size=6) + self.assertEqual( + { + "A": [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], + "B": [[[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]]], + }, + test.prediction["dataset"]["error1"], + ) + self.assertEqual( + (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"] + ) + self.assertEqual( + (2.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"] + ) + self.assertEqual( + (1 + 4) / 2.0, test.performance["dataset"]["total"]["mean_error"] + ) + + dataset = { + "in1": [1, 1, 1, 1, 1, 1, 2, 2, 3, 3], + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + } + test.loadData(name="dataset2", source=dataset) + + test.analyzeModel("dataset2") + self.assertAlmostEqual( + (1.0**2.0) * 8.0 / 10.0, + test.performance["dataset2"]["error1"]["mse"], + places=6, + ) + self.assertAlmostEqual( + ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0, + test.performance["dataset2"]["error2"]["mse"], + places=6, + ) + self.assertAlmostEqual( + ((1.0**2) * 8.0 / 10.0 + ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0) / 2.0, + test.performance["dataset2"]["total"]["mean_error"], + places=6, + ) + + test.analyzeModel("dataset2", batch_size=5) + self.assertAlmostEqual( + (1.0**2.0) * 8.0 / 10.0, + test.performance["dataset2"]["error1"]["mse"], + places=6, + ) + self.assertAlmostEqual( + ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0, + test.performance["dataset2"]["error2"]["mse"], + places=6, + ) + self.assertAlmostEqual( + ((1.0**2) * 8.0 / 10.0 + ((2.0**2) * 6.0 + (1.0**2) * 2.0) / 10.0) / 2.0, + test.performance["dataset2"]["total"]["mean_error"], + places=6, + ) + + test.analyzeModel("dataset2", batch_size=6) + self.assertEqual( + { + "A": [[[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]], [[2.0]]], + "B": [[[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]], [[1.0]]], + }, + test.prediction["dataset2"]["error1"], + ) + self.assertEqual( + (1.0**2) * 6.0 / 6.0, test.performance["dataset2"]["error1"]["mse"] + ) + self.assertEqual( + (2.0**2) * 6.0 / 6.0, test.performance["dataset2"]["error2"]["mse"] + ) + self.assertEqual( + (1 + 4) / 2.0, test.performance["dataset2"]["total"]["mean_error"] + ) + + test.analyzeModel("dataset2", minimize_gain={"error1": 0.5, "error2": 0.0}) + self.assertAlmostEqual( + (1.0**2.0) * 8.0 / 10.0 * 0.5, + test.performance["dataset2"]["error1"]["mse"], + places=6, + ) + self.assertAlmostEqual( + 0.0, test.performance["dataset2"]["error2"]["mse"], places=6 + ) + self.assertAlmostEqual( + ((1.0**2) * 8.0 / 10.0 * 0.5 + 0.0) / 2.0, + test.performance["dataset2"]["total"]["mean_error"], + places=6, + ) def test_analysis_results_closed_loop_state(self): NeuObj.clearNames() - input1 = Input('in1') - target1 = Input('out1') - target2 = Input('out2') - a = Parameter('a', sw=1, values=[[2]]) + input1 = Input("in1") + target1 = Input("out1") + target2 = Input("out2") + a = Parameter("a", sw=1, values=[[2]]) relation = Fir(W=a)(input1.last()) relation.closedLoop(input1) - output1 = Output('out', relation) + output1 = Output("out", relation) test = Modely(visualizer=None, seed=42) - test.addModel('model', output1) - test.addMinimize('error1', target1.last(), output1) - test.addMinimize('error2', target2.last(), output1) + test.addModel("model", output1) + test.addMinimize("error1", target1.last(), output1) + test.addMinimize("error2", target2.last(), output1) test.neuralizeModel() - #Prediction samples = None - dataset = {'in1': [1,1,1,1,1,1,1,1,1,1], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]} - test.loadData(name='dataset', source=dataset) + # Prediction samples = None + dataset = { + "in1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + } + test.loadData(name="dataset", source=dataset) # Test prediction - test.analyzeModel('dataset',prediction_samples=-1) - self.assertEqual({'A': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]], - 'B': [[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]],[[2.0]]]}, - test.prediction['dataset']['error1']) - self.assertEqual((0.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse']) - self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse']) - self.assertEqual((0+1)/2.0, test.performance['dataset']['total']['mean_error']) - - dataset = {'in1': [1,1,1,1,1,1,2,2,3,3], 'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]} - test.loadData(name='dataset2', source=dataset) - - test.analyzeModel('dataset2', batch_size=5) - self.assertEqual((0.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error1']['mse']) - self.assertEqual((1.0 ** 2) * 10.0 / 10.0, test.performance['dataset']['error2']['mse']) - self.assertEqual((0+1)/2.0, test.performance['dataset']['total']['mean_error']) + test.analyzeModel("dataset", prediction_samples=-1) + self.assertEqual( + { + "A": [ + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + ], + "B": [ + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + [[2.0]], + ], + }, + test.prediction["dataset"]["error1"], + ) + self.assertEqual( + (0.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"] + ) + self.assertEqual( + (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"] + ) + self.assertEqual( + (0 + 1) / 2.0, test.performance["dataset"]["total"]["mean_error"] + ) + + dataset = { + "in1": [1, 1, 1, 1, 1, 1, 2, 2, 3, 3], + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + } + test.loadData(name="dataset2", source=dataset) + + test.analyzeModel("dataset2", batch_size=5) + self.assertEqual( + (0.0**2) * 10.0 / 10.0, test.performance["dataset"]["error1"]["mse"] + ) + self.assertEqual( + (1.0**2) * 10.0 / 10.0, test.performance["dataset"]["error2"]["mse"] + ) + self.assertEqual( + (0 + 1) / 2.0, test.performance["dataset"]["total"]["mean_error"] + ) # Prediction samples = 5 - dataset = {'in1': [1,2,3,4,5,6,7,8,9,10], 'out1': [11,12,13,14,15,16,17,18,19,20], 'out2': [10,20,30,40,50,60,70,80,90,100]} - test.loadData(name='dataset3', source=dataset) - - test.analyzeModel('dataset3', prediction_samples=5, batch_size=2) - A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]]], - [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], - [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], - [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], - [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], - [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]] - B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]]], - [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], - [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], - [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], - [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], - [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]] - C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]], - [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], - [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], - [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], - [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], - [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]] - self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1']) - self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2']) - self.assertAlmostEqual(np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error1']['mse'], places=3) - self.assertAlmostEqual(np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error2']['mse'], places=3) - self.assertAlmostEqual((np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0+np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0)/2.0, test.performance['dataset3']['total']['mean_error'], places=3) - test.analyzeModel(splits=[50,30,20], dataset='dataset3', prediction_samples=1, batch_size=1) - A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]]], [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], - [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]] - B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]]], [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], - [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]] - C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]], [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], - [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]] - self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1']) - self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2']) - - test.analyzeModel('dataset3', prediction_samples=5, batch_size=4) - A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]]], - [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], - [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], - [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], - [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], - [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]] - B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]]], - [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], - [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], - [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], - [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], - [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]] - C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]], - [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], - [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], - [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], - [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], - [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]] - self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1']) - self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2']) - self.assertAlmostEqual(np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error1']['mse'], places=3) - self.assertAlmostEqual(np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0, test.performance['dataset3']['error2']['mse'], places=3) - self.assertAlmostEqual((np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/24.0+np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/24.0)/2.0, test.performance['dataset3']['total']['mean_error'], places=3) - - test.analyzeModel('dataset3', prediction_samples=4, batch_size=6) - A =[[[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]], - [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]], - [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]], - [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]], - [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]]] - B =[[[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]], - [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]], - [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]], - [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]], - [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]]] - C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]], - [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]], - [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]], - [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]], - [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]]] - self.assertEqual({'A': A, 'B': B}, test.prediction['dataset3']['error1']) - self.assertEqual({'A': C, 'B': B}, test.prediction['dataset3']['error2']) - self.assertAlmostEqual(np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/30.0, test.performance['dataset3']['error1']['mse'], places=3) - self.assertAlmostEqual(np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/30.0, test.performance['dataset3']['error2']['mse'], places=3) - self.assertAlmostEqual((np.sum((np.array(A).flatten()-np.array(B).flatten())**2)/30.0+np.sum((np.array(C).flatten()-np.array(B).flatten())**2)/30.0)/2.0, test.performance['dataset3']['total']['mean_error'], places=3) - - dataset = {'out1': [2,2,2,2,2,2,2,2,2,2], 'out2': [3,3,3,3,3,3,3,3,3,3]} - test.loadData(name='dataset4', source=dataset) - test.trainModel(dataset='dataset4', prediction_samples=-1) #TODO FIX - test.analyzeModel('dataset4', prediction_samples=-1) #TODO FIX - + dataset = { + "in1": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "out1": [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "out2": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + } + test.loadData(name="dataset3", source=dataset) + + test.analyzeModel("dataset3", prediction_samples=5, batch_size=2) + A = [ + [[[11.0]], [[12.0]], [[13.0]], [[14.0]]], + [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], + [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], + [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], + [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], + [[[16.0]], [[17.0]], [[18.0]], [[19.0]]], + ] + B = [ + [[[2.0]], [[4.0]], [[6.0]], [[8.0]]], + [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], + [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], + [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], + [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], + [[[64.0]], [[128.0]], [[192.0]], [[256.0]]], + ] + C = [ + [[[10.0]], [[20.0]], [[30.0]], [[40.0]]], + [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], + [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], + [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], + [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], + [[[60.0]], [[70.0]], [[80.0]], [[90.0]]], + ] + self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"]) + self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"]) + self.assertAlmostEqual( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset3"]["error1"]["mse"], + places=3, + ) + self.assertAlmostEqual( + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset3"]["error2"]["mse"], + places=3, + ) + self.assertAlmostEqual( + ( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0 + ) + / 2.0, + test.performance["dataset3"]["total"]["mean_error"], + places=3, + ) + test.analyzeModel( + splits=[50, 30, 20], dataset="dataset3", prediction_samples=1, batch_size=1 + ) + A = [ + [[[11.0]], [[12.0]], [[13.0]], [[14.0]]], + [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], + [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], + [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], + [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], + [[[16.0]], [[17.0]], [[18.0]], [[19.0]]], + ] + B = [ + [[[2.0]], [[4.0]], [[6.0]], [[8.0]]], + [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], + [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], + [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], + [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], + [[[64.0]], [[128.0]], [[192.0]], [[256.0]]], + ] + C = [ + [[[10.0]], [[20.0]], [[30.0]], [[40.0]]], + [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], + [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], + [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], + [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], + [[[60.0]], [[70.0]], [[80.0]], [[90.0]]], + ] + self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"]) + self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"]) + + test.analyzeModel("dataset3", prediction_samples=5, batch_size=4) + A = [ + [[[11.0]], [[12.0]], [[13.0]], [[14.0]]], + [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], + [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], + [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], + [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], + [[[16.0]], [[17.0]], [[18.0]], [[19.0]]], + ] + B = [ + [[[2.0]], [[4.0]], [[6.0]], [[8.0]]], + [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], + [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], + [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], + [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], + [[[64.0]], [[128.0]], [[192.0]], [[256.0]]], + ] + C = [ + [[[10.0]], [[20.0]], [[30.0]], [[40.0]]], + [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], + [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], + [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], + [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], + [[[60.0]], [[70.0]], [[80.0]], [[90.0]]], + ] + self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"]) + self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"]) + self.assertAlmostEqual( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset3"]["error1"]["mse"], + places=3, + ) + self.assertAlmostEqual( + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset3"]["error2"]["mse"], + places=3, + ) + self.assertAlmostEqual( + ( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0 + ) + / 2.0, + test.performance["dataset3"]["total"]["mean_error"], + places=3, + ) + + test.analyzeModel("dataset3", prediction_samples=4, batch_size=6) + A = [ + [[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]], + [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]], + [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]], + [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]], + [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]], + ] + B = [ + [[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]], + [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]], + [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]], + [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]], + [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]], + ] + C = [ + [[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]], + [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]], + [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]], + [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]], + [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]], + ] + self.assertEqual({"A": A, "B": B}, test.prediction["dataset3"]["error1"]) + self.assertEqual({"A": C, "B": B}, test.prediction["dataset3"]["error2"]) + self.assertAlmostEqual( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0, + test.performance["dataset3"]["error1"]["mse"], + places=3, + ) + self.assertAlmostEqual( + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0, + test.performance["dataset3"]["error2"]["mse"], + places=3, + ) + self.assertAlmostEqual( + ( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0 + + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0 + ) + / 2.0, + test.performance["dataset3"]["total"]["mean_error"], + places=3, + ) + + dataset = { + "out1": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "out2": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + } + test.loadData(name="dataset4", source=dataset) + test.trainModel(dataset="dataset4", prediction_samples=-1) # TODO FIX + test.analyzeModel("dataset4", prediction_samples=-1) # TODO FIX def test_analysis_results_closed_loop(self): NeuObj.clearNames() - input1 = Input('in1') - target1 = Input('out1') - target2 = Input('out2') - a = Parameter('a', sw=1, values=[[2]]) - output1 = Output('out', Fir(W=a)(input1.last())) + input1 = Input("in1") + target1 = Input("out1") + target2 = Input("out2") + a = Parameter("a", sw=1, values=[[2]]) + output1 = Output("out", Fir(W=a)(input1.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model', output1) - test.addMinimize('error1', target1.last(), output1) - test.addMinimize('error2', target2.last(), output1) + test.addModel("model", output1) + test.addMinimize("error1", target1.last(), output1) + test.addMinimize("error2", target2.last(), output1) test.neuralizeModel() - dataset = {'in1': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'out1': [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], - 'out2': [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]} - test.loadData(name='dataset', source=dataset) - - test.analyzeModel('dataset', closed_loop={'in1': 'out'}, prediction_samples=5, batch_size=2) - A = [[[[11.0]], [[12.0]], [[13.0]], [[14.0]]], - [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], - [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], - [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], - [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], - [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]] - B = [[[[2.0]], [[4.0]], [[6.0]], [[8.0]]], - [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], - [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], - [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], - [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], - [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]] - C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]], - [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], - [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], - [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], - [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], - [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]] - self.assertEqual({'A': A, 'B': B}, test.prediction['dataset']['error1']) - self.assertEqual({'A': C, 'B': B}, test.prediction['dataset']['error2']) - self.assertAlmostEqual(np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0, - test.performance['dataset']['error1']['mse'], places=3) - self.assertAlmostEqual(np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0, - test.performance['dataset']['error2']['mse'], places=3) - self.assertAlmostEqual((np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + np.sum( - (np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0) / 2.0, - test.performance['dataset']['total']['mean_error'], places=3) - - test.analyzeModel('dataset', closed_loop={'in1': 'out'}, prediction_samples=5, batch_size=4) - A = [[[[11.0]], [[12.0]], [[13.0]], [[14.0]]], - [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], - [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], - [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], - [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], - [[[16.0]], [[17.0]], [[18.0]], [[19.0]]]] - B = [[[[2.0]], [[4.0]], [[6.0]], [[8.0]]], - [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], - [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], - [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], - [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], - [[[64.0]], [[128.0]], [[192.0]], [[256.0]]]] - C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]]], - [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], - [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], - [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], - [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], - [[[60.0]], [[70.0]], [[80.0]], [[90.0]]]] - self.assertEqual({'A': A, 'B': B}, test.prediction['dataset']['error1']) - self.assertEqual({'A': C, 'B': B}, test.prediction['dataset']['error2']) - self.assertAlmostEqual(np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0, - test.performance['dataset']['error1']['mse'], places=3) - self.assertAlmostEqual(np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0, - test.performance['dataset']['error2']['mse'], places=3) - self.assertAlmostEqual((np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + np.sum( - (np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0) / 2.0, - test.performance['dataset']['total']['mean_error'], places=3) - - test.analyzeModel('dataset', closed_loop={'in1': 'out'}, prediction_samples=4, batch_size=6) - A = [[[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]], - [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]], - [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]], - [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]], - [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]]] - B = [[[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]], - [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]], - [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]], - [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]], - [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]]] - C = [[[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]], - [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]], - [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]], - [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]], - [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]]] - self.assertEqual({'A': A, 'B': B}, test.prediction['dataset']['error1']) - self.assertEqual({'A': C, 'B': B}, test.prediction['dataset']['error2']) - self.assertAlmostEqual(np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0, - test.performance['dataset']['error1']['mse'], places=3) - self.assertAlmostEqual(np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0, - test.performance['dataset']['error2']['mse'], places=3) - self.assertAlmostEqual((np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0 + np.sum( - (np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0) / 2.0, - test.performance['dataset']['total']['mean_error'], places=3) + dataset = { + "in1": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "out1": [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + "out2": [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + } + test.loadData(name="dataset", source=dataset) + + test.analyzeModel( + "dataset", closed_loop={"in1": "out"}, prediction_samples=5, batch_size=2 + ) + A = [ + [[[11.0]], [[12.0]], [[13.0]], [[14.0]]], + [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], + [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], + [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], + [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], + [[[16.0]], [[17.0]], [[18.0]], [[19.0]]], + ] + B = [ + [[[2.0]], [[4.0]], [[6.0]], [[8.0]]], + [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], + [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], + [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], + [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], + [[[64.0]], [[128.0]], [[192.0]], [[256.0]]], + ] + C = [ + [[[10.0]], [[20.0]], [[30.0]], [[40.0]]], + [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], + [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], + [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], + [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], + [[[60.0]], [[70.0]], [[80.0]], [[90.0]]], + ] + self.assertEqual({"A": A, "B": B}, test.prediction["dataset"]["error1"]) + self.assertEqual({"A": C, "B": B}, test.prediction["dataset"]["error2"]) + self.assertAlmostEqual( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset"]["error1"]["mse"], + places=3, + ) + self.assertAlmostEqual( + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset"]["error2"]["mse"], + places=3, + ) + self.assertAlmostEqual( + ( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0 + ) + / 2.0, + test.performance["dataset"]["total"]["mean_error"], + places=3, + ) + + test.analyzeModel( + "dataset", closed_loop={"in1": "out"}, prediction_samples=5, batch_size=4 + ) + A = [ + [[[11.0]], [[12.0]], [[13.0]], [[14.0]]], + [[[12.0]], [[13.0]], [[14.0]], [[15.0]]], + [[[13.0]], [[14.0]], [[15.0]], [[16.0]]], + [[[14.0]], [[15.0]], [[16.0]], [[17.0]]], + [[[15.0]], [[16.0]], [[17.0]], [[18.0]]], + [[[16.0]], [[17.0]], [[18.0]], [[19.0]]], + ] + B = [ + [[[2.0]], [[4.0]], [[6.0]], [[8.0]]], + [[[4.0]], [[8.0]], [[12.0]], [[16.0]]], + [[[8.0]], [[16.0]], [[24.0]], [[32.0]]], + [[[16.0]], [[32.0]], [[48.0]], [[64.0]]], + [[[32.0]], [[64.0]], [[96.0]], [[128.0]]], + [[[64.0]], [[128.0]], [[192.0]], [[256.0]]], + ] + C = [ + [[[10.0]], [[20.0]], [[30.0]], [[40.0]]], + [[[20.0]], [[30.0]], [[40.0]], [[50.0]]], + [[[30.0]], [[40.0]], [[50.0]], [[60.0]]], + [[[40.0]], [[50.0]], [[60.0]], [[70.0]]], + [[[50.0]], [[60.0]], [[70.0]], [[80.0]]], + [[[60.0]], [[70.0]], [[80.0]], [[90.0]]], + ] + self.assertEqual({"A": A, "B": B}, test.prediction["dataset"]["error1"]) + self.assertEqual({"A": C, "B": B}, test.prediction["dataset"]["error2"]) + self.assertAlmostEqual( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset"]["error1"]["mse"], + places=3, + ) + self.assertAlmostEqual( + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0, + test.performance["dataset"]["error2"]["mse"], + places=3, + ) + self.assertAlmostEqual( + ( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 24.0 + + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 24.0 + ) + / 2.0, + test.performance["dataset"]["total"]["mean_error"], + places=3, + ) + + test.analyzeModel( + "dataset", closed_loop={"in1": "out"}, prediction_samples=4, batch_size=6 + ) + A = [ + [[[11.0]], [[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]]], + [[[12.0]], [[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]]], + [[[13.0]], [[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]]], + [[[14.0]], [[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]]], + [[[15.0]], [[16.0]], [[17.0]], [[18.0]], [[19.0]], [[20.0]]], + ] + B = [ + [[[2.0]], [[4.0]], [[6.0]], [[8.0]], [[10.0]], [[12.0]]], + [[[4.0]], [[8.0]], [[12.0]], [[16.0]], [[20.0]], [[24.0]]], + [[[8.0]], [[16.0]], [[24.0]], [[32.0]], [[40.0]], [[48.0]]], + [[[16.0]], [[32.0]], [[48.0]], [[64.0]], [[80.0]], [[96.0]]], + [[[32.0]], [[64.0]], [[96.0]], [[128.0]], [[160.0]], [[192.0]]], + ] + C = [ + [[[10.0]], [[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]]], + [[[20.0]], [[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]]], + [[[30.0]], [[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]]], + [[[40.0]], [[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]]], + [[[50.0]], [[60.0]], [[70.0]], [[80.0]], [[90.0]], [[100.0]]], + ] + self.assertEqual({"A": A, "B": B}, test.prediction["dataset"]["error1"]) + self.assertEqual({"A": C, "B": B}, test.prediction["dataset"]["error2"]) + self.assertAlmostEqual( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0, + test.performance["dataset"]["error1"]["mse"], + places=3, + ) + self.assertAlmostEqual( + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0, + test.performance["dataset"]["error2"]["mse"], + places=3, + ) + self.assertAlmostEqual( + ( + np.sum((np.array(A).flatten() - np.array(B).flatten()) ** 2) / 30.0 + + np.sum((np.array(C).flatten() - np.array(B).flatten()) ** 2) / 30.0 + ) + / 2.0, + test.performance["dataset"]["total"]["mean_error"], + places=3, + ) diff --git a/tests/test_train.py b/tests/test_train.py index a9fe6fe4..bb8d551f 100644 --- a/tests/test_train.py +++ b/tests/test_train.py @@ -14,283 +14,354 @@ # 5 Tests # This file tests the value of the training parameters -data_folder = os.path.join(os.path.dirname(__file__), '_data/') +data_folder = os.path.join(os.path.dirname(__file__), "_data/") + class ModelyTrainingTest(unittest.TestCase): def test_training_values_fir(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('out1') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out', Fir(W=a)(input1.last())) - output2 = Output('out2', Fir(W_init='init_constant', W_init_params={'value':1})(input1.last())) - output3 = Output('out3', Fir(W_init='init_exp', b_init='init_exp')(input1.last())) - output4 = Output('out4', Fir(W_init='init_lin', b_init='init_lin')(input1.last())) - output5 = Output('out5', Fir(W_init='init_negexp', b_init='init_negexp')(input1.last())) - - test = Modely(visualizer=None,seed=42) - test.addModel('model', [output1,output2,output3,output4,output5]) - test.addMinimize('error', target.last(), output1) + input1 = Input("in1") + target = Input("out1") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out", Fir(W=a)(input1.last())) + output2 = Output( + "out2", + Fir(W_init="init_constant", W_init_params={"value": 1})(input1.last()), + ) + output3 = Output( + "out3", Fir(W_init="init_exp", b_init="init_exp")(input1.last()) + ) + output4 = Output( + "out4", Fir(W_init="init_lin", b_init="init_lin")(input1.last()) + ) + output5 = Output( + "out5", Fir(W_init="init_negexp", b_init="init_negexp")(input1.last()) + ) + + test = Modely(visualizer=None, seed=42) + test.addModel("model", [output1, output2, output3, output4, output5]) + test.addMinimize("error", target.last(), output1) test.neuralizeModel() - dataset = {'in1': [1], 'in2':[[1,2,3]], 'out1': [2]} - test.loadData(name='dataset', source=dataset) + dataset = {"in1": [1], "in2": [[1, 2, 3]], "out1": [2]} + test.loadData(name="dataset", source=dataset) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2) + self.assertListEqual([[1.0]], test.parameters["a"]) def test_training_values_linear(self): NeuObj.clearNames() - input1 = Input('in1') - input2 = Input('in2', dimensions=3) - target = Input('out1') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - output1 = Output('out', Linear(W=W,b=b)(input1.last())) - output2 = Output('out2', Linear(W_init='init_constant', W_init_params={'value':1})(input1.last())) - output3 = Output('out3', Linear(W_init='init_exp', b_init='init_exp')(input2.last())) - output4 = Output('out4', Linear(W_init='init_negexp', b_init='init_negexp')(input2.last())) - output5 = Output('out5', Linear(W_init='init_lin', b_init='init_lin')(input2.last())) + input1 = Input("in1") + input2 = Input("in2", dimensions=3) + target = Input("out1") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + output1 = Output("out", Linear(W=W, b=b)(input1.last())) + output2 = Output( + "out2", + Linear(W_init="init_constant", W_init_params={"value": 1})(input1.last()), + ) + output3 = Output( + "out3", Linear(W_init="init_exp", b_init="init_exp")(input2.last()) + ) + output4 = Output( + "out4", Linear(W_init="init_negexp", b_init="init_negexp")(input2.last()) + ) + output5 = Output( + "out5", Linear(W_init="init_lin", b_init="init_lin")(input2.last()) + ) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2,output3,output4,output5]) - test.addMinimize('error', target.last(), output1) + test.addModel("model", [output1, output2, output3, output4, output5]) + test.addMinimize("error", target.last(), output1) test.neuralizeModel() - dataset = {'in1': [1], 'in2':[[1,2,3]], 'out1': [3]} - test.loadData(name='dataset', source=dataset) + dataset = {"in1": [1], "in2": [[1, 2, 3]], "out1": [3]} + test.loadData(name="dataset", source=dataset) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertListEqual([-3.0], test.parameters['b']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertListEqual([-3.0], test.parameters["b"]) def test_training_clear_model(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('int1') - a = Parameter('a', sw=1, values=[[1]]) + input1 = Input("in1") + target = Input("int1") + a = Parameter("a", sw=1, values=[[1]]) fir_out = Fir(W=a)(input1.last()) - output1 = Output('out1', fir_out) + output1 = Output("out1", fir_out) - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - output2 = Output('out2', Linear(W=W,b=b)(fir_out)) + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + output2 = Output("out2", Linear(W=W, b=b)(fir_out)) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error', target.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error", target.last(), output2) test.neuralizeModel() - self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]})) - - dataset = {'in1': [1], 'int1': [3]} - test.loadData(name='dataset', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertEqual({"out1": [1.0], "out2": [2.0]}, test({"in1": [1]})) + + dataset = {"in1": [1], "int1": [3]} + test.loadData(name="dataset", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) def test_network_linear_interpolation_train(self): NeuObj.clearNames() - x = Input('x') - param = Parameter(name='a', sw=1) - rel1 = Fir(W=param)(Interpolation([1.0, 2.0, 3.0, 4.0], [2.0, 4.0, 6.0, 8.0], mode='linear')(x.last())) - out = Output('out',rel1) + x = Input("x") + param = Parameter(name="a", sw=1) + rel1 = Fir(W=param)( + Interpolation([1.0, 2.0, 3.0, 4.0], [2.0, 4.0, 6.0, 8.0], mode="linear")( + x.last() + ) + ) + out = Output("out", rel1) test = Modely(visualizer=None, seed=1) - test.addModel('fun',[out]) - test.addMinimize('error', out, x.last()) + test.addModel("fun", [out]) + test.addMinimize("error", out, x.last()) test.neuralizeModel(0.01) - dataset = {'x':np.random.uniform(1,4,100)} - test.loadData(name='dataset', source=dataset) + dataset = {"x": np.random.uniform(1, 4, 100)} + test.loadData(name="dataset", source=dataset) test.trainModel(num_of_epochs=100, train_batch_size=10) - self.assertAlmostEqual(test.parameters['a'][0][0], 0.5, places=2) + self.assertAlmostEqual(test.parameters["a"][0][0], 0.5, places=2) def test_multimodel_with_loss_gain_and_lr_gain(self): NeuObj.clearNames() ## Model1 - input1 = Input('in1') - a1 = Parameter('a1', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - output11 = Output('out11', Fir(W=a1)(input1.tw(0.05))) - a2 = Parameter('a2', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) - output12 = Output('out12', Fir(W=a2)(input1.tw(0.05))) - a3 = Parameter('a3', dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) - output13 = Output('out13', Fir(W=a3)(input1.tw(0.05))) + input1 = Input("in1") + a1 = Parameter("a1", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output11 = Output("out11", Fir(W=a1)(input1.tw(0.05))) + a2 = Parameter("a2", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output12 = Output("out12", Fir(W=a2)(input1.tw(0.05))) + a3 = Parameter("a3", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output13 = Output("out13", Fir(W=a3)(input1.tw(0.05))) test = Modely(visualizer=None, seed=42) - test.addModel('model1', [output11, output12, output13]) - test.addMinimize('error11', input1.next(), output11) - test.addMinimize('error12', input1.next(), output12) - test.addMinimize('error13', input1.next(), output13) + test.addModel("model1", [output11, output12, output13]) + test.addMinimize("error11", input1.next(), output11) + test.addMinimize("error12", input1.next(), output12) + test.addMinimize("error13", input1.next(), output13) ## Model2 - input2 = Input('in2') - b1 = Parameter('b1', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - output21 = Output('out21', Fir(W=b1)(input2.tw(0.05))) - b2 = Parameter('b2', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - output22 = Output('out22', Fir(W=b2)(input2.tw(0.05))) - b3 = Parameter('b3', dimensions=1, tw=0.05, values=[[1],[1],[1],[1],[1]]) - output23 = Output('out23', Fir(W=b3)(input2.tw(0.05))) - - test.addModel('model2', [output21, output22, output23]) - test.addMinimize('error21', input2.next(), output21) - test.addMinimize('error22', input2.next(), output22) - test.addMinimize('error23', input2.next(), output23) + input2 = Input("in2") + b1 = Parameter("b1", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output21 = Output("out21", Fir(W=b1)(input2.tw(0.05))) + b2 = Parameter("b2", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output22 = Output("out22", Fir(W=b2)(input2.tw(0.05))) + b3 = Parameter("b3", dimensions=1, tw=0.05, values=[[1], [1], [1], [1], [1]]) + output23 = Output("out23", Fir(W=b3)(input2.tw(0.05))) + + test.addModel("model2", [output21, output22, output23]) + test.addMinimize("error21", input2.next(), output21) + test.addMinimize("error22", input2.next(), output22) + test.addMinimize("error23", input2.next(), output23) test.neuralizeModel(0.01) data_in1 = [1, 1, 1, 1, 1, 2] data_in2 = [1, 1, 1, 1, 1, 2] - dataset = {'in1': data_in1, 'in2': data_in2} + dataset = {"in1": data_in1, "in2": data_in2} - test.loadData(name='dataset', source=dataset) + test.loadData(name="dataset", source=dataset) ## Train only model1 - self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b1'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['a2'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b2'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['a3'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b3'], [[1], [1], [1], [1], [1]]) - test.trainModel(optimizer='SGD', models='model1', splits=[100,0,0], lr=1, num_of_epochs=1) - self.assertListEqual(test.parameters['a1'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b1'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b2'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b3'], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b1"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["a2"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b2"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["a3"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b3"], [[1], [1], [1], [1], [1]]) + test.trainModel( + optimizer="SGD", models="model1", splits=[100, 0, 0], lr=1, num_of_epochs=1 + ) + self.assertListEqual(test.parameters["a1"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b1"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b2"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b3"], [[1], [1], [1], [1], [1]]) ## Train only model2 test.neuralizeModel(0.01, clear_model=True) - test.trainModel(optimizer='SGD', models='model2', splits=[100,0,0], lr=1, num_of_epochs=1) - self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a2'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a3'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]]) + test.trainModel( + optimizer="SGD", models="model2", splits=[100, 0, 0], lr=1, num_of_epochs=1 + ) + self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a2"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a3"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]]) ## Train both models test.neuralizeModel(0.01, clear_model=True) - test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1) - self.assertListEqual(test.parameters['a1'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]]) + test.trainModel( + optimizer="SGD", + models=["model1", "model2"], + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + ) + self.assertListEqual(test.parameters["a1"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]]) ## Train both models but set the gain of a to zero and the gain of b to double test.neuralizeModel(0.01, clear_model=True) - test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1, lr_param={'a1':0, 'b1':2}) - self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b1'], [[-11], [-11], [-11], [-11], [-11]]) - self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]]) + test.trainModel( + optimizer="SGD", + models=["model1", "model2"], + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + lr_param={"a1": 0, "b1": 2}, + ) + self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b1"], [[-11], [-11], [-11], [-11], [-11]]) + self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]]) ## Train both models but set the minimize gain of error1 to zero and the minimize gain of error2 to double test.neuralizeModel(0.01, clear_model=True) - test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1, minimize_gain={'error11':0}) - self.assertListEqual(test.parameters['a1'], [[1], [1], [1], [1], [1]]) - self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]]) + test.trainModel( + optimizer="SGD", + models=["model1", "model2"], + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + minimize_gain={"error11": 0}, + ) + self.assertListEqual(test.parameters["a1"], [[1], [1], [1], [1], [1]]) + self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]]) ## Train both models but set the minimize gain of error1 to zero and the minimize gain of error2 to double test.neuralizeModel(0.01, clear_model=True) - test.trainModel(optimizer='SGD', models=['model1','model2'], splits=[100,0,0], lr=1, num_of_epochs=1, minimize_gain={'error11':-1,'error22':2}) - self.assertListEqual(test.parameters['a1'], [[7], [7], [7], [7], [7]]) - self.assertListEqual(test.parameters['b1'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['a2'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b2'], [[-11], [-11], [-11], [-11], [-11]]) - self.assertListEqual(test.parameters['a3'], [[-5], [-5], [-5], [-5], [-5]]) - self.assertListEqual(test.parameters['b3'], [[-5], [-5], [-5], [-5], [-5]]) + test.trainModel( + optimizer="SGD", + models=["model1", "model2"], + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + minimize_gain={"error11": -1, "error22": 2}, + ) + self.assertListEqual(test.parameters["a1"], [[7], [7], [7], [7], [7]]) + self.assertListEqual(test.parameters["b1"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["a2"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b2"], [[-11], [-11], [-11], [-11], [-11]]) + self.assertListEqual(test.parameters["a3"], [[-5], [-5], [-5], [-5], [-5]]) + self.assertListEqual(test.parameters["b3"], [[-5], [-5], [-5], [-5], [-5]]) def test_train_equation_learner(self): # TODO aggiungi la verifica dei parametri NeuObj.clearNames() + def func(x): return np.cos(x) + np.sin(x) - - data_x = np.random.uniform(0, 2*np.pi, 200) + + data_x = np.random.uniform(0, 2 * np.pi, 200) data_y = func(data_x) - dataset = {'x': data_x, 'y': data_y} + dataset = {"x": data_x, "y": data_y} - x = Input('x') - y = Input('y') + x = Input("x") + y = Input("y") linear_in = Linear(output_dimension=5) linear_in_2 = Linear(output_dimension=5) - linear_out = Linear(output_dimension=1, W_init=init_constant, W_init_params={'value':1}) + linear_out = Linear( + output_dimension=1, W_init=init_constant, W_init_params={"value": 1} + ) - equation_learner = EquationLearner(functions=[Sin, Identity, Add, Cos], linear_in=linear_in) ## W=1*5 , b=1, activation_out=4 - equation_learner2 = EquationLearner(functions=[Add, Identity, Mul],linear_in=linear_in_2, linear_out=linear_out) ## INGRESSO W=4*5, b=5, activation_out=3 USCITA W=3*1, b=1 + equation_learner = EquationLearner( + functions=[Sin, Identity, Add, Cos], linear_in=linear_in + ) ## W=1*5 , b=1, activation_out=4 + equation_learner2 = EquationLearner( + functions=[Add, Identity, Mul], linear_in=linear_in_2, linear_out=linear_out + ) ## INGRESSO W=4*5, b=5, activation_out=3 USCITA W=3*1, b=1 eq1 = equation_learner(x.last()) eq2 = equation_learner2(eq1) - out = Output('eq2', eq2) + out = Output("eq2", eq2) example = Modely(visualizer=None) - example.addModel('model',[out]) - example.addMinimize('error', out, y.last()) + example.addModel("model", [out]) + example.addMinimize("error", out, y.last()) example.neuralizeModel() - example.loadData(name='dataset', source=dataset) + example.loadData(name="dataset", source=dataset) ## Print the initial weights - optimizer_defaults = {'weight_decay': 0.3,} - example.trainModel(train_dataset='dataset', lr=0.01, num_of_epochs=2, optimizer_defaults=optimizer_defaults, early_stopping=select_best_model) + optimizer_defaults = { + "weight_decay": 0.3, + } + example.trainModel( + train_dataset="dataset", + lr=0.01, + num_of_epochs=2, + optimizer_defaults=optimizer_defaults, + early_stopping=select_best_model, + ) def test_train_derivate_wrt_input(self): NeuObj.clearNames() - x = Input('x') - dy_dx_target = Input('dy_dx') + x = Input("x") + dy_dx_target = Input("dy_dx") x_last = x.last() def parametric_fun(x, a, b, c, d): import torch - return x ** 3 * a + x ** 2 * b + torch.sin(x) * c + d + + return x**3 * a + x**2 * b + torch.sin(x) * c + d def dx_parametric_fun(x, a, b, c, d): import torch - return (3 * x ** 2 * a) + (2 * x * b) + c * torch.cos(x) - fun = ParamFun(parametric_fun,['a','b','c','d'])(x_last) - approx_dy_dx = Output('d_out', Differentiate(fun, x_last)) + return (3 * x**2 * a) + (2 * x * b) + c * torch.cos(x) + + fun = ParamFun(parametric_fun, ["a", "b", "c", "d"])(x_last) + approx_dy_dx = Output("d_out", Differentiate(fun, x_last)) test = Modely(visualizer=None, seed=12) @@ -300,45 +371,108 @@ def dx_parametric_fun(x, a, b, c, d): data_b = -0.03 data_c = 2.02 data_d = -1.05 - dataset = {'x': data_x, 'dy_dx': dx_parametric_fun(data_x, data_a, data_b, data_c, data_d)} + dataset = { + "x": data_x, + "dy_dx": dx_parametric_fun(data_x, data_a, data_b, data_c, data_d), + } # d y_approx / d x == dy_dx # Se x era una time window and dy_dx dovrà essere una time window - test.addModel('model', [approx_dy_dx]) - test.addMinimize('sob_err', 'd_out', dy_dx_target.last()) + test.addModel("model", [approx_dy_dx]) + test.addMinimize("sob_err", "d_out", dy_dx_target.last()) test.neuralizeModel() - test.loadData('data', dataset) - test.trainModel(num_of_epochs=1000, splits=[70,20,10], lr=0.3) - self.assertAlmostEqual(test.parameters['a'][0], data_a, places=4) - self.assertAlmostEqual(test.parameters['b'][0], data_b, places=4) - self.assertAlmostEqual(test.parameters['c'][0], data_c, places=4) - #The value data_d is not match because the derivative does not depend on it - + test.loadData("data", dataset) + test.trainModel(num_of_epochs=1000, splits=[70, 20, 10], lr=0.3) + self.assertAlmostEqual(test.parameters["a"][0], data_a, places=4) + self.assertAlmostEqual(test.parameters["b"][0], data_b, places=4) + self.assertAlmostEqual(test.parameters["c"][0], data_c, places=4) + # The value data_d is not match because the derivative does not depend on it + def test_step(self): NeuObj.clearNames() - x = Input('x') + x = Input("x") relation = Fir()(x.tw(0.05)) relation.closedLoop(x) - output = Output('out', relation) + output = Output("out", relation) test = Modely(visualizer=None, log_internal=True) - test.addModel('model', output) - test.addMinimize('error', output, x.next()) + test.addModel("model", output) + test.addMinimize("error", output, x.next()) test.neuralizeModel(0.01) - train_data_x = np.array(10*[10] + 20*[20] + 30*[30], dtype=np.float32) - train_dataset = {'x': train_data_x, 'time': np.array(range(60), dtype=np.float32)} - test.loadData(name='dataset', source=train_dataset, ) - self.assertListEqual(list(test._data['dataset']['x'].shape), [55, 6, 1]) ## 60 observations, time window of 6 so in total 54+1 samples - test.trainModel(step=10, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9) - self.assertEqual(len(test.internals.keys()), 20) ## chosed 10 sample at index = [0, 21] and for each sample the horizon is 10 so in total 4*10=40 - test.trainModel(step=10, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=True, prediction_samples=9) - self.assertEqual(len(test.internals.keys()), 20) ## shuffle data does not change the number of samples - test.trainModel(step=0, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9) - self.assertEqual(len(test.internals.keys()), 40) ## chosed 10 sample at index = [0, 11, 22, 33] and for each sample the horizon is 10 so in total 4*10=40 - test.trainModel(step=1000, train_batch_size=10, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9) - self.assertEqual(len(test.internals.keys()), 10) ## clip step to max value = 36 so just 1 sample * 10 prediction samples - test.trainModel(step=10, train_batch_size=1, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9) - self.assertEqual(len(test.internals.keys()), 50) ## chosed sample = [0, 11, 22, 33, 44] and for each sample the horizon is 10 so in total 5*10=50 - test.trainModel(step=0, train_batch_size=1, train_dataset='dataset', num_of_epochs=1, shuffle_data=False, prediction_samples=9) - self.assertEqual(len(test.internals.keys()), 460) ## 46 sample * 10 horizon \ No newline at end of file + train_data_x = np.array(10 * [10] + 20 * [20] + 30 * [30], dtype=np.float32) + train_dataset = { + "x": train_data_x, + "time": np.array(range(60), dtype=np.float32), + } + test.loadData( + name="dataset", + source=train_dataset, + ) + self.assertListEqual( + list(test._data["dataset"]["x"].shape), [55, 6, 1] + ) ## 60 observations, time window of 6 so in total 54+1 samples + test.trainModel( + step=10, + train_batch_size=10, + train_dataset="dataset", + num_of_epochs=1, + shuffle_data=False, + prediction_samples=9, + ) + self.assertEqual( + len(test.internals.keys()), 20 + ) ## chosed 10 sample at index = [0, 21] and for each sample the horizon is 10 so in total 4*10=40 + test.trainModel( + step=10, + train_batch_size=10, + train_dataset="dataset", + num_of_epochs=1, + shuffle_data=True, + prediction_samples=9, + ) + self.assertEqual( + len(test.internals.keys()), 20 + ) ## shuffle data does not change the number of samples + test.trainModel( + step=0, + train_batch_size=10, + train_dataset="dataset", + num_of_epochs=1, + shuffle_data=False, + prediction_samples=9, + ) + self.assertEqual( + len(test.internals.keys()), 40 + ) ## chosed 10 sample at index = [0, 11, 22, 33] and for each sample the horizon is 10 so in total 4*10=40 + test.trainModel( + step=1000, + train_batch_size=10, + train_dataset="dataset", + num_of_epochs=1, + shuffle_data=False, + prediction_samples=9, + ) + self.assertEqual( + len(test.internals.keys()), 10 + ) ## clip step to max value = 36 so just 1 sample * 10 prediction samples + test.trainModel( + step=10, + train_batch_size=1, + train_dataset="dataset", + num_of_epochs=1, + shuffle_data=False, + prediction_samples=9, + ) + self.assertEqual( + len(test.internals.keys()), 50 + ) ## chosed sample = [0, 11, 22, 33, 44] and for each sample the horizon is 10 so in total 5*10=50 + test.trainModel( + step=0, + train_batch_size=1, + train_dataset="dataset", + num_of_epochs=1, + shuffle_data=False, + prediction_samples=9, + ) + self.assertEqual(len(test.internals.keys()), 460) ## 46 sample * 10 horizon diff --git a/tests/test_train_recurrent.py b/tests/test_train_recurrent.py index da059a37..434fb560 100644 --- a/tests/test_train_recurrent.py +++ b/tests/test_train_recurrent.py @@ -14,701 +14,1352 @@ # 15 Tests # Test the value of the weight after the recurrent training + # Linear function -def linear_fun(x,a,b): - return x*a+b +def linear_fun(x, a, b): + return x * a + b + -data_x = np.random.rand(500)*20-10 +data_x = np.random.rand(500) * 20 - 10 data_a = 2 data_b = -3 -dataset = {'in1': data_x, 'out': linear_fun(data_x,data_a,data_b)} -data_folder = '/tests/_data/' +dataset = {"in1": data_x, "out": linear_fun(data_x, data_a, data_b)} +data_folder = "/tests/_data/" + class ModelyTrainingTest(unittest.TestCase): def assertAlmostEqual(self, data1, data2, precision=3): if type(data1) == type(data2) == list: - assert np.asarray(data1, dtype=np.float32).ndim == np.asarray(data2,dtype=np.float32).ndim, f'Inputs must have the same dimension! Received {type(data1)} and {type(data2)}' + assert ( + np.asarray(data1, dtype=np.float32).ndim + == np.asarray(data2, dtype=np.float32).ndim + ), ( + f"Inputs must have the same dimension! Received {type(data1)} and {type(data2)}" + ) self.assertEqual(len(data1), len(data2)) for pred, label in zip(data1, data2): self.assertAlmostEqual(pred, label, precision=precision) elif type(data1) == type(data2) == dict: self.assertEqual(len(data1.items()), len(data2.items())) - for (pred_key,pred_value), (label_key,label_value) in zip(data1.items(), data2.items()): + for (pred_key, pred_value), (label_key, label_value) in zip( + data1.items(), data2.items() + ): self.assertAlmostEqual(pred_value, label_value, precision=precision) else: super().assertAlmostEqual(data1, data2, places=precision) def test_recurrent_shuffle(self): NeuObj.clearNames() - target = Input('target') - x = Input('x') + target = Input("target") + x = Input("x") relation = Fir(x.last()) relation.closedLoop(x) - output = Output('out', relation) + output = Output("out", relation) test = Modely(visualizer=None, seed=42, log_internal=True) - test.addModel('model', output) - test.addMinimize('out', target.next(), 'out') + test.addModel("model", output) + test.addMinimize("out", target.next(), "out") test.neuralizeModel(0.01) - dataset = {'x': [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], 'target': [21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40]} - test.loadData(name='dataset', source=dataset) - - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=4, prediction_samples=1, step=1, shuffle_data=True) - self.assertListEqual([[[4.0]], [[1.0]], [[9.0]], [[18.0]]], test.internals['inout_0_0']['XY']['x']) - self.assertListEqual([[[25.0]], [[22.0]], [[30.0]], [[39.0]]], test.internals['inout_0_0']['XY']['target']) - self.assertListEqual([[[26.0]], [[23.0]], [[31.0]], [[40.0]]], test.internals['inout_0_1']['XY']['target']) - self.assertListEqual([[[5.0]], [[16.0]], [[3.0]], [[13.0]]], test.internals['inout_1_0']['XY']['x']) - self.assertListEqual([[[26.0]], [[37.0]], [[24.0]], [[34.0]]], test.internals['inout_1_0']['XY']['target']) - self.assertListEqual([[[27.0]], [[38.0]], [[25.0]], [[35.0]]], test.internals['inout_1_1']['XY']['target']) - self.assertListEqual([[[15.0]], [[2.0]], [[17.0]], [[14.0]]], test.internals['inout_2_0']['XY']['x']) - self.assertListEqual([[[36.0]], [[23.0]], [[38.0]], [[35.0]]], test.internals['inout_2_0']['XY']['target']) - self.assertListEqual([[[37.0]], [[24.0]], [[39.0]], [[36.0]]], test.internals['inout_2_1']['XY']['target']) - - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=2, - prediction_samples=2, step=0, shuffle_data=True) + dataset = { + "x": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + ], + "target": [ + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + ], + } + test.loadData(name="dataset", source=dataset) + + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=0.01, + num_of_epochs=1, + train_batch_size=4, + prediction_samples=1, + step=1, + shuffle_data=True, + ) + self.assertListEqual( + [[[4.0]], [[1.0]], [[9.0]], [[18.0]]], + test.internals["inout_0_0"]["XY"]["x"], + ) + self.assertListEqual( + [[[25.0]], [[22.0]], [[30.0]], [[39.0]]], + test.internals["inout_0_0"]["XY"]["target"], + ) + self.assertListEqual( + [[[26.0]], [[23.0]], [[31.0]], [[40.0]]], + test.internals["inout_0_1"]["XY"]["target"], + ) + self.assertListEqual( + [[[5.0]], [[16.0]], [[3.0]], [[13.0]]], + test.internals["inout_1_0"]["XY"]["x"], + ) + self.assertListEqual( + [[[26.0]], [[37.0]], [[24.0]], [[34.0]]], + test.internals["inout_1_0"]["XY"]["target"], + ) + self.assertListEqual( + [[[27.0]], [[38.0]], [[25.0]], [[35.0]]], + test.internals["inout_1_1"]["XY"]["target"], + ) + self.assertListEqual( + [[[15.0]], [[2.0]], [[17.0]], [[14.0]]], + test.internals["inout_2_0"]["XY"]["x"], + ) + self.assertListEqual( + [[[36.0]], [[23.0]], [[38.0]], [[35.0]]], + test.internals["inout_2_0"]["XY"]["target"], + ) + self.assertListEqual( + [[[37.0]], [[24.0]], [[39.0]], [[36.0]]], + test.internals["inout_2_1"]["XY"]["target"], + ) + + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=0.01, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=2, + step=0, + shuffle_data=True, + ) # ( number_samples - window_size - prediction_samples )// (batch_size + step=0) * (predictoin_samples+1) - self.assertEqual((20-1-2)//2*3, len(test.internals.keys())) + self.assertEqual((20 - 1 - 2) // 2 * 3, len(test.internals.keys())) with self.assertRaises(ValueError): - test.trainModel(dataset='dataset', splits=[40,30,30], optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=2, - prediction_samples=50, step=0, shuffle_data=True) + test.trainModel( + dataset="dataset", + splits=[40, 30, 30], + optimizer="SGD", + lr=0.01, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=50, + step=0, + shuffle_data=True, + ) with self.assertRaises(ValueError): - test.trainModel(dataset='dataset', splits=[40,10,40], optimizer='SGD', lr=0.01, num_of_epochs=1, train_batch_size=2, - prediction_samples=50, step=0, shuffle_data=True) + test.trainModel( + dataset="dataset", + splits=[40, 10, 40], + optimizer="SGD", + lr=0.01, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=50, + step=0, + shuffle_data=True, + ) from nnodely.support.earlystopping import early_stop_patience, select_best_model - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.01, num_of_epochs=15, - train_batch_size=2, early_stopping=early_stop_patience, early_stopping_params={'patience':2}, select_model=select_best_model, - prediction_samples=2, step=0, shuffle_data=True) + + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=0.01, + num_of_epochs=15, + train_batch_size=2, + early_stopping=early_stop_patience, + early_stopping_params={"patience": 2}, + select_model=select_best_model, + prediction_samples=2, + step=0, + shuffle_data=True, + ) def test_train_multifiles(self): NeuObj.clearNames() - x = Input('x') - y = Input('y') - relation = Fir()(x.tw(0.05))+Fir(y.sw([-2,2])) + x = Input("x") + y = Input("y") + relation = Fir()(x.tw(0.05)) + Fir(y.sw([-2, 2])) relation.closedLoop(x) - output = Output('out', relation) + output = Output("out", relation) test = Modely(visualizer=None, log_internal=True) - test.addModel('model', output) - test.addMinimize('error', 'out', x.next()) + test.addModel("model", output) + test.addMinimize("error", "out", x.next()) test.neuralizeModel(0.01) ## The folder contains 3 files with 10, 20 and 30 samples respectively - data_struct = ['x', 'y'] - data_folder = os.path.join(os.path.dirname(__file__), 'multifile/') - test.loadData(name='dataset', source=data_folder, format=data_struct, skiplines=1) - self.assertEqual(len(test._data['dataset']['x']), 42) - self.assertEqual(len(test._data['dataset']['y']), 42) - - test.trainModel(splits=[70, 20, 10], train_batch_size = 3, num_of_epochs=1, prediction_samples=2) - self.assertEqual(len(list(test.internals.keys())), 3*7) - self.assertEqual(list(np.mean(np.array(test.internals['inout_0_0']['XY']['y']),axis=1)), - list(np.mean(np.array(test.internals['inout_0_1']['XY']['y']),axis=1))) - self.assertEqual(list(np.mean(np.array(test.internals['inout_0_0']['XY']['y']), axis=1)), - list(np.mean(np.array(test.internals['inout_0_2']['XY']['y']), axis=1))) - self.assertEqual(list(np.mean(np.array(test.internals['inout_1_0']['XY']['y']),axis=1)), - list(np.mean(np.array(test.internals['inout_1_1']['XY']['y']),axis=1))) - self.assertEqual(list(np.mean(np.array(test.internals['inout_1_0']['XY']['y']), axis=1)), - list(np.mean(np.array(test.internals['inout_1_2']['XY']['y']), axis=1))) - self.assertEqual(list(np.mean(np.array(test.internals['inout_2_0']['XY']['y']),axis=1)), - list(np.mean(np.array(test.internals['inout_2_1']['XY']['y']),axis=1))) - self.assertEqual(list(np.mean(np.array(test.internals['inout_2_0']['XY']['y']), axis=1)), - list(np.mean(np.array(test.internals['inout_2_2']['XY']['y']), axis=1))) - self.assertEqual(list(np.mean(np.array(test.internals['inout_6_0']['XY']['y']),axis=1)), - list(np.mean(np.array(test.internals['inout_6_1']['XY']['y']),axis=1))) - self.assertEqual(list(np.mean(np.array(test.internals['inout_6_0']['XY']['y']), axis=1)), - list(np.mean(np.array(test.internals['inout_6_2']['XY']['y']), axis=1))) + data_struct = ["x", "y"] + data_folder = os.path.join(os.path.dirname(__file__), "multifile/") + test.loadData( + name="dataset", source=data_folder, format=data_struct, skiplines=1 + ) + self.assertEqual(len(test._data["dataset"]["x"]), 42) + self.assertEqual(len(test._data["dataset"]["y"]), 42) + + test.trainModel( + splits=[70, 20, 10], + train_batch_size=3, + num_of_epochs=1, + prediction_samples=2, + ) + self.assertEqual(len(list(test.internals.keys())), 3 * 7) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_0_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_0_1"]["XY"]["y"]), axis=1)), + ) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_0_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_0_2"]["XY"]["y"]), axis=1)), + ) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_1_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_1_1"]["XY"]["y"]), axis=1)), + ) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_1_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_1_2"]["XY"]["y"]), axis=1)), + ) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_2_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_2_1"]["XY"]["y"]), axis=1)), + ) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_2_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_2_2"]["XY"]["y"]), axis=1)), + ) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_6_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_6_1"]["XY"]["y"]), axis=1)), + ) + self.assertEqual( + list(np.mean(np.array(test.internals["inout_6_0"]["XY"]["y"]), axis=1)), + list(np.mean(np.array(test.internals["inout_6_2"]["XY"]["y"]), axis=1)), + ) def test_training_values_fir_connect_linear(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) + input1 = Input("in1") + target = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) relation = Fir(W=a)(input1.last()) - inout = Input('inout') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) + inout = Input("inout") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) relation.connect(inout) - output1 = Output('out1', relation) - output2 = Output('out2', Linear(W=W,b=b)(inout.last())) + output1 = Output("out1", relation) + output2 = Output("out2", Linear(W=W, b=b)(inout.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error', target.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error", target.last(), output2) test.neuralizeModel() - self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]})) - - dataset = {'in1': [1], 'target1': [3]} - test.loadData(name='dataset', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertEqual({"out1": [1.0], "out2": [2.0]}, test({"in1": [1]})) + + dataset = {"in1": [1], "target1": [3]} + test.loadData(name="dataset", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) - - dataset = {'in1': [1,1], 'target1': [3,3]} - test.loadData(name='dataset2', source=dataset) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) + + dataset = {"in1": [1, 1], "target1": [3, 3]} + test.loadData(name="dataset2", source=dataset) test.neuralizeModel(clear_model=True) # the out is 3.0 due the mean of the error is not the same of two epochs - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=1 + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=2 + ) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) def test_training_values_fir_train_connect_linear(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('out1') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out1-net',Fir(W=a)(input1.last())) + input1 = Input("in1") + target = Input("out1") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out1-net", Fir(W=a)(input1.last())) - inout = Input('inout') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=1) - output2 = Output('out2-net', Linear(W=W,b=b)(inout.last())) + inout = Input("inout") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=1) + output2 = Output("out2-net", Linear(W=W, b=b)(inout.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error', target.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error", target.last(), output2) test.neuralizeModel() - self.assertEqual({'out1-net': [1.0], 'out2-net': [2.0]}, test({'in1': [1]}, connect={'inout': 'out1-net'})) - - dataset = {'in1': [1], 'out1': [3]} - test.loadData(name='dataset', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'}) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertEqual( + {"out1-net": [1.0], "out2-net": [2.0]}, + test({"in1": [1]}, connect={"inout": "out1-net"}), + ) + + dataset = {"in1": [1], "out1": [3]} + test.loadData(name="dataset", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1-net"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1-net"}, + ) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1-net'}) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1-net"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1-net"}, + ) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2, connect={'inout': 'out1-net'}) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=2, + connect={"inout": "out1-net"}, + ) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) with self.assertRaises(KeyError): - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2) - dataset = {'in1': [1,1], 'out1': [3,3]} - test.loadData(name='dataset2', source=dataset) + dataset = {"in1": [1, 1], "out1": [3, 3]} + test.loadData(name="dataset2", source=dataset) test.neuralizeModel(clear_model=True) # the out is 3.0 due the mean of the error is not the same of two epochs - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, connect={'inout': 'out1-net'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + connect={"inout": "out1-net"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2, connect={'inout': 'out1-net'}) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=2, + connect={"inout": "out1-net"}, + ) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) def test_training_values_fir_connect_linear_only_model(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out1',Fir(W=a)(input1.last())) + input1 = Input("in1") + target = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out1", Fir(W=a)(input1.last())) - inout = Input('inout') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - output2 = Output('out2', Linear(W=W,b=b)(inout.last())) + inout = Input("inout") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + output2 = Output("out2", Linear(W=W, b=b)(inout.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model1', output1) - test.addModel('model2', output2) - test.addMinimize('error', target.last(), output2) + test.addModel("model1", output1) + test.addModel("model2", output2) + test.addMinimize("error", target.last(), output2) test.addConnect(output1, inout) test.neuralizeModel() - self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]})) + self.assertEqual({"out1": [1.0], "out2": [2.0]}, test({"in1": [1]})) - dataset = {'in1': [1], 'target1': [3]} - test.loadData(name='dataset', source=dataset) + dataset = {"in1": [1], "target1": [3]} + test.loadData(name="dataset", source=dataset) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + models="model1", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1 + ) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel( + models="model1", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1 + ) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertListEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + models="model2", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1 + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + models="model2", optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1 + ) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertListEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) def test_training_values_fir_train_connect_linear_only_model(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out1',Fir(W=a)(input1.last())) + input1 = Input("in1") + target = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out1", Fir(W=a)(input1.last())) - inout = Input('inout') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - output2 = Output('out2', Linear(W=W,b=b)(inout.last())) + inout = Input("inout") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + output2 = Output("out2", Linear(W=W, b=b)(inout.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model1', output1) - test.addModel('model2', output2) - test.addMinimize('error', target.last(), output2) + test.addModel("model1", output1) + test.addModel("model2", output2) + test.addMinimize("error", target.last(), output2) test.neuralizeModel() - self.assertEqual({'out1': [1.0], 'out2': [1.0]}, test({'in1': [1]})) - self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]}, connect={'inout': 'out1'})) - - dataset = {'in1': [1], 'target1': [3]} - test.loadData(name='dataset', source=dataset) - - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'}) - self.assertListEqual([[-51.0]], test.parameters['W']) - self.assertListEqual([-15.0], test.parameters['b']) - self.assertListEqual([[-51.0]], test.parameters['a']) + self.assertEqual({"out1": [1.0], "out2": [1.0]}, test({"in1": [1]})) + self.assertEqual( + {"out1": [1.0], "out2": [2.0]}, + test({"in1": [1]}, connect={"inout": "out1"}), + ) + + dataset = {"in1": [1], "target1": [3]} + test.loadData(name="dataset", source=dataset) + + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1"}, + ) + self.assertListEqual([[-51.0]], test.parameters["W"]) + self.assertListEqual([-15.0], test.parameters["b"]) + self.assertListEqual([[-51.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'}) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[3.0]], test.parameters['a']) - test.trainModel(models='model1', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'}) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + models="model1", + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1"}, + ) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[3.0]], test.parameters["a"]) + test.trainModel( + models="model1", + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1"}, + ) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(models='model2', optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, connect={'inout': 'out1'}) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertListEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + models="model2", + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + models="model2", + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + connect={"inout": "out1"}, + ) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertListEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) def test_training_values_fir_connect_linear_more_prediction(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('out1') - a = Parameter('a', sw=1, values=[[1]]) - relation =Fir(W=a)(input1.last()) + input1 = Input("in1") + target = Input("out1") + a = Parameter("a", sw=1, values=[[1]]) + relation = Fir(W=a)(input1.last()) - inout = Input('inout') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=1) + inout = Input("inout") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=1) relation.connect(inout) - output1 = Output('out1-net', relation) - output2 = Output('out2-net', Linear(W=W,b=b)(inout.last())) + output1 = Output("out1-net", relation) + output2 = Output("out2-net", Linear(W=W, b=b)(inout.last())) - test = Modely(visualizer=None,seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error', target.last(), output2) + test = Modely(visualizer=None, seed=42) + test.addModel("model", [output1, output2]) + test.addMinimize("error", target.last(), output2) test.neuralizeModel() - self.assertEqual({'out1-net': [1.0], 'out2-net': [2.0]}, test({'in1': [1]})) - - dataset = {'in1': [0,2,7,1], 'out1': [3,4,5,1], 'inout': [1,1,2,2]} - test.loadData(name='dataset2', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0) - self.assertListEqual([[-9.0]], test.parameters['W']) - self.assertEqual([0.5], test.parameters['b']) - self.assertListEqual([[-9.0]], test.parameters['a']) + self.assertEqual({"out1-net": [1.0], "out2-net": [2.0]}, test({"in1": [1]})) + + dataset = {"in1": [0, 2, 7, 1], "out1": [3, 4, 5, 1], "inout": [1, 1, 2, 2]} + test.loadData(name="dataset2", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + ) + self.assertListEqual([[-9.0]], test.parameters["W"]) + self.assertEqual([0.5], test.parameters["b"]) + self.assertListEqual([[-9.0]], test.parameters["a"]) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + ) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3) - self.assertListEqual([[-9.0]], test.parameters['W']) - self.assertEqual([0.5], test.parameters['b']) - self.assertListEqual([[-9.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + ) + self.assertListEqual([[-9.0]], test.parameters["W"]) + self.assertEqual([0.5], test.parameters["b"]) + self.assertListEqual([[-9.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - #TODO add this test for check prediction_Sample -1 for connect + # TODO add this test for check prediction_Sample -1 for connect # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=-1) # self.assertListEqual([[-9.0]], test.parameters['W']) ? # self.assertEqual([0.5], test.parameters['b']) ? # self.assertListEqual([[-9.0]], test.parameters['a']) ? - dataset = {'in1': [0, 2, 7, 1, 5, 0, 2], 'out1': [1, 4, 8, 2, 6, 1, 1]} - test.loadData(name='dataset3', source=dataset) + dataset = {"in1": [0, 2, 7, 1, 5, 0, 2], "out1": [1, 4, 8, 2, 6, 1, 1]} + test.loadData(name="dataset3", source=dataset) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=2, prediction_samples=3) - self.assertListEqual([[-162.75]], test.parameters['W']) - self.assertEqual([-15.75], test.parameters['b']) - self.assertListEqual([[-162.75]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + shuffle_data=False, + lr=1, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=3, + ) + self.assertListEqual([[-162.75]], test.parameters["W"]) + self.assertEqual([-15.75], test.parameters["b"]) + self.assertListEqual([[-162.75]], test.parameters["a"]) # Because is a connect and the window is 1 the initialization of the state is overwritten by the out1 - test.loadData(name='dataset4', source=dataset|{'inout': [0,0,0,0,0,0,0]}) + test.loadData( + name="dataset4", source=dataset | {"inout": [0, 0, 0, 0, 0, 0, 0]} + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset4', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=2, prediction_samples=3) - self.assertListEqual([[-162.75]], test.parameters['W']) - self.assertEqual([-15.75], test.parameters['b']) - self.assertListEqual([[-162.75]], test.parameters['a']) + test.trainModel( + train_dataset="dataset4", + optimizer="SGD", + shuffle_data=False, + lr=1, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=3, + ) + self.assertListEqual([[-162.75]], test.parameters["W"]) + self.assertEqual([-15.75], test.parameters["b"]) + self.assertListEqual([[-162.75]], test.parameters["a"]) def test_training_values_fir_train_connect_linear_more_prediction(self): NeuObj.clearNames() - input1 = Input('in1') - target = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out1',Fir(W=a)(input1.last())) + input1 = Input("in1") + target = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out1", Fir(W=a)(input1.last())) - inout = Input('inout') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - output2 = Output('out2', Linear(W=W,b=b)(inout.last())) + inout = Input("inout") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + output2 = Output("out2", Linear(W=W, b=b)(inout.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error', target.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error", target.last(), output2) test.neuralizeModel() - self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1]},connect={'inout': 'out1'})) - - dataset = {'in1': [0,2,7,1], 'target1': [3,4,5,1]} - test.loadData(name='dataset2', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout': 'out1'}) - self.assertListEqual([[-9.0]], test.parameters['W']) - self.assertListEqual([0.5], test.parameters['b']) - self.assertListEqual([[-9.0]], test.parameters['a']) + self.assertEqual( + {"out1": [1.0], "out2": [2.0]}, + test({"in1": [1]}, connect={"inout": "out1"}), + ) + + dataset = {"in1": [0, 2, 7, 1], "target1": [3, 4, 5, 1]} + test.loadData(name="dataset2", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + connect={"inout": "out1"}, + ) + self.assertListEqual([[-9.0]], test.parameters["W"]) + self.assertListEqual([0.5], test.parameters["b"]) + self.assertListEqual([[-9.0]], test.parameters["a"]) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10, connect={'inout': 'out1'}) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + connect={"inout": "out1"}, + ) # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, connect={'inout': 'out1'}) # TODO add this test test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout': 'out1'}) - self.assertListEqual([[-9.0]], test.parameters['W']) - self.assertListEqual([0.5], test.parameters['b']) - self.assertListEqual([[-9.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + connect={"inout": "out1"}, + ) + self.assertListEqual([[-9.0]], test.parameters["W"]) + self.assertListEqual([0.5], test.parameters["b"]) + self.assertListEqual([[-9.0]], test.parameters["a"]) # Because is a connect and the window is 1 the initialization of the state is overwritten by the out1 - dataset = {'in1': [0,2,7,1], 'target1': [3,4,5,1], 'inout':[1,1,2,2]} - test.loadData(name='dataset3', source=dataset) + dataset = {"in1": [0, 2, 7, 1], "target1": [3, 4, 5, 1], "inout": [1, 1, 2, 2]} + test.loadData(name="dataset3", source=dataset) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout': 'out1'}) - self.assertListEqual([[-9.0]], test.parameters['W']) - self.assertListEqual([0.5], test.parameters['b']) - self.assertListEqual([[-9.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + connect={"inout": "out1"}, + ) + self.assertListEqual([[-9.0]], test.parameters["W"]) + self.assertListEqual([0.5], test.parameters["b"]) + self.assertListEqual([[-9.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3) - self.assertListEqual([[-273.0]], test.parameters['W']) - self.assertListEqual([-137.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + shuffle_data=False, + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + ) + self.assertListEqual([[-273.0]], test.parameters["W"]) + self.assertListEqual([-137.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=-1) - self.assertListEqual([[-273.0]], test.parameters['W']) - self.assertListEqual([-137.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + shuffle_data=False, + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=-1, + ) + self.assertListEqual([[-273.0]], test.parameters["W"]) + self.assertListEqual([-137.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset3', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, train_batch_size=1) - self.assertListEqual([[-273.0]], test.parameters['W']) - self.assertListEqual([-137.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - - dataset = {'in1': [0, 2, 7, 1, 5, 0, 2], 'target1': [1, 4, 8, 2, 6, 1, 1]} - test.loadData(name='dataset4', source=dataset) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + shuffle_data=False, + lr=1, + num_of_epochs=1, + train_batch_size=1, + ) + self.assertListEqual([[-273.0]], test.parameters["W"]) + self.assertListEqual([-137.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + + dataset = {"in1": [0, 2, 7, 1, 5, 0, 2], "target1": [1, 4, 8, 2, 6, 1, 1]} + test.loadData(name="dataset4", source=dataset) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset4', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, - train_batch_size=2, prediction_samples=3, connect={'inout': 'out1'}) - self.assertListEqual([[-162.75]], test.parameters['W']) - self.assertListEqual([-15.75], test.parameters['b']) - self.assertListEqual([[-162.75]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset4", + optimizer="SGD", + shuffle_data=False, + lr=1, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=3, + connect={"inout": "out1"}, + ) + self.assertListEqual([[-162.75]], test.parameters["W"]) + self.assertListEqual([-15.75], test.parameters["b"]) + self.assertListEqual([[-162.75]], test.parameters["a"]) # Because is a connect and the window is 1 the initialization of the state is overwritten by the out1 - test.loadData(name='dataset5', source=dataset | {'inout': [0, 0, 0, 0, 0, 0, 0]}) + test.loadData( + name="dataset5", source=dataset | {"inout": [0, 0, 0, 0, 0, 0, 0]} + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset5', optimizer='SGD', shuffle_data=False, lr=1, num_of_epochs=1, - train_batch_size=2, prediction_samples=3, connect={'inout': 'out1'}) - self.assertListEqual([[-162.75]], test.parameters['W']) - self.assertListEqual([-15.75], test.parameters['b']) - self.assertListEqual([[-162.75]], test.parameters['a']) + test.trainModel( + train_dataset="dataset5", + optimizer="SGD", + shuffle_data=False, + lr=1, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=3, + connect={"inout": "out1"}, + ) + self.assertListEqual([[-162.75]], test.parameters["W"]) + self.assertListEqual([-15.75], test.parameters["b"]) + self.assertListEqual([[-162.75]], test.parameters["a"]) def test_training_values_fir_connect_linear_more_window(self): NeuObj.clearNames() - input1 = Input('in1', dimensions=2) - W = Parameter('W', values=[[-1], [-5]]) - b = Parameter('b', values=1) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=1) lin_out = Linear(W=W, b=b)(input1.sw(2)) - inout = Input('inout') - a = Parameter('a', sw=2, values=[[4], [5]]) - a_big = Parameter('ab', sw=5, values=[[1], [2], [3], [4], [5]]) + inout = Input("inout") + a = Parameter("a", sw=2, values=[[4], [5]]) + a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]]) lin_out.connect(inout) - output1 = Output('out1', lin_out) - output2 = Output('out2', Fir(W=a)(inout.sw(2))) - output3 = Output('out3', Fir(W=a_big)(inout.sw(5))) - output4 = Output('out4', Fir(W=a)(lin_out)) + output1 = Output("out1", lin_out) + output2 = Output("out2", Fir(W=a)(inout.sw(2))) + output3 = Output("out3", Fir(W=a_big)(inout.sw(5))) + output4 = Output("out4", Fir(W=a)(lin_out)) - target = Input('target') + target = Input("target") test = Modely(visualizer=None, seed=42, log_internal=True) - test.addModel('model', [output1, output2, output3, output4]) - test.addMinimize('error2', target.last(), output2) - #test.addMinimize('error3', target.last(), output3) - #test.addMinimize('error4', target.last(), output4) + test.addModel("model", [output1, output2, output3, output4]) + test.addMinimize("error2", target.last(), output2) + # test.addMinimize('error3', target.last(), output3) + # test.addMinimize('error4', target.last(), output4) test.neuralizeModel() # Dataset with only one sample - dataset = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2]], 'target': [3,4,5,1,3]} - self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]], - 'out2': [-96.0, -194.0, -179.0, -125.0], - 'out3': [-96.0, -206.0, -235.0, -239.0], - 'out4': [-96.0, -194.0, -179.0, -125.0] - }, test(dataset)) - test.loadData(name='dataset', source=dataset) + dataset = { + "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2]], + "target": [3, 4, 5, 1, 3], + } + self.assertEqual( + { + "out1": [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]], + "out2": [-96.0, -194.0, -179.0, -125.0], + "out3": [-96.0, -206.0, -235.0, -239.0], + "out4": [-96.0, -194.0, -179.0, -125.0], + }, + test(dataset), + ) + test.loadData(name="dataset", source=dataset) # TODO add and error # dataset = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2]], 'target': [3,4,5,1]} # test.loadData(name='dataset2', source=dataset) - self.assertListEqual([[-1], [-5]], test.parameters['W']) - self.assertEqual([1 ], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0) - self.assertListEqual([[6143], [5627]], test.parameters['W']) - self.assertEqual([2305], test.parameters['b']) - self.assertListEqual([[-3836], [-3323]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertEqual([1], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + ) + self.assertListEqual([[6143], [5627]], test.parameters["W"]) + self.assertEqual([2305], test.parameters["b"]) + self.assertListEqual([[-3836], [-3323]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + ) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=1, + ) # Data set with more samples test.neuralizeModel(clear_model=True) - dataset2 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0]} - test.loadData(name='dataset2', source=dataset2) + dataset2 = { + "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]], + "target": [3, 4, 5, 1, 3, 0, 1, 0], + } + test.loadData(name="dataset2", source=dataset2) self.maxDiff = None - self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0], [-13.0, -30.0], [-30.0, -28.0], [-28.0, 1.0]], - 'out2': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0], - 'out3': [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0], - 'out4': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0] - }, test(dataset2)) + self.assertEqual( + { + "out1": [ + [-4.0, -16.0], + [-16.0, -26.0], + [-26.0, -15.0], + [-15.0, -13.0], + [-13.0, -30.0], + [-30.0, -28.0], + [-28.0, 1.0], + ], + "out2": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0], + "out3": [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0], + "out4": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0], + }, + test(dataset2), + ) # Use a train_batch_size of 4 # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1) # TODO add this test - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertEqual([3142], test.parameters['b']) - self.assertListEqual([[-7682], [-7457.5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertEqual([3142], test.parameters["b"]) + self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertEqual([3142], test.parameters['b']) - self.assertListEqual([[-7682], [-7457.5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=4, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertEqual([3142], test.parameters["b"]) + self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) # Use a small batch but with a prediction sample of 3 = to 4 samples test.neuralizeModel(clear_model=True) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertEqual([3142], test.parameters['b']) - self.assertListEqual([[-7682], [-7457.5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=4, + ) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertEqual([3142], test.parameters["b"]) + self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) # Different minimize - test.removeMinimize('error2') - test.addMinimize('error3', target.last(), output3) + test.removeMinimize("error2") + test.addMinimize("error3", target.last(), output3) test.neuralizeModel(clear_model=True) - self.assertListEqual([[-1], [-5]], test.parameters['W']) - self.assertEqual([1], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertEqual([1], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) # Use a train_batch_size of 4 - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertEqual([3142], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertEqual([3142], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertEqual([3142], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=4, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertEqual([3142], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, shuffle_data=False, num_of_epochs=1, train_batch_size=1, prediction_samples=3) - self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]], test.internals['inout_0_0']['XY']['in1']) - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_0_1']['XY']['in1']) - self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_0_2']['XY']['in1']) - self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]], test.internals['inout_0_3']['XY']['in1']) - self.assertListEqual([[[3.0]]], test.internals['inout_0_0']['XY']['target']) - self.assertListEqual([[[0.0]]], test.internals['inout_0_1']['XY']['target']) - self.assertListEqual([[[1.0]]], test.internals['inout_0_2']['XY']['target']) - self.assertListEqual([[[0.0]]], test.internals['inout_0_3']['XY']['target']) - self.assertDictEqual({'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]}, test.internals['inout_0_0']['out']) - self.assertDictEqual({'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]}, test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[4*-1+2*-5+1.0], [6*-1+5*-5+1.0]]], - 'out2': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]], - 'out3': [[[(-15)*3.0+(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]], - 'out4': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]]}, test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[6*-1+5*-5+1.0], [4*-1+5*-5+1.0]]], - 'out2': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]], - 'out3': [[[(-15)*2.0+(4*-1+2*-5+1.0)*3.0+(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]], - 'out4': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]]}, test.internals['inout_0_2']['out']) - self.assertDictEqual({'out1': [[[4*-1+5*-5+1.0], [0*-1+0*-5+1.0]]], - 'out2': [[[(4*-1+5*-5+1.0)*4.0+(0*-1+0*-5+1.0)*5.0]]], - 'out3': [[[(-15)*1.0+(4*-1+2*-5+1.0)*2.0+(6*-1+5*-5+1.0)*3.0+(4*-1+5*-5+1.0)*4.0+(0*-1+0*-5+1.0)*5.0]]], - 'out4': [[[(4*-1+5*-5+1.0)*4.0+(0*-1+0*-5+1.0)*5.0]]]}, test.internals['inout_0_3']['out']) - self.assertDictEqual({'inout': [[[0.0],[0.0], [-15.0], [-13.0], [np.inf]]]}, test.internals['inout_0_0']['state']) - self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, test.internals['inout_0_1']['state']) - self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, test.internals['inout_0_2']['state']) - self.assertDictEqual({'inout': [[[-13.0], [-30.0], [-28.0], [1.0], [np.inf]]]}, test.internals['inout_0_3']['state']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + shuffle_data=False, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + ) + self.assertListEqual( + [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"] + ) + self.assertListEqual( + [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_0_3"]["XY"]["in1"] + ) + self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"]) + self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_0_3"]["XY"]["target"]) + self.assertDictEqual( + { + "out1": [[[-15.0], [-13.0]]], + "out2": [[[-125.0]]], + "out3": [[[-125.0]]], + "out4": [[[-125.0]]], + }, + test.internals["inout_0_0"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[-13.0], [-30.0]]], + "out2": [[[-202.0]]], + "out3": [[[-247.0]]], + "out4": [[[-202.0]]], + }, + test.internals["inout_0_1"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]], + "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + "out3": [ + [ + [ + (-15) * 3.0 + + (4 * -1 + 2 * -5 + 1.0) * 4 + + (6 * -1 + 5 * -5 + 1.0) * 5 + ] + ] + ], + "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + }, + test.internals["inout_0_1"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]], + "out2": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + "out3": [ + [ + [ + (-15) * 2.0 + + (4 * -1 + 2 * -5 + 1.0) * 3.0 + + (6 * -1 + 5 * -5 + 1.0) * 4.0 + + (4 * -1 + 5 * -5 + 1.0) * 5.0 + ] + ] + ], + "out4": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + }, + test.internals["inout_0_2"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[4 * -1 + 5 * -5 + 1.0], [0 * -1 + 0 * -5 + 1.0]]], + "out2": [ + [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]] + ], + "out3": [ + [ + [ + (-15) * 1.0 + + (4 * -1 + 2 * -5 + 1.0) * 2.0 + + (6 * -1 + 5 * -5 + 1.0) * 3.0 + + (4 * -1 + 5 * -5 + 1.0) * 4.0 + + (0 * -1 + 0 * -5 + 1.0) * 5.0 + ] + ] + ], + "out4": [ + [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]] + ], + }, + test.internals["inout_0_3"]["out"], + ) + self.assertDictEqual( + {"inout": [[[0.0], [0.0], [-15.0], [-13.0], [np.inf]]]}, + test.internals["inout_0_0"]["state"], + ) + self.assertDictEqual( + {"inout": [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, + test.internals["inout_0_1"]["state"], + ) + self.assertDictEqual( + {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, + test.internals["inout_0_2"]["state"], + ) + self.assertDictEqual( + {"inout": [[[-13.0], [-30.0], [-28.0], [1.0], [np.inf]]]}, + test.internals["inout_0_3"]["state"], + ) # Replace instead of roll # self.assertDictEqual({'inout': [[[0.0], [0.0], [-15.0], [-13.0], [0.0]]]}, test.internals['inout_0_0']['state']) # self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [0.0]]]}, test.internals['inout_0_1']['state']) # self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [0.0]]]}, test.internals['inout_0_2']['state']) # self.assertDictEqual({'inout': [[[-13.0], [-30.0], [-28.0], [1.0], [-15.0]]]}, test.internals['inout_0_3']['state']) - self.assertListEqual([[22273.5], [20993.0]], test.parameters['W']) - self.assertEqual([6154.0], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]], test.parameters['ab']) + self.assertListEqual([[22273.5], [20993.0]], test.parameters["W"]) + self.assertEqual([6154.0], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual( + [[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]], + test.parameters["ab"], + ) with self.assertRaises(KeyError): - test.internals['inout_1_0'] + test.internals["inout_1_0"] test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, train_batch_size=1, - prediction_samples=2) - self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]], test.internals['inout_0_0']['XY']['in1']) - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_0_1']['XY']['in1']) - self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_0_2']['XY']['in1']) - self.assertListEqual([[[3.0]]], test.internals['inout_0_0']['XY']['target']) - self.assertListEqual([[[0.0]]], test.internals['inout_0_1']['XY']['target']) - self.assertListEqual([[[1.0]]], test.internals['inout_0_2']['XY']['target']) - self.assertDictEqual({'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]}, test.internals['inout_0_0']['out']) - self.assertDictEqual({'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]}, test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[4*-1+2*-5+1.0], [6*-1+5*-5+1.0]]], - 'out2': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]], - 'out3': [[[(-15)*3.0+(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]], - 'out4': [[[(4*-1+2*-5+1.0)*4+(6*-1+5*-5+1.0)*5]]]}, test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[6*-1+5*-5+1.0], [4*-1+5*-5+1.0]]], - 'out2': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]], - 'out3': [[[(-15)*2.0+(4*-1+2*-5+1.0)*3.0+(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]], - 'out4': [[[(6*-1+5*-5+1.0)*4.0+(4*-1+5*-5+1.0)*5.0]]]}, test.internals['inout_0_2']['out']) - self.assertDictEqual({'inout': [[[0.0], [0.0], [-15.0], [-13.0], [np.inf]]]}, test.internals['inout_0_0']['state']) - self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, test.internals['inout_0_1']['state']) - self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, test.internals['inout_0_2']['state']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=0.01, + shuffle_data=False, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=2, + ) + self.assertListEqual( + [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"] + ) + self.assertListEqual( + [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"] + ) + self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"]) + self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"]) + self.assertDictEqual( + { + "out1": [[[-15.0], [-13.0]]], + "out2": [[[-125.0]]], + "out3": [[[-125.0]]], + "out4": [[[-125.0]]], + }, + test.internals["inout_0_0"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[-13.0], [-30.0]]], + "out2": [[[-202.0]]], + "out3": [[[-247.0]]], + "out4": [[[-202.0]]], + }, + test.internals["inout_0_1"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]], + "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + "out3": [ + [ + [ + (-15) * 3.0 + + (4 * -1 + 2 * -5 + 1.0) * 4 + + (6 * -1 + 5 * -5 + 1.0) * 5 + ] + ] + ], + "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + }, + test.internals["inout_0_1"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]], + "out2": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + "out3": [ + [ + [ + (-15) * 2.0 + + (4 * -1 + 2 * -5 + 1.0) * 3.0 + + (6 * -1 + 5 * -5 + 1.0) * 4.0 + + (4 * -1 + 5 * -5 + 1.0) * 5.0 + ] + ] + ], + "out4": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + }, + test.internals["inout_0_2"]["out"], + ) + self.assertDictEqual( + {"inout": [[[0.0], [0.0], [-15.0], [-13.0], [np.inf]]]}, + test.internals["inout_0_0"]["state"], + ) + self.assertDictEqual( + {"inout": [[[0.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, + test.internals["inout_0_1"]["state"], + ) + self.assertDictEqual( + {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, + test.internals["inout_0_2"]["state"], + ) # Replace instead of rolling # self.assertDictEqual({'inout': [[[0.0], [0.0], [-15.0], [-13.0], [0.0]]]}, test.internals['inout_0_0']['state']) # self.assertDictEqual({'inout': [[[0.0], [-15.0], [-13.0], [-30.0], [0.0]]]}, test.internals['inout_0_1']['state']) # self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [0.0]]]}, test.internals['inout_0_2']['state']) - W = test.internals['inout_1_0']['param']['W'] - b = test.internals['inout_1_0']['param']['b'] - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_1_0']['XY']['in1']) - self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_1_1']['XY']['in1']) - self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]], test.internals['inout_1_2']['XY']['in1']) - self.assertListEqual([[[0.0]]], test.internals['inout_1_0']['XY']['target']) - self.assertListEqual([[[1.0]]], test.internals['inout_1_1']['XY']['target']) - self.assertListEqual([[[0.0]]], test.internals['inout_1_2']['XY']['target']) - self.assertAlmostEqual({'inout': [[[0.0], [0.0], [W[0][0]*4.0+W[1][0]*2.0+b[0]], [W[0][0]*6.0+W[1][0]*5.0+b[0]], [np.inf]]]}, test.internals['inout_1_0']['state']) - self.assertAlmostEqual({'inout': [[[0.0], [W[0][0]*4.0+W[1][0]*2.0+b[0]], [W[0][0]*6.0+W[1][0]*5.0+b[0]], [W[0][0]*4.0+W[1][0]*5.0+b[0]], [np.inf]]]}, test.internals['inout_1_1']['state']) - self.assertAlmostEqual({'inout': [[[W[0][0]*4.0+W[1][0]*2.0+b[0]], [W[0][0]*6.0+W[1][0]*5.0+b[0]], [W[0][0]*4.0+W[1][0]*5.0+b[0]], [W[0][0]*0.0+W[1][0]*0.0+b[0]], [np.inf]]]}, test.internals['inout_1_2']['state']) + W = test.internals["inout_1_0"]["param"]["W"] + b = test.internals["inout_1_0"]["param"]["b"] + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_1_0"]["XY"]["in1"] + ) + self.assertListEqual( + [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_1_1"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_1_2"]["XY"]["in1"] + ) + self.assertListEqual([[[0.0]]], test.internals["inout_1_0"]["XY"]["target"]) + self.assertListEqual([[[1.0]]], test.internals["inout_1_1"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_1_2"]["XY"]["target"]) + self.assertAlmostEqual( + { + "inout": [ + [ + [0.0], + [0.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [np.inf], + ] + ] + }, + test.internals["inout_1_0"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [0.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [np.inf], + ] + ] + }, + test.internals["inout_1_1"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], + [np.inf], + ] + ] + }, + test.internals["inout_1_2"]["state"], + ) # Replace instead of rolling # self.assertAlmostEqual({'inout': [[[0.0], [0.0], [W[0][0][0] * 4.0 + W[0][1][0] * 2.0 + b[0][0]], # [W[0][0][0] * 6.0 + W[0][1][0] * 5.0 + b[0][0]], [0.0]]]}, @@ -722,43 +1373,111 @@ def test_training_values_fir_connect_linear_more_window(self): # test.internals['inout_1_2']['state']) with self.assertRaises(KeyError): - test.internals['inout_2_0'] + test.internals["inout_2_0"] test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, train_batch_size=2, prediction_samples=1) - self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]], test.internals['inout_0_0']['XY']['in1']) - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]], test.internals['inout_0_1']['XY']['in1']) - self.assertListEqual([[[3.0]],[[0.0]]], test.internals['inout_0_0']['XY']['target']) - self.assertListEqual([[[0.0]],[[1.0]]], test.internals['inout_0_1']['XY']['target']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=0.01, + shuffle_data=False, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=1, + ) + self.assertListEqual( + [[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]], + test.internals["inout_0_0"]["XY"]["in1"], + ) + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]], + test.internals["inout_0_1"]["XY"]["in1"], + ) + self.assertListEqual( + [[[3.0]], [[0.0]]], test.internals["inout_0_0"]["XY"]["target"] + ) + self.assertListEqual( + [[[0.0]], [[1.0]]], test.internals["inout_0_1"]["XY"]["target"] + ) with self.assertRaises(KeyError): - test.internals['inout_1_0'] + test.internals["inout_1_0"] test.neuralizeModel(clear_model=True) - dataset3 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0], 'inout':[9,8,7,6,5,4,3,2]} - test.loadData(name='dataset3', source=dataset3) - test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, - train_batch_size=1, - prediction_samples=2) - self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [np.inf]]]}, test.internals['inout_0_0']['state']) - self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, test.internals['inout_0_1']['state']) - self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, test.internals['inout_0_2']['state']) + dataset3 = { + "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]], + "target": [3, 4, 5, 1, 3, 0, 1, 0], + "inout": [9, 8, 7, 6, 5, 4, 3, 2], + } + test.loadData(name="dataset3", source=dataset3) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + lr=0.01, + shuffle_data=False, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=2, + ) + self.assertDictEqual( + {"inout": [[[8.0], [7.0], [-15.0], [-13.0], [np.inf]]]}, + test.internals["inout_0_0"]["state"], + ) + self.assertDictEqual( + {"inout": [[[7.0], [-15.0], [-13.0], [-30.0], [np.inf]]]}, + test.internals["inout_0_1"]["state"], + ) + self.assertDictEqual( + {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [np.inf]]]}, + test.internals["inout_0_2"]["state"], + ) # Replace insead of rolling # self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [9.0]]]}, test.internals['inout_0_0']['state']) # self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [8.0]]]}, test.internals['inout_0_1']['state']) # self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [7.0]]]}, test.internals['inout_0_2']['state']) - W = test.internals['inout_1_0']['param']['W'] - b = test.internals['inout_1_0']['param']['b'] - self.assertAlmostEqual({'inout': [[[7.0], [6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], - [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], [np.inf]]]}, - test.internals['inout_1_0']['state']) - self.assertAlmostEqual({'inout': [[[6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], - [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], - [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [np.inf]]]}, - test.internals['inout_1_1']['state']) - self.assertAlmostEqual({'inout': [ - [[W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], - [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], [np.inf]]]}, - test.internals['inout_1_2']['state']) + W = test.internals["inout_1_0"]["param"]["W"] + b = test.internals["inout_1_0"]["param"]["b"] + self.assertAlmostEqual( + { + "inout": [ + [ + [7.0], + [6.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [np.inf], + ] + ] + }, + test.internals["inout_1_0"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [6.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [np.inf], + ] + ] + }, + test.internals["inout_1_1"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], + [np.inf], + ] + ] + }, + test.internals["inout_1_2"]["state"], + ) # replace insead of rolling # self.assertAlmostEqual({'inout': [[[7.0], [6.0], [W[0][0][0] * 4.0 + W[0][1][0] * 2.0 + b[0][0]], # [W[0][0][0] * 6.0 + W[0][1][0] * 5.0 + b[0][0]], [8.0]]]}, @@ -774,184 +1493,411 @@ def test_training_values_fir_connect_linear_more_window(self): def test_training_values_fir_connect_train_linear_more_window(self): NeuObj.clearNames() - input1 = Input('in1', dimensions=2) - W = Parameter('W', values=[[-1], [-5]]) - b = Parameter('b', values=[1]) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=[1]) lin_out = Linear(W=W, b=b)(input1.sw(2)) - output1 = Output('out1', lin_out) + output1 = Output("out1", lin_out) - inout = Input('inout') - a = Parameter('a', sw=2, values=[[4], [5]]) - a_big = Parameter('ab', sw=5, values=[[1], [2], [3], [4], [5]]) - output2 = Output('out2', Fir(W=a)(inout.sw(2))) - output3 = Output('out3', Fir(W=a_big)(inout.sw(5))) - output4 = Output('out4', Fir(W=a)(lin_out)) + inout = Input("inout") + a = Parameter("a", sw=2, values=[[4], [5]]) + a_big = Parameter("ab", sw=5, values=[[1], [2], [3], [4], [5]]) + output2 = Output("out2", Fir(W=a)(inout.sw(2))) + output3 = Output("out3", Fir(W=a_big)(inout.sw(5))) + output4 = Output("out4", Fir(W=a)(lin_out)) - target = Input('target') + target = Input("target") test = Modely(visualizer=None, seed=42, log_internal=True) - test.addModel('model', [output1, output2, output3, output4]) - test.addMinimize('error2', target.last(), output2) + test.addModel("model", [output1, output2, output3, output4]) + test.addMinimize("error2", target.last(), output2) test.neuralizeModel() # Dataset with only one sample - dataset = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2]], 'target': [3,4,5,1,3]} - self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]], - 'out2': [-96.0, -194.0, -179.0, -125.0], - 'out3': [-96.0, -206.0, -235.0, -239.0], - 'out4': [-96.0, -194.0, -179.0, -125.0] - }, test(dataset, connect={'inout':'out1'})) - test.loadData(name='dataset', source=dataset) - - self.assertListEqual([[-1], [-5]], test.parameters['W']) - self.assertListEqual([1], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout':'out1'}) - self.assertListEqual([[6143], [5627]], test.parameters['W']) - self.assertListEqual([2305], test.parameters['b']) - self.assertListEqual([[-3836], [-3323]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + dataset = { + "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2]], + "target": [3, 4, 5, 1, 3], + } + self.assertEqual( + { + "out1": [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0]], + "out2": [-96.0, -194.0, -179.0, -125.0], + "out3": [-96.0, -206.0, -235.0, -239.0], + "out4": [-96.0, -194.0, -179.0, -125.0], + }, + test(dataset, connect={"inout": "out1"}), + ) + test.loadData(name="dataset", source=dataset) + + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertListEqual([1], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + connect={"inout": "out1"}, + ) + self.assertListEqual([[6143], [5627]], test.parameters["W"]) + self.assertListEqual([2305], test.parameters["b"]) + self.assertListEqual([[-3836], [-3323]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) with self.assertRaises(KeyError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + ) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10, connect={'inout':'out1'}) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + connect={"inout": "out1"}, + ) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, connect={'inout':'out1'}) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=1, + connect={"inout": "out1"}, + ) # Data set with more samples test.neuralizeModel(clear_model=True) - dataset2 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0]} - test.loadData(name='dataset2', source=dataset2) + dataset2 = { + "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]], + "target": [3, 4, 5, 1, 3, 0, 1, 0], + } + test.loadData(name="dataset2", source=dataset2) self.maxDiff = None - self.assertEqual({'out1': [[-4.0, -16.0], [-16.0, -26.0], [-26.0, -15.0], [-15.0, -13.0], [-13.0, -30.0], [-30.0, -28.0], [-28.0, 1.0]], - 'out2': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0], - 'out3': [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0], - 'out4': [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0] - }, test(dataset2, connect={'inout':'out1'})) + self.assertEqual( + { + "out1": [ + [-4.0, -16.0], + [-16.0, -26.0], + [-26.0, -15.0], + [-15.0, -13.0], + [-13.0, -30.0], + [-30.0, -28.0], + [-28.0, 1.0], + ], + "out2": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0], + "out3": [-96.0, -206.0, -235.0, -239.0, -315.0, -355.0, -238.0], + "out4": [-96.0, -194.0, -179.0, -125.0, -202.0, -260.0, -107.0], + }, + test(dataset2, connect={"inout": "out1"}), + ) # Use a train_batch_size of 4 # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, connect={'inout':'out1'}) TODO add this test - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout':'out1'}) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertListEqual([3142], test.parameters['b']) - self.assertListEqual([[-7682], [-7457.5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + connect={"inout": "out1"}, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertListEqual([3142], test.parameters["b"]) + self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4, connect={'inout':'out1'}) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertListEqual([3142], test.parameters['b']) - self.assertListEqual([[-7682], [-7457.5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=4, + connect={"inout": "out1"}, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertListEqual([3142], test.parameters["b"]) + self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) # Use a small batch but with a prediction sample of 3 = to 4 samples test.neuralizeModel(clear_model=True) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4, connect={'inout':'out1'}) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout':'out1'}) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertListEqual([3142], test.parameters['b']) - self.assertListEqual([[-7682], [-7457.5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=4, + connect={"inout": "out1"}, + ) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + connect={"inout": "out1"}, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertListEqual([3142], test.parameters["b"]) + self.assertListEqual([[-7682], [-7457.5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) # Different minimize - test.removeMinimize('error2') - test.addMinimize('error3', target.last(), output3) + test.removeMinimize("error2") + test.addMinimize("error3", target.last(), output3) test.neuralizeModel(clear_model=True) - self.assertListEqual([[-1], [-5]], test.parameters['W']) - self.assertListEqual([1], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters['ab']) + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertListEqual([1], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [4], [5]], test.parameters["ab"]) # Use a train_batch_size of 4 - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, connect={'inout':'out1'}) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertListEqual([3142], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + connect={"inout": "out1"}, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertListEqual([3142], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4, connect={'inout':'out1'}) - self.assertListEqual([[12779], [11678.5]], test.parameters['W']) - self.assertListEqual([3142], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters['ab']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=4, + connect={"inout": "out1"}, + ) + self.assertListEqual([[12779], [11678.5]], test.parameters["W"]) + self.assertListEqual([3142], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual([[1], [2], [3], [-7682], [-7457.5]], test.parameters["ab"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, connect={'inout':'out1'}) - self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]],test.internals['inout_0_0']['XY']['in1']) - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_0_1']['XY']['in1']) - self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_0_2']['XY']['in1']) - self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]],test.internals['inout_0_3']['XY']['in1']) - self.assertListEqual([[[3.0]]],test.internals['inout_0_0']['XY']['target']) - self.assertListEqual([[[0.0]]],test.internals['inout_0_1']['XY']['target']) - self.assertListEqual([[[1.0]]],test.internals['inout_0_2']['XY']['target']) - self.assertListEqual([[[0.0]]],test.internals['inout_0_3']['XY']['target']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + connect={"inout": "out1"}, + ) + self.assertListEqual( + [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"] + ) + self.assertListEqual( + [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_0_3"]["XY"]["in1"] + ) + self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"]) + self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_0_3"]["XY"]["target"]) + self.assertDictEqual( + { + "out1": [[[-15.0], [-13.0]]], + "out2": [[[-125.0]]], + "out3": [[[-125.0]]], + "out4": [[[-125.0]]], + }, + test.internals["inout_0_0"]["out"], + ) self.assertDictEqual( - {'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]}, - test.internals['inout_0_0']['out']) + { + "out1": [[[-13.0], [-30.0]]], + "out2": [[[-202.0]]], + "out3": [[[-247.0]]], + "out4": [[[-202.0]]], + }, + test.internals["inout_0_1"]["out"], + ) self.assertDictEqual( - {'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]}, - test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]], - 'out2': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], - 'out3': [[[(-15) * 3.0 + (4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], - 'out4': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]]}, - test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]], - 'out2': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]], - 'out3': [[[(-15) * 2.0 + (4 * -1 + 2 * -5 + 1.0) * 3.0 + (6 * -1 + 5 * -5 + 1.0) * 4.0 + ( - 4 * -1 + 5 * -5 + 1.0) * 5.0]]], - 'out4': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]]}, - test.internals['inout_0_2']['out']) - self.assertDictEqual({'out1': [[[4 * -1 + 5 * -5 + 1.0], [0 * -1 + 0 * -5 + 1.0]]], - 'out2': [[[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]], - 'out3': [[[(-15) * 1.0 + (4 * -1 + 2 * -5 + 1.0) * 2.0 + (6 * -1 + 5 * -5 + 1.0) * 3.0 + ( - 4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]], - 'out4': [[[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]]]}, - test.internals['inout_0_3']['out']) - self.assertListEqual([[22273.5], [20993.0]], test.parameters['W']) - self.assertListEqual([6154.0], test.parameters['b']) - self.assertListEqual([[4], [5]], test.parameters['a']) - self.assertListEqual([[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]], - test.parameters['ab']) + { + "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]], + "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + "out3": [ + [ + [ + (-15) * 3.0 + + (4 * -1 + 2 * -5 + 1.0) * 4 + + (6 * -1 + 5 * -5 + 1.0) * 5 + ] + ] + ], + "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + }, + test.internals["inout_0_1"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]], + "out2": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + "out3": [ + [ + [ + (-15) * 2.0 + + (4 * -1 + 2 * -5 + 1.0) * 3.0 + + (6 * -1 + 5 * -5 + 1.0) * 4.0 + + (4 * -1 + 5 * -5 + 1.0) * 5.0 + ] + ] + ], + "out4": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + }, + test.internals["inout_0_2"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[4 * -1 + 5 * -5 + 1.0], [0 * -1 + 0 * -5 + 1.0]]], + "out2": [ + [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]] + ], + "out3": [ + [ + [ + (-15) * 1.0 + + (4 * -1 + 2 * -5 + 1.0) * 2.0 + + (6 * -1 + 5 * -5 + 1.0) * 3.0 + + (4 * -1 + 5 * -5 + 1.0) * 4.0 + + (0 * -1 + 0 * -5 + 1.0) * 5.0 + ] + ] + ], + "out4": [ + [[(4 * -1 + 5 * -5 + 1.0) * 4.0 + (0 * -1 + 0 * -5 + 1.0) * 5.0]] + ], + }, + test.internals["inout_0_3"]["out"], + ) + self.assertListEqual([[22273.5], [20993.0]], test.parameters["W"]) + self.assertListEqual([6154.0], test.parameters["b"]) + self.assertListEqual([[4], [5]], test.parameters["a"]) + self.assertListEqual( + [[-1784.0], [-4020.0], [-7564.5], [-10843.5], [-9033.0]], + test.parameters["ab"], + ) with self.assertRaises(KeyError): - test.internals['inout_1_0'] + test.internals["inout_1_0"] test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, - train_batch_size=1, - prediction_samples=2, connect={'inout':'out1'}) - self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]]],test.internals['inout_0_0']['XY']['in1']) - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_0_1']['XY']['in1']) - self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_0_2']['XY']['in1']) - self.assertListEqual([[[3.0]]],test.internals['inout_0_0']['XY']['target']) - self.assertListEqual([[[0.0]]],test.internals['inout_0_1']['XY']['target']) - self.assertListEqual([[[1.0]]],test.internals['inout_0_2']['XY']['target']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=0.01, + shuffle_data=False, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=2, + connect={"inout": "out1"}, + ) + self.assertListEqual( + [[[1.0, 3.0], [4.0, 2.0]]], test.internals["inout_0_0"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_0_1"]["XY"]["in1"] + ) + self.assertListEqual( + [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_0_2"]["XY"]["in1"] + ) + self.assertListEqual([[[3.0]]], test.internals["inout_0_0"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_0_1"]["XY"]["target"]) + self.assertListEqual([[[1.0]]], test.internals["inout_0_2"]["XY"]["target"]) + self.assertDictEqual( + { + "out1": [[[-15.0], [-13.0]]], + "out2": [[[-125.0]]], + "out3": [[[-125.0]]], + "out4": [[[-125.0]]], + }, + test.internals["inout_0_0"]["out"], + ) self.assertDictEqual( - {'out1': [[[-15.0], [-13.0]]], 'out2': [[[-125.0]]], 'out3': [[[-125.0]]], 'out4': [[[-125.0]]]}, - test.internals['inout_0_0']['out']) + { + "out1": [[[-13.0], [-30.0]]], + "out2": [[[-202.0]]], + "out3": [[[-247.0]]], + "out4": [[[-202.0]]], + }, + test.internals["inout_0_1"]["out"], + ) self.assertDictEqual( - {'out1': [[[-13.0], [-30.0]]], 'out2': [[[-202.0]]], 'out3': [[[-247.0]]], 'out4': [[[-202.0]]]}, - test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]], - 'out2': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], - 'out3': [[[(-15) * 3.0 + (4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], - 'out4': [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]]}, - test.internals['inout_0_1']['out']) - self.assertDictEqual({'out1': [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]], - 'out2': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]], - 'out3': [[[(-15) * 2.0 + (4 * -1 + 2 * -5 + 1.0) * 3.0 + (6 * -1 + 5 * -5 + 1.0) * 4.0 + ( - 4 * -1 + 5 * -5 + 1.0) * 5.0]]], - 'out4': [[[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]]]}, - test.internals['inout_0_2']['out']) - W = test.internals['inout_1_0']['param']['W'] - b = test.internals['inout_1_0']['param']['b'] - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_1_0']['XY']['in1']) - self.assertListEqual([[[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_1_1']['XY']['in1']) - self.assertListEqual([[[4.0, 5.0], [0.0, 0.0]]],test.internals['inout_1_2']['XY']['in1']) - self.assertListEqual([[[0.0]]],test.internals['inout_1_0']['XY']['target']) - self.assertListEqual([[[1.0]]],test.internals['inout_1_1']['XY']['target']) - self.assertListEqual([[[0.0]]],test.internals['inout_1_2']['XY']['target']) + { + "out1": [[[4 * -1 + 2 * -5 + 1.0], [6 * -1 + 5 * -5 + 1.0]]], + "out2": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + "out3": [ + [ + [ + (-15) * 3.0 + + (4 * -1 + 2 * -5 + 1.0) * 4 + + (6 * -1 + 5 * -5 + 1.0) * 5 + ] + ] + ], + "out4": [[[(4 * -1 + 2 * -5 + 1.0) * 4 + (6 * -1 + 5 * -5 + 1.0) * 5]]], + }, + test.internals["inout_0_1"]["out"], + ) + self.assertDictEqual( + { + "out1": [[[6 * -1 + 5 * -5 + 1.0], [4 * -1 + 5 * -5 + 1.0]]], + "out2": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + "out3": [ + [ + [ + (-15) * 2.0 + + (4 * -1 + 2 * -5 + 1.0) * 3.0 + + (6 * -1 + 5 * -5 + 1.0) * 4.0 + + (4 * -1 + 5 * -5 + 1.0) * 5.0 + ] + ] + ], + "out4": [ + [[(6 * -1 + 5 * -5 + 1.0) * 4.0 + (4 * -1 + 5 * -5 + 1.0) * 5.0]] + ], + }, + test.internals["inout_0_2"]["out"], + ) + W = test.internals["inout_1_0"]["param"]["W"] + b = test.internals["inout_1_0"]["param"]["b"] + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]]], test.internals["inout_1_0"]["XY"]["in1"] + ) + self.assertListEqual( + [[[6.0, 5.0], [4.0, 5.0]]], test.internals["inout_1_1"]["XY"]["in1"] + ) + self.assertListEqual( + [[[4.0, 5.0], [0.0, 0.0]]], test.internals["inout_1_2"]["XY"]["in1"] + ) + self.assertListEqual([[[0.0]]], test.internals["inout_1_0"]["XY"]["target"]) + self.assertListEqual([[[1.0]]], test.internals["inout_1_1"]["XY"]["target"]) + self.assertListEqual([[[0.0]]], test.internals["inout_1_2"]["XY"]["target"]) # self.assertAlmostEqual({'inout': [[[0.0], [0.0], [0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], # [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]]]]}, # test.internals['inout_1_0']['state']) @@ -963,48 +1909,117 @@ def test_training_values_fir_connect_train_linear_more_window(self): # [[0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], # [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]]]]}, # test.internals['inout_1_2']['state']) - self.assertAlmostEqual({'inout': [[[0.0], [0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], - [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]}, - test.internals['inout_1_0']['state']) - self.assertAlmostEqual({'inout': [[[0.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], - [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], - [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]}, - test.internals['inout_1_1']['state']) - self.assertAlmostEqual({'inout': [ - [[W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], - [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], [float('inf')]]]}, - test.internals['inout_1_2']['state']) + self.assertAlmostEqual( + { + "inout": [ + [ + [0.0], + [0.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [float("inf")], + ] + ] + }, + test.internals["inout_1_0"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [0.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [float("inf")], + ] + ] + }, + test.internals["inout_1_1"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], + [float("inf")], + ] + ] + }, + test.internals["inout_1_2"]["state"], + ) with self.assertRaises(KeyError): - test.internals['inout_2_0'] + test.internals["inout_2_0"] test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, - train_batch_size=2, prediction_samples=1, connect={'inout':'out1'}) - self.assertListEqual([[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]],test.internals['inout_0_0']['XY']['in1']) - self.assertListEqual([[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]],test.internals['inout_0_1']['XY']['in1']) - self.assertListEqual([[[3.0]], [[0.0]]],test.internals['inout_0_0']['XY']['target']) - self.assertListEqual([[[0.0]], [[1.0]]],test.internals['inout_0_1']['XY']['target']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=0.01, + shuffle_data=False, + num_of_epochs=1, + train_batch_size=2, + prediction_samples=1, + connect={"inout": "out1"}, + ) + self.assertListEqual( + [[[1.0, 3.0], [4.0, 2.0]], [[4.0, 2.0], [6.0, 5.0]]], + test.internals["inout_0_0"]["XY"]["in1"], + ) + self.assertListEqual( + [[[4.0, 2.0], [6.0, 5.0]], [[6.0, 5.0], [4.0, 5.0]]], + test.internals["inout_0_1"]["XY"]["in1"], + ) + self.assertListEqual( + [[[3.0]], [[0.0]]], test.internals["inout_0_0"]["XY"]["target"] + ) + self.assertListEqual( + [[[0.0]], [[1.0]]], test.internals["inout_0_1"]["XY"]["target"] + ) with self.assertRaises(KeyError): - test.internals['inout_1_0'] + test.internals["inout_1_0"] test.neuralizeModel(clear_model=True) - dataset3 = {'in1': [[0,1],[2,3],[7,4],[1,3],[4,2],[6,5],[4,5],[0,0]], 'target': [3,4,5,1,3,0,1,0], 'inout':[9,8,7,6,5,4,3,2]} - test.loadData(name='dataset3', source=dataset3) - test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=0.01, shuffle_data=False, num_of_epochs=1, - train_batch_size=1, - prediction_samples=2, connect={'inout':'out1'}) + dataset3 = { + "in1": [[0, 1], [2, 3], [7, 4], [1, 3], [4, 2], [6, 5], [4, 5], [0, 0]], + "target": [3, 4, 5, 1, 3, 0, 1, 0], + "inout": [9, 8, 7, 6, 5, 4, 3, 2], + } + test.loadData(name="dataset3", source=dataset3) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + lr=0.01, + shuffle_data=False, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=2, + connect={"inout": "out1"}, + ) # self.assertDictEqual({'inout': [[[8.0], [7.0], [6.0], [-15.0], [-13.0]]]}, test.internals['inout_0_0']['state']) # self.assertDictEqual({'inout': [[[7.0], [6.0], [-15.0], [-13.0], [-30.0]]]}, test.internals['inout_0_1']['state']) # self.assertDictEqual({'inout': [[[6.0], [-15.0], [-13.0], [-30.0], [-28.0]]]}, test.internals['inout_0_2']['state']) - self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [float('inf')]]]}, test.internals['inout_0_0']['state']) - self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [float('inf')]]]}, test.internals['inout_0_1']['state']) - self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [float('inf')]]]}, test.internals['inout_0_2']['state']) + self.assertDictEqual( + {"inout": [[[8.0], [7.0], [-15.0], [-13.0], [float("inf")]]]}, + test.internals["inout_0_0"]["state"], + ) + self.assertDictEqual( + {"inout": [[[7.0], [-15.0], [-13.0], [-30.0], [float("inf")]]]}, + test.internals["inout_0_1"]["state"], + ) + self.assertDictEqual( + {"inout": [[[-15.0], [-13.0], [-30.0], [-28.0], [float("inf")]]]}, + test.internals["inout_0_2"]["state"], + ) # Replace instead of rolling # self.assertDictEqual({'inout': [[[8.0], [7.0], [-15.0], [-13.0], [9.0]]]}, test.internals['inout_0_0']['connect']) # self.assertDictEqual({'inout': [[[7.0], [-15.0], [-13.0], [-30.0], [8.0]]]}, test.internals['inout_0_1']['connect']) # self.assertDictEqual({'inout': [[[-15.0], [-13.0], [-30.0], [-28.0], [7.0]]]}, test.internals['inout_0_2']['connect']) - W = test.internals['inout_1_0']['param']['W'] - b = test.internals['inout_1_0']['param']['b'] + W = test.internals["inout_1_0"]["param"]["W"] + b = test.internals["inout_1_0"]["param"]["b"] # self.assertAlmostEqual({'inout': [[[7.0], [6.0], [5.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], # [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]]]]}, # test.internals['inout_1_0']['state']) @@ -1017,249 +2032,396 @@ def test_training_values_fir_connect_train_linear_more_window(self): # [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]]]]}, # test.internals['inout_1_2']['state']) # Replace insead of rolling - self.assertAlmostEqual({'inout': [[[7.0], [6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], - [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]}, - test.internals['inout_1_0']['state']) - self.assertAlmostEqual({'inout': [[[6.0], [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], - [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], - [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [float('inf')]]]}, - test.internals['inout_1_1']['state']) - self.assertAlmostEqual({'inout': [ - [[W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], - [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], [float('inf')]]]}, - test.internals['inout_1_2']['state']) + self.assertAlmostEqual( + { + "inout": [ + [ + [7.0], + [6.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [float("inf")], + ] + ] + }, + test.internals["inout_1_0"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [6.0], + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [float("inf")], + ] + ] + }, + test.internals["inout_1_1"]["state"], + ) + self.assertAlmostEqual( + { + "inout": [ + [ + [W[0][0] * 4.0 + W[1][0] * 2.0 + b[0]], + [W[0][0] * 6.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 4.0 + W[1][0] * 5.0 + b[0]], + [W[0][0] * 0.0 + W[1][0] * 0.0 + b[0]], + [float("inf")], + ] + ] + }, + test.internals["inout_1_2"]["state"], + ) def test_training_values_fir_and_liner_closed_loop(self): NeuObj.clearNames() - input1 = Input('in1') - target_out1 = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) + input1 = Input("in1") + target_out1 = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) relation1 = Fir(W=a)(input1.last()) relation1.closedLoop(input1) - output1 = Output('out1', relation1) + output1 = Output("out1", relation1) - input2 = Input('in2') - target_out2 = Input('target2') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - relation2 = Linear(W=W,b=b)(input2.last()) + input2 = Input("in2") + target_out2 = Input("target2") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + relation2 = Linear(W=W, b=b)(input2.last()) relation2.closedLoop(input2) - output2 = Output('out2', relation2) + output2 = Output("out2", relation2) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error1', target_out1.last(), output1) - test.addMinimize('error2', target_out2.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error1", target_out1.last(), output1) + test.addMinimize("error2", target_out2.last(), output2) test.neuralizeModel() - self.assertEqual({'out1': [0.0], 'out2': [1.0]}, test()) - self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1.0],'in2': [1.0]})) - self.assertEqual({'out1': [1.0], 'out2': [3.0]}, test()) + self.assertEqual({"out1": [0.0], "out2": [1.0]}, test()) + self.assertEqual( + {"out1": [1.0], "out2": [2.0]}, test({"in1": [1.0], "in2": [1.0]}) + ) + self.assertEqual({"out1": [1.0], "out2": [3.0]}, test()) test.resetStates() - self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}, test(prediction_samples=5, num_of_samples=6)) - - dataset = {'in1': [1], 'in2': [1.0], 'target1': [3], 'target2': [3]} - test.loadData(name='dataset', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[5.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertListEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertEqual( + { + "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + }, + test(prediction_samples=5, num_of_samples=6), + ) + + dataset = {"in1": [1], "in2": [1.0], "target1": [3], "target2": [3]} + test.loadData(name="dataset", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[5.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertListEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[5.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertListEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[5.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertListEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertListEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - - dataset = {'in1': [1.0,1.0], 'in2': [1.0,1.0], 'target1': [3.0,3.0], 'target2': [3.0,3.0]} - test.loadData(name='dataset2', source=dataset) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=2) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertListEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + + dataset = { + "in1": [1.0, 1.0], + "in2": [1.0, 1.0], + "target1": [3.0, 3.0], + "target2": [3.0, 3.0], + } + test.loadData(name="dataset2", source=dataset) test.neuralizeModel(clear_model=True) # the out is 3.0 due the mean of the error is not the same of two epochs - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertListEqual([3.0], test.parameters['b']) - self.assertListEqual([[5.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=1 + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertListEqual([3.0], test.parameters["b"]) + self.assertListEqual([[5.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertListEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=2 + ) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertListEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) def test_training_values_fir_and_liner_train_closed_loop(self): NeuObj.clearNames() - input1 = Input('in1') - target_out1 = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out1',Fir(W=a)(input1.last())) + input1 = Input("in1") + target_out1 = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out1", Fir(W=a)(input1.last())) - input2 = Input('in2') - target_out2 = Input('target2') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=1) - output2 = Output('out2', Linear(W=W,b=b)(input2.last())) + input2 = Input("in2") + target_out2 = Input("target2") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=1) + output2 = Output("out2", Linear(W=W, b=b)(input2.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error1', target_out1.last(), output1) - test.addMinimize('error2', target_out2.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error1", target_out1.last(), output1) + test.addMinimize("error2", target_out2.last(), output2) test.neuralizeModel() - self.assertEqual({'out1': [0.0], 'out2': [1.0]}, test(closed_loop={'in1':'out1','in2':'out2'})) - self.assertEqual({'out1': [1.0], 'out2': [2.0]}, test({'in1': [1.0],'in2': [1.0]},closed_loop={'in1':'out1','in2':'out2'})) + self.assertEqual( + {"out1": [0.0], "out2": [1.0]}, + test(closed_loop={"in1": "out1", "in2": "out2"}), + ) + self.assertEqual( + {"out1": [1.0], "out2": [2.0]}, + test( + {"in1": [1.0], "in2": [1.0]}, closed_loop={"in1": "out1", "in2": "out2"} + ), + ) # # The memory is reset for each call - self.assertEqual({'out1': [0.0], 'out2': [1.0]}, test(closed_loop={'in1':'out1', 'in2':'out2'})) - - dataset = {'in1': [1], 'in2': [1.0], 'target1': [3], 'target2': [3]} - test.loadData(name='dataset', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertEqual([3.0], test.parameters['b']) - self.assertListEqual([[5.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'}) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertEqual( + {"out1": [0.0], "out2": [1.0]}, + test(closed_loop={"in1": "out1", "in2": "out2"}), + ) + + dataset = {"in1": [1], "in2": [1.0], "target1": [3], "target2": [3]} + test.loadData(name="dataset", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + closed_loop={"in1": "out1", "in2": "out2"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertEqual([3.0], test.parameters["b"]) + self.assertListEqual([[5.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + closed_loop={"in1": "out1", "in2": "out2"}, + ) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertEqual([3.0], test.parameters['b']) - self.assertListEqual([[5.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'}) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + closed_loop={"in1": "out1", "in2": "out2"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertEqual([3.0], test.parameters["b"]) + self.assertListEqual([[5.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=1, + closed_loop={"in1": "out1", "in2": "out2"}, + ) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertEqual([3.0], test.parameters['b']) - self.assertListEqual([[5.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=1) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertEqual([3.0], test.parameters["b"]) + self.assertListEqual([[5.0]], test.parameters["a"]) + test.trainModel(optimizer="SGD", splits=[100, 0, 0], lr=1, num_of_epochs=1) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(optimizer='SGD', splits=[100, 0, 0], lr=1, num_of_epochs=2, closed_loop={'in1':'out1','in2':'out2'}) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - - dataset = {'in1': [1.0,1.0], 'in2': [1.0,1.0], 'target1': [3.0,3.0], 'target2': [3.0,3.0]} - test.loadData(name='dataset2', source=dataset) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + optimizer="SGD", + splits=[100, 0, 0], + lr=1, + num_of_epochs=2, + closed_loop={"in1": "out1", "in2": "out2"}, + ) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + + dataset = { + "in1": [1.0, 1.0], + "in2": [1.0, 1.0], + "target1": [3.0, 3.0], + "target2": [3.0, 3.0], + } + test.loadData(name="dataset2", source=dataset) test.neuralizeModel(clear_model=True) # the out is 3.0 due the mean of the error is not the same of two epochs - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, closed_loop={'in1':'out1','in2':'out2'}) - self.assertListEqual([[3.0]], test.parameters['W']) - self.assertEqual([3.0], test.parameters['b']) - self.assertListEqual([[5.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + closed_loop={"in1": "out1", "in2": "out2"}, + ) + self.assertListEqual([[3.0]], test.parameters["W"]) + self.assertEqual([3.0], test.parameters["b"]) + self.assertListEqual([[5.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=2, closed_loop={'in1':'out1','in2':'out2'}) - self.assertListEqual([[-3.0]], test.parameters['W']) - self.assertEqual([-3.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=2, + closed_loop={"in1": "out1", "in2": "out2"}, + ) + self.assertListEqual([[-3.0]], test.parameters["W"]) + self.assertEqual([-3.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) def test_training_values_fir_and_linear_closed_loop_more_prediction(self): NeuObj.clearNames() - input1 = Input('in1') - target_out1 = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) + input1 = Input("in1") + target_out1 = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) relation1 = Fir(W=a)(input1.last()) relation1.closedLoop(input1) - output1 = Output('out1',relation1) + output1 = Output("out1", relation1) - input2 = Input('in2') - target_out2 = Input('target2') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - relation2=Linear(W=W,b=b)(input2.last()) + input2 = Input("in2") + target_out2 = Input("target2") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + relation2 = Linear(W=W, b=b)(input2.last()) relation2.closedLoop(input2) - output2 = Output('out2', relation2) + output2 = Output("out2", relation2) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error1', target_out1.last(), output1) - test.addMinimize('error2', target_out2.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error1", target_out1.last(), output1) + test.addMinimize("error2", target_out2.last(), output2) test.neuralizeModel() - self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}, - test(prediction_samples=5, num_of_samples=6)) - #self.assertEqual({'out1': [1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0]}, + self.assertEqual( + { + "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + }, + test(prediction_samples=5, num_of_samples=6), + ) + # self.assertEqual({'out1': [1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0]}, # test({'in1':[1.0,2.0]},prediction_samples=5)) - self.assertEqual({'out1': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0]}, - test({'in1': [1.0, 2.0]}, prediction_samples=5, num_of_samples=7)) - #self.assertEqual({'out1': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [0.0, -1.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]}, + self.assertEqual( + { + "out1": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], + "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0], + }, + test({"in1": [1.0, 2.0]}, prediction_samples=5, num_of_samples=7), + ) + # self.assertEqual({'out1': [2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0], 'out2': [0.0, -1.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]}, # test({'in2':[-1.0,-2.0,-3.0]},prediction_samples=5)) - self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0]}, - test({'in2':[-1.0,-2.0,-3.0]}, prediction_samples=5, num_of_samples=8)) - - dataset = {'in1': [0,2,7,1], 'in2': [-1,0,-3,7], 'target1': [3,4,5,1], 'target2': [-3,-4,-5,-1]} - test.loadData(name='dataset2', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0) - self.assertListEqual([[-24.5]], test.parameters['W']) - self.assertListEqual([-9.0], test.parameters['b']) - self.assertListEqual([[-4.0]], test.parameters['a']) + self.assertEqual( + { + "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "out2": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0], + }, + test({"in2": [-1.0, -2.0, -3.0]}, prediction_samples=5, num_of_samples=8), + ) + + dataset = { + "in1": [0, 2, 7, 1], + "in2": [-1, 0, -3, 7], + "target1": [3, 4, 5, 1], + "target2": [-3, -4, -5, -1], + } + test.loadData(name="dataset2", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + ) + self.assertListEqual([[-24.5]], test.parameters["W"]) + self.assertListEqual([-9.0], test.parameters["b"]) + self.assertListEqual([[-4.0]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1) - self.assertListEqual([[-24.5]], test.parameters['W']) - self.assertListEqual([-9.0], test.parameters['b']) - self.assertListEqual([[-4.0]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", optimizer="SGD", lr=1, num_of_epochs=1 + ) + self.assertListEqual([[-24.5]], test.parameters["W"]) + self.assertListEqual([-9.0], test.parameters["b"]) + self.assertListEqual([[-4.0]], test.parameters["a"]) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + ) # test.neuralizeModel(clear_model=True) # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1) # self.assertListEqual([[[1.0]]], test.parameters['W']) # self.assertListEqual([[1.0]], test.parameters['b']) # self.assertListEqual([[1.0]], test.parameters['a']) test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([-24.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + ) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([-24.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) # test.neuralizeModel(clear_model=True) # self.assertListEqual([[[1.0]]],test.parameters['W']) @@ -1272,50 +2434,97 @@ def test_training_values_fir_and_linear_closed_loop_more_prediction(self): def test_training_values_fir_and_linear_train_closed_loop_more_prediction(self): NeuObj.clearNames() - input1 = Input('in1') - target_out1 = Input('target1') - a = Parameter('a', sw=1, values=[[1]]) - output1 = Output('out1',Fir(W=a)(input1.last())) + input1 = Input("in1") + target_out1 = Input("target1") + a = Parameter("a", sw=1, values=[[1]]) + output1 = Output("out1", Fir(W=a)(input1.last())) - input2 = Input('in2') - target_out2 = Input('target2') - W = Parameter('W', values=[[1]]) - b = Parameter('b', values=[1]) - output2 = Output('out2', Linear(W=W,b=b)(input2.last())) + input2 = Input("in2") + target_out2 = Input("target2") + W = Parameter("W", values=[[1]]) + b = Parameter("b", values=[1]) + output2 = Output("out2", Linear(W=W, b=b)(input2.last())) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error1', target_out1.last(), output1) - test.addMinimize('error2', target_out2.last(), output2) + test.addModel("model", [output1, output2]) + test.addMinimize("error1", target_out1.last(), output1) + test.addMinimize("error2", target_out2.last(), output2) test.neuralizeModel() # self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}, # test(prediction_samples=5, closed_loop={'in2':'out2','in1':'out1'}, _num_of_samples=6)) - self.assertEqual({'out1': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], 'out2': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0]}, - test({'in1':[1.0, 2.0]}, prediction_samples=5, closed_loop={'in2':'out2','in1':'out1'}, num_of_samples=7)) - self.assertEqual({'out1': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'out2': [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0]}, - test({'in2':[-1.0,-2.0,-3.0]}, prediction_samples=5, closed_loop={'in2':'out2','in1':'out1'}, num_of_samples=8)) - - dataset = {'in1': [0,2,7,1], 'in2': [-1,0,-3,7], 'target1': [3,4,5,1], 'target2': [-3,-4,-5,-1]} - test.loadData(name='dataset2', source=dataset) - - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, closed_loop={'in2':'out2','in1':'out1'}) - self.assertListEqual([[-24.5]], test.parameters['W']) - self.assertListEqual([-9.0], test.parameters['b']) - self.assertListEqual([[-4.0]], test.parameters['a']) + self.assertEqual( + { + "out1": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], + "out2": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0], + }, + test( + {"in1": [1.0, 2.0]}, + prediction_samples=5, + closed_loop={"in2": "out2", "in1": "out1"}, + num_of_samples=7, + ), + ) + self.assertEqual( + { + "out1": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "out2": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0], + }, + test( + {"in2": [-1.0, -2.0, -3.0]}, + prediction_samples=5, + closed_loop={"in2": "out2", "in1": "out1"}, + num_of_samples=8, + ), + ) + + dataset = { + "in1": [0, 2, 7, 1], + "in2": [-1, 0, -3, 7], + "target1": [3, 4, 5, 1], + "target2": [-3, -4, -5, -1], + } + test.loadData(name="dataset2", source=dataset) + + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + closed_loop={"in2": "out2", "in1": "out1"}, + ) + self.assertListEqual([[-24.5]], test.parameters["W"]) + self.assertListEqual([-9.0], test.parameters["b"]) + self.assertListEqual([[-4.0]], test.parameters["a"]) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10, closed_loop={'in2':'out2','in1':'out1'}) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + closed_loop={"in2": "out2", "in1": "out1"}, + ) # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, closed_loop={'in2':'out2','in1':'out1'}) # TODO add this test test.neuralizeModel(clear_model=True) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3, closed_loop={'in2':'out2','in1':'out1'}) - self.assertListEqual([[1.0]], test.parameters['W']) - self.assertListEqual([-24.0], test.parameters['b']) - self.assertListEqual([[1.0]], test.parameters['a']) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + closed_loop={"in2": "out2", "in1": "out1"}, + ) + self.assertListEqual([[1.0]], test.parameters["W"]) + self.assertListEqual([-24.0], test.parameters["b"]) + self.assertListEqual([[1.0]], test.parameters["a"]) # test.neuralizeModel(clear_model=True) # TODO add this test # self.assertListEqual([[[1.0]]],test.parameters['W']) @@ -1328,96 +2537,242 @@ def test_training_values_fir_and_linear_train_closed_loop_more_prediction(self): def test_training_values_fir_and_liner_closed_loop_bigger_window(self): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=[1]) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=[1]) relation1 = Linear(W=W, b=b)(input1.sw(2)) - input2 = Input('in2') - a = Parameter('a', sw=4, values=[[1,3],[2,4],[3,5],[4,6]]) - relation2 = Fir(output_dimension=2,W=a)(input2.sw(4)) + input2 = Input("in2") + a = Parameter("a", sw=4, values=[[1, 3], [2, 4], [3, 5], [4, 6]]) + relation2 = Fir(output_dimension=2, W=a)(input2.sw(4)) relation2.closedLoop(input1) relation1.closedLoop(input2) - output1 = Output('out1', relation1) - output2 = Output('out2', relation2) + output1 = Output("out1", relation1) + output2 = Output("out2", relation2) - target1 = Input('target1') - target2 = Input('target2', dimensions=2) + target1 = Input("target1") + target2 = Input("target2", dimensions=2) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error1', output1, target1.sw(2)) - test.addMinimize('error2', output2, target2.last()) + test.addModel("model", [output1, output2]) + test.addMinimize("error1", output1, target1.sw(2)) + test.addMinimize("error2", output2, target2.last()) test.neuralizeModel() - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], 'out2': [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2, 2, 2]})) - - dataset = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2], - 'target1': [-11, -17, -12, -20], - 'target2': [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]]} - test.loadData(name='dataset', source=dataset) - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], - 'out2': [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]]}, - test(dataset)) - - self.assertListEqual([[-1],[-5]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a']) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0) - self.assertListEqual([[83.0],[105.0]], test.parameters['W']) - self.assertListEqual([6.0], test.parameters['b']) - self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a']) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], + "out2": [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]], + }, + test( + { + "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], + "in2": [-10, -16, -5, 2, 2, 2], + } + ), + ) + + dataset = { + "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], + "in2": [-10, -16, -5, 2], + "target1": [-11, -17, -12, -20], + "target2": [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]], + } + test.loadData(name="dataset", source=dataset) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], + "out2": [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]], + }, + test(dataset), + ) + + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + ) + self.assertListEqual([[83.0], [105.0]], test.parameters["W"]) + self.assertListEqual([6.0], test.parameters["b"]) + self.assertListEqual( + [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]], + test.parameters["a"], + ) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + ) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=1, + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error1':0}) - self.assertListEqual([[-1],[-5]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a']) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + minimize_gain={"error1": 0}, + ) + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual( + [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]], + test.parameters["a"], + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error2':0}) - self.assertListEqual([[83.0],[105.0]], test.parameters['W']) - self.assertListEqual([6.0], test.parameters['b']) - self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a']) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + minimize_gain={"error2": 0}, + ) + self.assertListEqual([[83.0], [105.0]], test.parameters["W"]) + self.assertListEqual([6.0], test.parameters["b"]) + self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - dataset2 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5], - 'target1':[-11,-17,-12,-20,5,1,0], - 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-33.0, -84.0],[-31.0, -84.0],[0.0, -84.0],[-31.0, 0.0]]} - test.loadData(name='dataset2', source=dataset2) - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0],[-4.0,1.0],[1.0,-4.0],[-4.0,0.0]], - 'out2': [[[-49, -107]], [[-8, -40]], [[-4, -10]], [[19, 33]], [[-11, -17]], [[-24, -44]]]}, - test(dataset2)) + dataset2 = { + "in1": [ + [1.0, 2.0], + [2.0, 3.0], + [4.0, 6.0], + [0.0, 1.0], + [0.0, 0.0], + [0.0, 1.0], + [1.0, 0.0], + ], + "in2": [-10, -16, -5, 2, 3, -3, 5], + "target1": [-11, -17, -12, -20, 5, 1, 0], + "target2": [ + [-34.0, -86.0], + [-31.0, -90.0], + [-32.0, -86.0], + [-33.0, -84.0], + [-31.0, -84.0], + [0.0, -84.0], + [-31.0, 0.0], + ], + } + test.loadData(name="dataset2", source=dataset2) + self.assertEqual( + { + "out1": [ + [-10.0, -16.0], + [-16.0, -33.0], + [-33.0, -4.0], + [-4.0, 1.0], + [1.0, -4.0], + [-4.0, 0.0], + ], + "out2": [ + [[-49, -107]], + [[-8, -40]], + [[-4, -10]], + [[19, 33]], + [[-11, -17]], + [[-24, -44]], + ], + }, + test(dataset2), + ) # Use a train_batch_size of 4 # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1) # TODO Add this test - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0) - self.assertListEqual([[20.0], [21.0]], test.parameters['W']) - self.assertListEqual([2.75], test.parameters['b']) - self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + ) + self.assertListEqual([[20.0], [21.0]], test.parameters["W"]) + self.assertListEqual([2.75], test.parameters["b"]) + self.assertListEqual( + [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]], + test.parameters["a"], + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4) - self.assertListEqual([[20.0], [21.0]], test.parameters['W']) - self.assertListEqual([2.75], test.parameters['b']) - self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=4, + ) + self.assertListEqual([[20.0], [21.0]], test.parameters["W"]) + self.assertListEqual([2.75], test.parameters["b"]) + self.assertListEqual( + [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]], + test.parameters["a"], + ) # Use a small batch but with a prediction sample of 3 = to 4 samples - dataset3 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5], - 'target1':[-11, -17, -30, -2, 582, 1421, -18975], - 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-48, -106], [-140, -256], [2254, 3341], [7420, 11374]]} - test.loadData(name='dataset3', source=dataset3) + dataset3 = { + "in1": [ + [1.0, 2.0], + [2.0, 3.0], + [4.0, 6.0], + [0.0, 1.0], + [0.0, 0.0], + [0.0, 1.0], + [1.0, 0.0], + ], + "in2": [-10, -16, -5, 2, 3, -3, 5], + "target1": [-11, -17, -30, -2, 582, 1421, -18975], + "target2": [ + [-34.0, -86.0], + [-31.0, -90.0], + [-32.0, -86.0], + [-48, -106], + [-140, -256], + [2254, 3341], + [7420, 11374], + ], + } + test.loadData(name="dataset3", source=dataset3) test.neuralizeModel(clear_model=True) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4) - test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3) - self.assertListEqual([[-3010.5],[-5211.25]], test.parameters['W']) - self.assertListEqual([199.5], test.parameters['b']) - self.assertListEqual([[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=4, + ) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + ) + self.assertListEqual([[-3010.5], [-5211.25]], test.parameters["W"]) + self.assertListEqual([199.5], test.parameters["b"]) + self.assertListEqual( + [[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]], + test.parameters["a"], + ) # test.neuralizeModel(clear_model=True) # test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=0.01, num_of_epochs=2, train_batch_size=2, prediction_samples=2) @@ -1427,90 +2782,246 @@ def test_training_values_fir_and_liner_closed_loop_bigger_window(self): def test_training_values_fir_and_liner_train_closed_loop_bigger_window(self): NeuObj.clearNames() - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[-1],[-5]]) - b = Parameter('b', values=[1]) - output1 = Output('out1', Linear(W=W,b=b)(input1.sw(2))) + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[-1], [-5]]) + b = Parameter("b", values=[1]) + output1 = Output("out1", Linear(W=W, b=b)(input1.sw(2))) - input2 = Input('in2') - a = Parameter('a', sw=4, values=[[1,3],[2,4],[3,5],[4,6]]) - output2 = Output('out2', Fir(output_dimension=2,W=a)(input2.sw(4))) + input2 = Input("in2") + a = Parameter("a", sw=4, values=[[1, 3], [2, 4], [3, 5], [4, 6]]) + output2 = Output("out2", Fir(output_dimension=2, W=a)(input2.sw(4))) - target1 = Input('target1') - target2 = Input('target2', dimensions=2) + target1 = Input("target1") + target2 = Input("target2", dimensions=2) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error1', output1, target1.sw(2)) - test.addMinimize('error2', output2, target2.last()) + test.addModel("model", [output1, output2]) + test.addMinimize("error1", output1, target1.sw(2)) + test.addMinimize("error2", output2, target2.last()) test.neuralizeModel() - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], 'out2': [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]]}, - test({'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2, 2, 2]},closed_loop={'in1':'out2','in2':'out1'})) - - dataset = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], 'in2': [-10, -16, -5, 2], - 'target1': [-11, -17, -12, -20], - 'target2': [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]]} - test.loadData(name='dataset', source=dataset) - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], - 'out2': [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]]}, - test(dataset,closed_loop={'in1':'out2','in2':'out1'})) - - self.assertListEqual([[-1],[-5]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a']) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0,closed_loop={'in1':'out2','in2':'out1'}) - self.assertListEqual([[83.0],[105.0]], test.parameters['W']) - self.assertListEqual([6.0], test.parameters['b']) - self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a']) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], + "out2": [[[-49.0, -107.0]], [[-12.0, -46.0]], [[13.0, 15.0]]], + }, + test( + { + "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], + "in2": [-10, -16, -5, 2, 2, 2], + }, + closed_loop={"in1": "out2", "in2": "out1"}, + ), + ) + + dataset = { + "in1": [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0]], + "in2": [-10, -16, -5, 2], + "target1": [-11, -17, -12, -20], + "target2": [[-34.0, -86.0], [-31.0, -90.0], [-32.0, -86.0], [-33.0, -84.0]], + } + test.loadData(name="dataset", source=dataset) + self.assertEqual( + { + "out1": [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0]], + "out2": [[[-49.0, -107.0]], [[-120.0, -214.0]], [[-205.0, -333.0]]], + }, + test(dataset, closed_loop={"in1": "out2", "in2": "out1"}), + ) + + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"]) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + closed_loop={"in1": "out2", "in2": "out1"}, + ) + self.assertListEqual([[83.0], [105.0]], test.parameters["W"]) + self.assertListEqual([6.0], test.parameters["b"]) + self.assertListEqual( + [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]], + test.parameters["a"], + ) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=10,closed_loop={'in1':'out2','in2':'out1'}) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=10, + closed_loop={"in1": "out2", "in2": "out1"}, + ) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1,closed_loop={'in1':'out2','in2':'out1'}) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=1, + closed_loop={"in1": "out2", "in2": "out1"}, + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error1':0},closed_loop={'in1':'out2','in2':'out1'}) - self.assertListEqual([[-1],[-5]], test.parameters['W']) - self.assertListEqual([1.0], test.parameters['b']) - self.assertListEqual([[-159.0, -227.0], [-254., -364.], [-77., -110.], [36., 52.]], test.parameters['a']) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + minimize_gain={"error1": 0}, + closed_loop={"in1": "out2", "in2": "out1"}, + ) + self.assertListEqual([[-1], [-5]], test.parameters["W"]) + self.assertListEqual([1.0], test.parameters["b"]) + self.assertListEqual( + [[-159.0, -227.0], [-254.0, -364.0], [-77.0, -110.0], [36.0, 52.0]], + test.parameters["a"], + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, minimize_gain={'error2':0},closed_loop={'in1':'out2','in2':'out1'}) - self.assertListEqual([[83.0],[105.0]], test.parameters['W']) - self.assertListEqual([6.0], test.parameters['b']) - self.assertListEqual([[1,3],[2,4],[3,5],[4,6]], test.parameters['a']) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + minimize_gain={"error2": 0}, + closed_loop={"in1": "out2", "in2": "out1"}, + ) + self.assertListEqual([[83.0], [105.0]], test.parameters["W"]) + self.assertListEqual([6.0], test.parameters["b"]) + self.assertListEqual([[1, 3], [2, 4], [3, 5], [4, 6]], test.parameters["a"]) test.neuralizeModel(clear_model=True) - dataset2 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5], - 'target1':[-11,-17,-12,-20,5,1,0], - 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-33.0, -84.0],[-31.0, -84.0],[0.0, -84.0],[-31.0, 0.0]]} - test.loadData(name='dataset2', source=dataset2) - self.assertEqual({'out1': [[-10.0, -16.0], [-16.0, -33.0], [-33.0, -4.0],[-4.0,1.0],[1.0,-4.0],[-4.0,0.0]], - 'out2': [[[-49, -107]], [[-8, -40]], [[-4, -10]], [[19, 33]], [[-11, -17]], [[-24, -44]]]}, - test(dataset2,closed_loop={'in1':'out2','in2':'out1'})) + dataset2 = { + "in1": [ + [1.0, 2.0], + [2.0, 3.0], + [4.0, 6.0], + [0.0, 1.0], + [0.0, 0.0], + [0.0, 1.0], + [1.0, 0.0], + ], + "in2": [-10, -16, -5, 2, 3, -3, 5], + "target1": [-11, -17, -12, -20, 5, 1, 0], + "target2": [ + [-34.0, -86.0], + [-31.0, -90.0], + [-32.0, -86.0], + [-33.0, -84.0], + [-31.0, -84.0], + [0.0, -84.0], + [-31.0, 0.0], + ], + } + test.loadData(name="dataset2", source=dataset2) + self.assertEqual( + { + "out1": [ + [-10.0, -16.0], + [-16.0, -33.0], + [-33.0, -4.0], + [-4.0, 1.0], + [1.0, -4.0], + [-4.0, 0.0], + ], + "out2": [ + [[-49, -107]], + [[-8, -40]], + [[-4, -10]], + [[19, 33]], + [[-11, -17]], + [[-24, -44]], + ], + }, + test(dataset2, closed_loop={"in1": "out2", "in2": "out1"}), + ) # Use a train_batch_size of 4 # test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=1, closed_loop={'in1':'out2','in2':'out1'}) # TODO Add this test - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, prediction_samples=0, closed_loop={'in1':'out2','in2':'out1'}) - self.assertListEqual([[20.0], [21.0]], test.parameters['W']) - self.assertListEqual([2.75], test.parameters['b']) - self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + prediction_samples=0, + closed_loop={"in1": "out2", "in2": "out1"}, + ) + self.assertListEqual([[20.0], [21.0]], test.parameters["W"]) + self.assertListEqual([2.75], test.parameters["b"]) + self.assertListEqual( + [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]], + test.parameters["a"], + ) test.neuralizeModel(clear_model=True) - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=4,closed_loop={'in1':'out2','in2':'out1'}) - self.assertListEqual([[20.0], [21.0]], test.parameters['W']) - self.assertListEqual([2.75], test.parameters['b']) - self.assertListEqual([[23., 197.5], [-68.75, -94.75], [12., -76.5], [-70.75, -1.25]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=4, + closed_loop={"in1": "out2", "in2": "out1"}, + ) + self.assertListEqual([[20.0], [21.0]], test.parameters["W"]) + self.assertListEqual([2.75], test.parameters["b"]) + self.assertListEqual( + [[23.0, 197.5], [-68.75, -94.75], [12.0, -76.5], [-70.75, -1.25]], + test.parameters["a"], + ) # Use a small batch but with a prediction sample of 3 = to 4 samples - dataset3 = {'in1': [[1.0, 2.0], [2.0, 3.0], [4.0, 6.0], [0.0, 1.0], [0.0, 0.0], [0.0, 1.0], [1.0, 0.0]], 'in2': [-10, -16, -5, 2, 3, -3, 5], - 'target1':[-11, -17, -30, -2, 582, 1421, -18975], - 'target2':[[-34.0, -86.0],[-31.0, -90.0],[-32.0, -86.0],[-48, -106], [-140, -256], [2254, 3341], [7420, 11374]]} - test.loadData(name='dataset3', source=dataset3) + dataset3 = { + "in1": [ + [1.0, 2.0], + [2.0, 3.0], + [4.0, 6.0], + [0.0, 1.0], + [0.0, 0.0], + [0.0, 1.0], + [1.0, 0.0], + ], + "in2": [-10, -16, -5, 2, 3, -3, 5], + "target1": [-11, -17, -30, -2, 582, 1421, -18975], + "target2": [ + [-34.0, -86.0], + [-31.0, -90.0], + [-32.0, -86.0], + [-48, -106], + [-140, -256], + [2254, 3341], + [7420, 11374], + ], + } + test.loadData(name="dataset3", source=dataset3) test.neuralizeModel(clear_model=True) with self.assertRaises(ValueError): - test.trainModel(train_dataset='dataset2', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=4,closed_loop={'in1':'out2','in2':'out1'}) - test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=1, train_batch_size=1, prediction_samples=3,closed_loop={'in1':'out2','in2':'out1'}) - self.assertListEqual([[-3010.5],[-5211.25]], test.parameters['W']) - self.assertListEqual([199.5], test.parameters['b']) - self.assertListEqual([[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]], test.parameters['a']) + test.trainModel( + train_dataset="dataset2", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=4, + closed_loop={"in1": "out2", "in2": "out1"}, + ) + test.trainModel( + train_dataset="dataset3", + optimizer="SGD", + lr=1, + num_of_epochs=1, + train_batch_size=1, + prediction_samples=3, + closed_loop={"in1": "out2", "in2": "out1"}, + ) + self.assertListEqual([[-3010.5], [-5211.25]], test.parameters["W"]) + self.assertListEqual([199.5], test.parameters["b"]) + self.assertListEqual( + [[252.75, 1172.5], [420.5, 1998.25], [-228.5, 627.5], [-626.75, 3036.5]], + test.parameters["a"], + ) # test.neuralizeModel(clear_model=True) # test.trainModel(train_dataset='dataset3', optimizer='SGD', lr=1, num_of_epochs=2, train_batch_size=2, prediction_samples=2,closed_loop={'in1':'out2','in2':'out1'}) @@ -1520,105 +3031,230 @@ def test_training_values_fir_and_liner_train_closed_loop_bigger_window(self): def test_train_compare_state_and_closed_loop(self): NeuObj.clearNames() - dataset = {'control': [-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -5, 2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2], - 'target1': [-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2,-1, -1, -1, -2], - 'target2': [[-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0], - [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0], - [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0], - [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0], - [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0], - [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0], - [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0], - [-1.0, -1.0], [-1.0, -2.0], [-1.0, -1.0], [-1.0, -2.0] - ]} - - feed = Input('control') - input1 = Input('in1', dimensions=2) - W = Parameter('W', values=[[0.1],[0.1]]) - b = Parameter('b', values=[0.1]) - output1 = Output('out1', feed.sw(2)+Linear(W=W, b=b)(input1.sw(2))) - - input2 = Input('in2') - a = Parameter('a', sw=4, values=[[0.1,0.3],[0.2,0.4],[0.3,0.5],[0.4,0.6]]) - output2 = Output('out2', Fir(output_dimension=2,W=a)(input2.sw(4))) - - target1 = Input('target1') - target2 = Input('target2', dimensions=2) + dataset = { + "control": [ + -1, + -1, + -5, + 2, + -1, + -1, + -5, + 2, + -1, + -1, + -5, + 2, + -1, + -1, + -5, + 2, + -1, + -1, + -5, + 2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + ], + "target1": [ + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + -1, + -1, + -1, + -2, + ], + "target2": [ + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + [-1.0, -1.0], + [-1.0, -2.0], + ], + } + + feed = Input("control") + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[0.1], [0.1]]) + b = Parameter("b", values=[0.1]) + output1 = Output("out1", feed.sw(2) + Linear(W=W, b=b)(input1.sw(2))) + + input2 = Input("in2") + a = Parameter( + "a", sw=4, values=[[0.1, 0.3], [0.2, 0.4], [0.3, 0.5], [0.4, 0.6]] + ) + output2 = Output("out2", Fir(output_dimension=2, W=a)(input2.sw(4))) + + target1 = Input("target1") + target2 = Input("target2", dimensions=2) test = Modely(visualizer=None, seed=42) - test.addModel('model', [output1,output2]) - test.addMinimize('error1', output1, target1.sw(2)) - test.addMinimize('error2', output2, target2.last()) + test.addModel("model", [output1, output2]) + test.addMinimize("error1", output1, target1.sw(2)) + test.addMinimize("error2", output2, target2.last()) test.neuralizeModel() - test.loadData(name='dataset', source=dataset) - test.trainModel(splits=[60,40,0], optimizer='SGD', lr=0.001, num_of_epochs=1, prediction_samples=10, - closed_loop={'in1': 'out2', 'in2': 'out1'}) + test.loadData(name="dataset", source=dataset) + test.trainModel( + splits=[60, 40, 0], + optimizer="SGD", + lr=0.001, + num_of_epochs=1, + prediction_samples=10, + closed_loop={"in1": "out2", "in2": "out1"}, + ) NeuObj.clearNames() - feed = Input('control') - input1 = Input('in1',dimensions=2) - W = Parameter('W', values=[[0.1],[0.1]]) - b = Parameter('b', values=[0.1]) + feed = Input("control") + input1 = Input("in1", dimensions=2) + W = Parameter("W", values=[[0.1], [0.1]]) + b = Parameter("b", values=[0.1]) relation1 = feed.sw(2) + Linear(W=W, b=b)(input1.sw(2)) - input2 = Input('in2') - a = Parameter('a', sw=4, values=[[0.1,0.3],[0.2,0.4],[0.3,0.5],[0.4,0.6]]) + input2 = Input("in2") + a = Parameter( + "a", sw=4, values=[[0.1, 0.3], [0.2, 0.4], [0.3, 0.5], [0.4, 0.6]] + ) relation2 = Fir(output_dimension=2, W=a)(input2.sw(4)) relation1.closedLoop(input2) relation2.closedLoop(input1) - output1 = Output('out1', relation1) - output2 = Output('out2', relation2) + output1 = Output("out1", relation1) + output2 = Output("out2", relation2) - target1 = Input('target1') - target2 = Input('target2', dimensions=2) + target1 = Input("target1") + target2 = Input("target2", dimensions=2) test2 = Modely(visualizer=None, seed=42) - test2.addModel('model', [output1, output2]) - test2.addMinimize('error1', output1, target1.sw(2)) - test2.addMinimize('error2', output2, target2.last()) + test2.addModel("model", [output1, output2]) + test2.addMinimize("error1", output1, target1.sw(2)) + test2.addMinimize("error2", output2, target2.last()) test2.neuralizeModel() - test2.loadData(name='dataset', source=dataset) - test2.trainModel(splits=[60,40,0], optimizer='SGD', lr=0.001, num_of_epochs=1, prediction_samples=10) - - self.assertListEqual(test2.parameters['W'], test.parameters['W']) - self.assertListEqual(test2.parameters['a'], test.parameters['a']) - self.assertListEqual(test2.parameters['b'], test.parameters['b']) - self.assertListEqual(test2._training['error1']['train'] , test._training['error1']['train']) - self.assertListEqual(test2._training['error1']['val'], test._training['error1']['val']) - self.assertListEqual(test2._training['error2']['train'] , test._training['error2']['train']) - self.assertListEqual(test2._training['error2']['val'], test._training['error2']['val']) + test2.loadData(name="dataset", source=dataset) + test2.trainModel( + splits=[60, 40, 0], + optimizer="SGD", + lr=0.001, + num_of_epochs=1, + prediction_samples=10, + ) + + self.assertListEqual(test2.parameters["W"], test.parameters["W"]) + self.assertListEqual(test2.parameters["a"], test.parameters["a"]) + self.assertListEqual(test2.parameters["b"], test.parameters["b"]) + self.assertListEqual( + test2._training["error1"]["train"], test._training["error1"]["train"] + ) + self.assertListEqual( + test2._training["error1"]["val"], test._training["error1"]["val"] + ) + self.assertListEqual( + test2._training["error2"]["train"], test._training["error2"]["train"] + ) + self.assertListEqual( + test2._training["error2"]["val"], test._training["error2"]["val"] + ) test2 = Modely(visualizer=None, seed=42, log_internal=True) - test2.addModel('model', [output1, output2]) - test2.addMinimize('error1', output1, target1.sw(2)) - test2.addMinimize('error2', output2, target2.last()) + test2.addModel("model", [output1, output2]) + test2.addMinimize("error1", output1, target1.sw(2)) + test2.addMinimize("error2", output2, target2.last()) test2.neuralizeModel() - test2.loadData(name='dataset', source=dataset) - test2.trainModel(splits=[100,0,0], train_batch_size=1, step=10, optimizer='SGD', lr=0.001, num_of_epochs=1, prediction_samples=3) - - self.assertEqual(len(test2.internals.keys()), (3+1)*((26+10)//(10+1))) - #self.assertEqual(test2.internals) + test2.loadData(name="dataset", source=dataset) + test2.trainModel( + splits=[100, 0, 0], + train_batch_size=1, + step=10, + optimizer="SGD", + lr=0.001, + num_of_epochs=1, + prediction_samples=3, + ) + + self.assertEqual(len(test2.internals.keys()), (3 + 1) * ((26 + 10) // (10 + 1))) + # self.assertEqual(test2.internals) def test_train_derivate_wrt_input_closed_loop(self): NeuObj.clearNames() - x = Input('x') - x_target = Input('x_target') - y = Input('y') + x = Input("x") + x_target = Input("x_target") + y = Input("y") x_last = x.last() y_last = y.last() - p=Parameter('fir',sw=1,values=[[-0.5]]) + p = Parameter("fir", sw=1, values=[[-0.5]]) fun = Sin(x_last) + Fir(W=p)(x_last) + Cos(y_last) out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last) out_der.closedLoop(x) - out = Output('out', out_der) + out = Output("out", out_der) m = Modely(visualizer=None, seed=7) - m.addModel('model', [out]) - m.addMinimize('error', out, x_target.next()) + m.addModel("model", [out]) + m.addMinimize("error", out, x_target.next()) m.neuralizeModel() K = 0 @@ -1634,39 +3270,46 @@ def fun_data(x, y, K): x_data.append(x) y_data.append(y) - dataset = {'x': x_data, 'y': y_data, 'x_target': x_data} - m.loadData('dataset', dataset) - m.trainModel(lr=0.4, num_of_epochs=200, closed_loop={'y': 'out'}, prediction_samples=9) - result = m({'x': [-0.2], 'y': [0.5]}, closed_loop={'y':'out'}, num_of_samples=10, prediction_samples=10) - self.assertAlmostEqual([a.tolist() for a in x_data[0:10]],result['out']) + dataset = {"x": x_data, "y": y_data, "x_target": x_data} + m.loadData("dataset", dataset) + m.trainModel( + lr=0.4, num_of_epochs=200, closed_loop={"y": "out"}, prediction_samples=9 + ) + result = m( + {"x": [-0.2], "y": [0.5]}, + closed_loop={"y": "out"}, + num_of_samples=10, + prediction_samples=10, + ) + self.assertAlmostEqual([a.tolist() for a in x_data[0:10]], result["out"]) def test_train_derivate_wrt_input_connect(self): NeuObj.clearNames() - x = Input('x') - y = Input('y') + x = Input("x") + y = Input("y") x_last = x.last() y_last = y.last() - p1 = Parameter('p1', sw=1, values=[[0]]) + p1 = Parameter("p1", sw=1, values=[[0]]) fun = Sin(x_last) + Fir(W=p1)(x_last) + Cos(y_last) out_der = Differentiate(fun, x_last) + Differentiate(fun, y_last) - x2 = Input('x2') - y2 = Input('y2') + x2 = Input("x2") + y2 = Input("y2") x2_last = x2.last() y2_last = y2.last() - p2 = Parameter('p2', sw=1, values=[[0]]) + p2 = Parameter("p2", sw=1, values=[[0]]) fun2 = Sin(x2_last) + Fir(W=p2)(x2_last) + Cos(y2_last) out_der2 = Differentiate(fun2, x2_last) + Differentiate(fun2, y2_last) out_der.connect(x2) - out1 = Output('out1', out_der) - out2 = Output('out2', out_der2) + out1 = Output("out1", out_der) + out2 = Output("out2", out_der2) - target = Input('target') + target = Input("target") m = Modely(visualizer=None, seed=5) - m.addModel('model', [out1,out2]) - m.addMinimize('error', out_der2, target.last()) + m.addModel("model", [out1, out2]) + m.addMinimize("error", out_der2, target.last()) m.neuralizeModel() K1 = -0.5 @@ -1676,76 +3319,121 @@ def fun_data(x, y, K): return K + np.cos(x) - np.sin(y) def fun_data2(x, y, K1, K2): - return K2 + np.cos(fun_data(x,y,K1)) - np.sin(fun_data(x,y,K1)) + return K2 + np.cos(fun_data(x, y, K1)) - np.sin(fun_data(x, y, K1)) target = [] import numpy as np + x = np.random.rand(100) y = np.random.rand(100) - for (xi,yi) in zip(x,y): + for xi, yi in zip(x, y): r = fun_data2(xi, yi, K1, K2) target.append(r) - dataset = {'x': x.tolist(), 'y': y.tolist(), 'target': target} - m.loadData('dataset', dataset) - m.trainModel(lr=0.3, num_of_epochs=200, splits=[70,20,10], connect={'y2': 'out1'}, prediction_samples=9) - - result = m({'x': x.tolist(), 'y': y.tolist()}, connect={'y2':'out1'}, num_of_samples=10, prediction_samples=10) - self.assertAlmostEqual([a.tolist() for a in target[0:10]],result['out2']) + dataset = {"x": x.tolist(), "y": y.tolist(), "target": target} + m.loadData("dataset", dataset) + m.trainModel( + lr=0.3, + num_of_epochs=200, + splits=[70, 20, 10], + connect={"y2": "out1"}, + prediction_samples=9, + ) + + result = m( + {"x": x.tolist(), "y": y.tolist()}, + connect={"y2": "out1"}, + num_of_samples=10, + prediction_samples=10, + ) + self.assertAlmostEqual([a.tolist() for a in target[0:10]], result["out2"]) def test_training_values_fir_connect_linear_more_window_2(self): NeuObj.clearNames() - input1 = Input('in1') - W = Parameter('W', sw=4, values=[[1], [1], [1], [1]]) - b = Parameter('b', values=0) + input1 = Input("in1") + W = Parameter("W", sw=4, values=[[1], [1], [1], [1]]) + b = Parameter("b", values=0) lin_out = Fir(W=W, b=b)(input1.sw(4)) - inout = Input('inout') + inout = Input("inout") lin_out.connect(inout) - W1 = Parameter('W1', sw=4, values=[[1], [1], [1], [1]]) - b1 = Parameter('b1', values=0) + W1 = Parameter("W1", sw=4, values=[[1], [1], [1], [1]]) + b1 = Parameter("b1", values=0) fir_out = Fir(W=W1, b=b1)(inout.sw(4)) - output = Output('out', inout.sw(4)) - output1 = Output('out1', lin_out) - output2 = Output('out2', fir_out) + output = Output("out", inout.sw(4)) + output1 = Output("out1", lin_out) + output2 = Output("out2", fir_out) - target = Input('target') + target = Input("target") test = Modely(visualizer=None, seed=42, log_internal=True) - test.addModel('model', [output, output1, output2]) - test.addMinimize('error2', target.last(), output2) + test.addModel("model", [output, output1, output2]) + test.addMinimize("error2", target.last(), output2) test.neuralizeModel() # Dataset with only one sample - dataset = {'in1': [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0], - 'inout':[10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0], - 'target': [10.0, 22.0, 34.0, 46.0, 58.0, 70.0, 82.0, 94.0, 106.0]} - test.loadData(name='dataset', source=dataset) - + dataset = { + "in1": [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0], + "inout": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0], + "target": [10.0, 22.0, 34.0, 46.0, 58.0, 70.0, 82.0, 94.0, 106.0], + } + test.loadData(name="dataset", source=dataset) # Use a small batch but with a prediction sample of 3 = to 4 samples - self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters['W']) - self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters['W1']) - self.assertListEqual([0.0], test.parameters['b']) - self.assertListEqual([0.0], test.parameters['b1']) + self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters["W"]) + self.assertListEqual([[1.0], [1.0], [1.0], [1.0]], test.parameters["W1"]) + self.assertListEqual([0.0], test.parameters["b"]) + self.assertListEqual([0.0], test.parameters["b1"]) inference = test(inputs=dataset, prediction_samples=3) - self.assertDictEqual({'out': [[10.0, 20.0, 30.0, 12.0], [20.0, 30.0, 12.0, 20.0], [30.0, 12.0, 20.0, 28.0], [12.0, 20.0, 28.0, 36.0], [50.0, 60.0, 70.0, 44.0], [60.0, 70.0, 44.0, 52.0]], - 'out1': [12.0, 20.0, 28.0, 36.0, 44.0, 52.0], - 'out2': [72.0, 82.0, 90.0, 96.0, 224.0, 226.0]}, - inference) - test.trainModel(train_dataset='dataset', optimizer='SGD', lr=0.0001, train_batch_size=1, num_of_epochs=1, shuffle_data=False, prediction_samples=3) - for i, (k,v) in enumerate(test.internals.items()): + self.assertDictEqual( + { + "out": [ + [10.0, 20.0, 30.0, 12.0], + [20.0, 30.0, 12.0, 20.0], + [30.0, 12.0, 20.0, 28.0], + [12.0, 20.0, 28.0, 36.0], + [50.0, 60.0, 70.0, 44.0], + [60.0, 70.0, 44.0, 52.0], + ], + "out1": [12.0, 20.0, 28.0, 36.0, 44.0, 52.0], + "out2": [72.0, 82.0, 90.0, 96.0, 224.0, 226.0], + }, + inference, + ) + test.trainModel( + train_dataset="dataset", + optimizer="SGD", + lr=0.0001, + train_batch_size=1, + num_of_epochs=1, + shuffle_data=False, + prediction_samples=3, + ) + for i, (k, v) in enumerate(test.internals.items()): if i > 3: break - self.assertEqual(v['out']['out'][0][-1][0], v['out']['out1'][0][0][0]) - self.assertEqual(sum([x[0] for x in v['out']['out'][0]]), v['out']['out2'][0][0][0]) - self.assertDictEqual({'inout': [[[20.0], [30.0], [12.0], [float('inf')]]]}, test.internals['inout_0_0']['state']) - self.assertDictEqual({'inout': [[[30.0], [12.0], [20.0], [float('inf')]]]}, test.internals['inout_0_1']['state']) - self.assertDictEqual({'inout': [[[12.0], [20.0], [28.0], [float('inf')]]]}, test.internals['inout_0_2']['state']) - self.assertDictEqual({'inout': [[[20.0], [28.0], [36.0], [float('inf')]]]}, test.internals['inout_0_3']['state']) - + self.assertEqual(v["out"]["out"][0][-1][0], v["out"]["out1"][0][0][0]) + self.assertEqual( + sum([x[0] for x in v["out"]["out"][0]]), v["out"]["out2"][0][0][0] + ) + self.assertDictEqual( + {"inout": [[[20.0], [30.0], [12.0], [float("inf")]]]}, + test.internals["inout_0_0"]["state"], + ) + self.assertDictEqual( + {"inout": [[[30.0], [12.0], [20.0], [float("inf")]]]}, + test.internals["inout_0_1"]["state"], + ) + self.assertDictEqual( + {"inout": [[[12.0], [20.0], [28.0], [float("inf")]]]}, + test.internals["inout_0_2"]["state"], + ) + self.assertDictEqual( + {"inout": [[[20.0], [28.0], [36.0], [float("inf")]]]}, + test.internals["inout_0_3"]["state"], + ) # def test_state_initialization(self): # NeuObj.clearNames() @@ -1782,4 +3470,4 @@ def test_training_values_fir_connect_linear_more_window_2(self): # self.assertEqual(model.internals['inout_0_3']['out']['out'], [[[42.0]]]) # self.assertEqual(model.internals['inout_0_4']['out']['out'], [[[89.0]]]) # self.assertEqual(model.internals['inout_0_5']['out']['out'], [[[184.0]]]) - # self.assertEqual(model.internals['inout_1_0']['out']['out'], [[[6.0]]]) \ No newline at end of file + # self.assertEqual(model.internals['inout_1_0']['out']['out'], [[[6.0]]]) diff --git a/tests/test_utils.py b/tests/test_utils.py index e3d6665e..03783d48 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,14 +12,21 @@ sys.path.append(os.getcwd()) + class ModelyTrainingTest(unittest.TestCase): # test the linear interpolation function with batches of input data with shape torch.Size([N, 1, 1]) def test_linear_interp_with_batched_input_1(self): # x is a tensor of query points, and is a tensor of shape torch.Size([N, 1, 1]) - x = torch.tensor([[[-1.0]],[[0.15]],[[0.25]],[[0.35]],[[1.3]]]) + x = torch.tensor([[[-1.0]], [[0.15]], [[0.25]], [[0.35]], [[1.3]]]) # x_data and y_data are tensors of shape torch.Size([Q, 1]) - x_data = torch.tensor([[0.0],[0.1],[0.2],[0.3],[0.4],[0.5],[0.6],[0.7],[0.8],[0.9]]) - y_data = torch.tensor([[0.5],[0.6],[0.7],[0.8],[0.9],[1.0],[1.1],[1.2],[1.3],[1.4]]) + x_data = torch.tensor( + [[0.0], [0.1], [0.2], [0.3], [0.4], [0.5], [0.6], [0.7], [0.8], [0.9]] + ) + y_data = torch.tensor( + [[0.5], [0.6], [0.7], [0.8], [0.9], [1.0], [1.1], [1.2], [1.3], [1.4]] + ) - y = linear_interp(x,x_data,y_data) - self.assertEqual(y.shape, x.shape) # check that the output has the same shape as the input + y = linear_interp(x, x_data, y_data) + self.assertEqual( + y.shape, x.shape + ) # check that the output has the same shape as the input diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index b7f50364..f3fc0ba7 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -14,33 +14,43 @@ # 3 Tests # Test of visualizers + class ModelyTestVisualizer(unittest.TestCase): def __init__(self, *args, **kwargs): NeuObj.clearNames() super(ModelyTestVisualizer, self).__init__(*args, **kwargs) - self.x = x = Input('x') - self.y = y = Input('y') - self.z = z = Input('z') - self.a = a = Input('a', dimensions=2) - self.b = b = Input('b', dimensions=2) + self.x = x = Input("x") + self.y = y = Input("y") + self.z = z = Input("z") + self.a = a = Input("a", dimensions=2) + self.b = b = Input("b", dimensions=2) ## create the relations def myFun(K1, p1, p2): return K1 * p1 * p2 - P_time = Parameter('P_time', dimensions=2, sw=5, values=[[0,0],[-0.1,0.1],[-0.2,0.2],[-0.3,0.3],[-0.4,0.4]]) - K_x = Parameter('k_x', dimensions=1, tw=1, init='init_constant', init_params={'value': 1}) - K_y = Parameter('k_y', dimensions=1, tw=1) - w = Parameter('w', dimensions=1, tw=1, init='init_constant', init_params={'value': 1}) - t = Parameter('t', dimensions=1, tw=1) - c_v = Constant('c_v', tw=1, values=[[1], [2]]) + P_time = Parameter( + "P_time", + dimensions=2, + sw=5, + values=[[0, 0], [-0.1, 0.1], [-0.2, 0.2], [-0.3, 0.3], [-0.4, 0.4]], + ) + K_x = Parameter( + "k_x", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} + ) + K_y = Parameter("k_y", dimensions=1, tw=1) + w = Parameter( + "w", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} + ) + t = Parameter("t", dimensions=1, tw=1) + c_v = Constant("c_v", tw=1, values=[[1], [2]]) c = 5 - w_5 = Parameter('w_5', dimensions=1, tw=5) - t_5 = Parameter('t_5', dimensions=1, tw=5) + w_5 = Parameter("w_5", dimensions=1, tw=5) + t_5 = Parameter("t_5", dimensions=1, tw=5) c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]] - c_5_2 = Constant('c_5_2', tw=5, values=c_5) - parfun_x = ParamFun(myFun, parameters_and_constants=[K_x,c_v]) + c_5_2 = Constant("c_5_2", tw=5, values=c_5) + parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v]) parfun_y = ParamFun(myFun, parameters_and_constants=[K_y]) parfun_zz = ParamFun(myFun) parfun_2d = ParamFun(myFun, parameters_and_constants=[K_x, K_x]) @@ -58,24 +68,42 @@ def fuzzyfunth(x): fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1)) fuzzyTriang = Fuzzify(centers=[1, 2, 3, 7])(x.tw(1)) - fuzzyRect = Fuzzify(centers=[1, 2, 3, 7], functions='Rectangular')(x.tw(1)) - fuzzyList = Fuzzify(centers=[1, 3, 2, 7], functions=[fuzzyfun,fuzzyfunth])(x.tw(1)) + fuzzyRect = Fuzzify(centers=[1, 2, 3, 7], functions="Rectangular")(x.tw(1)) + fuzzyList = Fuzzify(centers=[1, 3, 2, 7], functions=[fuzzyfun, fuzzyfunth])( + x.tw(1) + ) self.stream = fuzzyList - self.out = Output('out', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))) - self.out2 = Output('out2', Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) - self.out3 = Output('out3', Add(fir_w, fir_t)) - self.out4 = Output('out4', Linear(output_dimension=1)(fuzzy+fuzzyTriang+fuzzyRect+fuzzyList)) - self.out5 = Output('out5', Fir(time_part) + Fir(sample_select)) - self.out6 = Output('out6', LocalModel(output_function=Fir())(x.tw(1), fuzzy)) - self.out7 = Output('out7', parfun_zz(z.last())) - self.out8 = Output('out8', Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + Fir(parfun_zz(x.tw(5), t_5, c_5_2))) - self.out9 = Output('out9', Fir(parfun_2d(x.tw(1)) + parfun_3d(x.tw(1),x.tw(1)))) - self.out10 = Output('out10', a.sw(5)+P_time) - self.out11 = Output('out11', TimeConcatenate(TimeConcatenate( - TimeConcatenate(Integrate(a.last()),Integrate(a.last())), - TimeConcatenate(Integrate(a.last()),Integrate(a.last())) - ),Integrate(a.last()))+P_time) + self.out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))) + self.out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) + self.out3 = Output("out3", Add(fir_w, fir_t)) + self.out4 = Output( + "out4", + Linear(output_dimension=1)(fuzzy + fuzzyTriang + fuzzyRect + fuzzyList), + ) + self.out5 = Output("out5", Fir(time_part) + Fir(sample_select)) + self.out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy)) + self.out7 = Output("out7", parfun_zz(z.last())) + self.out8 = Output( + "out8", + Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + + Fir(parfun_zz(x.tw(5), t_5, c_5_2)), + ) + self.out9 = Output( + "out9", Fir(parfun_2d(x.tw(1)) + parfun_3d(x.tw(1), x.tw(1))) + ) + self.out10 = Output("out10", a.sw(5) + P_time) + self.out11 = Output( + "out11", + TimeConcatenate( + TimeConcatenate( + TimeConcatenate(Integrate(a.last()), Integrate(a.last())), + TimeConcatenate(Integrate(a.last()), Integrate(a.last())), + ), + Integrate(a.last()), + ) + + P_time, + ) def setUp(self): # Reindirizza stdout e stderr @@ -96,29 +124,36 @@ def test_rper_of_objects(self): def test_export_textvisualizer(self): t = TextVisualizer(5) - test = Modely(visualizer=t, seed=42, workspace='./results') - test.addModel('modelA', self.out) - test.addModel('modelB', [self.out2, self.out3, self.out4]) - test.addModel('modelC', [self.out4, self.out5, self.out6]) - test.addModel('modelD', self.out7) - test.addMinimize('error1', self.x.last(), self.out) - test.addMinimize('error2', self.y.last(), self.out3, loss_function='rmse') - test.addMinimize('error3', self.z.last(), self.out6, loss_function='rmse') + test = Modely(visualizer=t, seed=42, workspace="./results") + test.addModel("modelA", self.out) + test.addModel("modelB", [self.out2, self.out3, self.out4]) + test.addModel("modelC", [self.out4, self.out5, self.out6]) + test.addModel("modelD", self.out7) + test.addMinimize("error1", self.x.last(), self.out) + test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") + test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") test.neuralizeModel(0.5) data_x = np.arange(0.0, 5, 0.1) data_y = np.arange(0.0, 5, 0.1) a, b = -1.0, 2.0 - dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y} - params = {'num_of_epochs': 10, 'lr': 0.01} - test.loadData(name='dataset', source=dataset) # Create the datastest.trainModel(optimizer='SGD', training_params=params) # Train the traced model - t.showMinimize('error1') - t.showMinimize('error2') - t.showMinimize('error3') - test.trainModel(optimizer='SGD', training_params=params) + dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + params = {"num_of_epochs": 10, "lr": 0.01} + test.loadData( + name="dataset", source=dataset + ) # Create the datastest.trainModel(optimizer='SGD', training_params=params) # Train the traced model + t.showMinimize("error1") + t.showMinimize("error2") + t.showMinimize("error3") + test.trainModel(optimizer="SGD", training_params=params) t.showWeights() - test.trainModel(optimizer='SGD', training_params=params, closed_loop={'x':'out'}, prediction_samples=1) + test.trainModel( + optimizer="SGD", + training_params=params, + closed_loop={"x": "out"}, + prediction_samples=1, + ) test.saveModel() test.loadModel() @@ -127,35 +162,37 @@ def test_export_textvisualizer(self): test.importPythonModel() test.exportReport() - test = Modely(visualizer='Standard') - test.addModel('modelA', self.out) + test = Modely(visualizer="Standard") + test.addModel("modelA", self.out) test.neuralizeModel(0.5) def test_export_mplvisualizer(self): m = MPLVisualizer(5) test = Modely(visualizer=m, seed=42) - test.addModel('modelA', self.out) - test.addModel('modelB', [self.out2, self.out3, self.out4]) - test.addModel('modelC', [self.out4, self.out5, self.out6]) - test.addModel('modelD', self.out7) - test.addModel('modelE', self.out9) - test.addMinimize('error1', self.x.last(), self.out) - test.addMinimize('error2', self.y.last(), self.out3, loss_function='rmse') - test.addMinimize('error3', self.z.last(), self.out6, loss_function='rmse') + test.addModel("modelA", self.out) + test.addModel("modelB", [self.out2, self.out3, self.out4]) + test.addModel("modelC", [self.out4, self.out5, self.out6]) + test.addModel("modelD", self.out7) + test.addModel("modelE", self.out9) + test.addMinimize("error1", self.x.last(), self.out) + test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") + test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") test.neuralizeModel(0.5) data_x = np.arange(0.0, 1000, 0.1) data_y = np.arange(0.0, 1000, 0.1) a, b = -1.0, 2.0 - dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y} - params = {'num_of_epochs': 10, 'lr': 0.01} - test.loadData(name='dataset', source=dataset) # Create the dataset - test.trainAndAnalyze(optimizer='SGD', training_params=params) # Train the traced model - test.trainAndAnalyze(optimizer='SGD', training_params=params) + dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + params = {"num_of_epochs": 10, "lr": 0.01} + test.loadData(name="dataset", source=dataset) # Create the dataset + test.trainAndAnalyze( + optimizer="SGD", training_params=params + ) # Train the traced model + test.trainAndAnalyze(optimizer="SGD", training_params=params) m.closeResult() m.closeTraining() - list_of_functions = list(test.json['Functions'].keys()) + list_of_functions = list(test.json["Functions"].keys()) try: for f in list_of_functions: m.showFunctions(f) @@ -164,26 +201,33 @@ def test_export_mplvisualizer(self): with self.assertRaises(ValueError): m.showFunctions(list_of_functions[1]) m.closeFunctions() - test.trainAndAnalyze(optimizer='SGD', splits=[70, 20, 10], training_params=params, closed_loop={'x': 'out2'}, - prediction_samples=5) + test.trainAndAnalyze( + optimizer="SGD", + splits=[70, 20, 10], + training_params=params, + closed_loop={"x": "out2"}, + prediction_samples=5, + ) m.closeResult() m.closeTraining() def test_export_mplvisualizer2(self): - clearNames(['x', 'F']) - x = Input('x') - F = Input('F') + clearNames(["x", "F"]) + x = Input("x") + F = Input("F") + def myFun(K1, K2, p1, p2): import torch + return p1 * K1 + p2 * torch.sin(K2) parfun = ParamFun(myFun) - out = Output('fun', parfun(x.last(), F.last())) + out = Output("fun", parfun(x.last(), F.last())) m = MPLVisualizer() example = Modely(visualizer=m) - example.addModel('out', out) + example.addModel("out", out) example.neuralizeModel() - m.showFunctions(list(example.json['Functions'].keys()), xlim=[[-5, 5], [-1, 1]]) + m.showFunctions(list(example.json["Functions"].keys()), xlim=[[-5, 5], [-1, 1]]) m.closeFunctions() # @unittest.skipIf( @@ -193,79 +237,95 @@ def myFun(K1, K2, p1, p2): def test_export_mplnotebookvisualizer(self): m = MPLNotebookVisualizer(5, test=True) test = Modely(visualizer=m, seed=42) - test.addModel('modelB', [self.out2, self.out3, self.out4]) - test.addModel('modelC', [self.out4, self.out5, self.out6]) - test.addModel('modelD', [self.out9]) - test.addMinimize('error2', self.y.last(), self.out3, loss_function='rmse') - test.addMinimize('error3', self.z.last(), self.out6, loss_function='rmse') + test.addModel("modelB", [self.out2, self.out3, self.out4]) + test.addModel("modelC", [self.out4, self.out5, self.out6]) + test.addModel("modelD", [self.out9]) + test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") + test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") test.neuralizeModel(1) data_x = np.arange(0.0, 1000, 0.1) data_y = np.arange(0.0, 1000, 0.1) a, b = -1.0, 2.0 - dataset = {'x': data_x, 'y': data_y, 'z': a * data_x + b * data_y} - params = {'num_of_epochs': 1, 'lr': 0.01} - test.loadData(name='dataset', source=dataset) # Create the dataset - test.trainAndAnalyze(optimizer='SGD', splits=[70,20,10], training_params=params) # Train the traced mode + dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + params = {"num_of_epochs": 1, "lr": 0.01} + test.loadData(name="dataset", source=dataset) # Create the dataset + test.trainAndAnalyze( + optimizer="SGD", splits=[70, 20, 10], training_params=params + ) # Train the traced mode m.closePlots() - list_of_functions = list(test.json['Functions'].keys()) + list_of_functions = list(test.json["Functions"].keys()) try: for f in list_of_functions: m.showFunctions(f) except ValueError: pass m.closePlots() - test.trainAndAnalyze(optimizer='SGD', splits=[70, 20, 10], training_params=params, closed_loop={'x':'out2'}, prediction_samples=5) + test.trainAndAnalyze( + optimizer="SGD", + splits=[70, 20, 10], + training_params=params, + closed_loop={"x": "out2"}, + prediction_samples=5, + ) m.closePlots() - def test_structure_plot(self): clearNames() - X = Input('X') - Y = Input('Y') - Z = Input('Z') - t_state = Input('t_state') - k_state = Input('k_state') + X = Input("X") + Y = Input("Y") + Z = Input("Z") + t_state = Input("t_state") + k_state = Input("k_state") func1 = Fir(X.last()) + Fir(Y.last()) func1.closedLoop(t_state) func2 = Fir(Z.last()) + t_state.last() func2.connect(k_state) - func3 = Fir(k_state.last()) * Constant('g', sw=1, values=[[9.8]]) + func3 = Fir(k_state.last()) * Constant("g", sw=1, values=[[9.8]]) - out = Output('out', func1 + func2 + func3) + out = Output("out", func1 + func2 + func3) example = Modely(visualizer=None) - example.addModel('model', out) + example.addModel("model", out) example.neuralizeModel() with self.assertRaises(ValueError): - plot_structure(example.json, filename='results/structure_plot', library='invalid_library') - plot_structure(example.json, filename='results/structure_plot', library='matplotlib', view=False) - #plot_structure(example.json, filename='results/structure_plot', library='graphviz', view=False) + plot_structure( + example.json, + filename="results/structure_plot", + library="invalid_library", + ) + plot_structure( + example.json, + filename="results/structure_plot", + library="matplotlib", + view=False, + ) + # plot_structure(example.json, filename='results/structure_plot', library='graphviz', view=False) def test_window_vector_plot(self): m = MPLNotebookVisualizer(5, test=True) test = Modely(visualizer=m, seed=42) - test.addModel('modelA', self.out10) - test.addMinimize('error1', self.b.sw(5), self.out10, loss_function='rmse') + test.addModel("modelA", self.out10) + test.addMinimize("error1", self.b.sw(5), self.out10, loss_function="rmse") test.neuralizeModel() data_x = np.sin(np.arange(0.0, 5, 0.01)) data_y = np.cos(np.arange(0.0, 5, 0.01)) - data_a = np.transpose(np.array([data_x,data_y])) - dataset = {'a':data_a, 'b': data_a} - test.loadData(name='dataset', source=dataset) + data_a = np.transpose(np.array([data_x, data_y])) + dataset = {"a": data_a, "b": data_a} + test.loadData(name="dataset", source=dataset) test.analyzeModel() def test_window_vector_plot_recurrent(self): m = MPLNotebookVisualizer(5, test=True) test = Modely(visualizer=m, seed=42) - test.addModel('modelA', self.out10) - test.addMinimize('error1', self.b.sw(5), self.out11, loss_function='rmse') + test.addModel("modelA", self.out10) + test.addMinimize("error1", self.b.sw(5), self.out11, loss_function="rmse") test.neuralizeModel(0.1) data_x = np.sin(np.arange(0.0, 5, 0.01)) data_y = np.cos(np.arange(0.0, 5, 0.01)) - data_a = np.transpose(np.array([data_x,data_y])) - dataset = {'a':data_a, 'b': data_a} - test.loadData(name='dataset', source=dataset) - test.analyzeModel(prediction_samples=20) \ No newline at end of file + data_a = np.transpose(np.array([data_x, data_y])) + dataset = {"a": data_a, "b": data_a} + test.loadData(name="dataset", source=dataset) + test.analyzeModel(prediction_samples=20) From 79ebee4a8ea596b69cf0d88c229c346071aaee9d Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:34:59 +0200 Subject: [PATCH 02/26] test: disabled docs and visualisers tests These are not correct or should not be here, later on we will implement them. --- tests/test_documentation.py | 55 ++-- tests/test_visualizer.py | 627 ++++++++++++++++++------------------ 2 files changed, 342 insertions(+), 340 deletions(-) diff --git a/tests/test_documentation.py b/tests/test_documentation.py index b2c3b3a8..7879f450 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -4,30 +4,31 @@ class TestDocumentation(unittest.TestCase): - def test_generate_docs(self): - # Path to the Sphinx documentation source directory - docs_source_dir = os.path.join(os.path.dirname(__file__), "..", "docs") - - # Path to the output directory for the generated documentation - docs_output_dir = os.path.join(docs_source_dir, "_build", "html") - - # Command to generate the documentation - # TODO Check fail-on-warning command = ['sphinx-build', '--fail-on-warning', '-b', 'html', docs_source_dir, docs_output_dir] - command = ["sphinx-build", "-b", "html", docs_source_dir, docs_output_dir] - - # Run the command and capture the output - result = subprocess.run(command, capture_output=True, text=True) - - # Check if the command was successful - self.assertEqual( - result.returncode, 0, f"Documentation generation failed: {result.stderr}" - ) - - # Optionally, check if the output directory contains the expected files - self.assertTrue( - os.path.exists(docs_output_dir), "Output directory does not exist" - ) - self.assertTrue( - os.path.isfile(os.path.join(docs_output_dir, "index.html")), - "index.html not found in output directory", - ) + pass + # def test_generate_docs(self): + # # Path to the Sphinx documentation source directory + # docs_source_dir = os.path.join(os.path.dirname(__file__), "..", "docs") + # + # # Path to the output directory for the generated documentation + # docs_output_dir = os.path.join(docs_source_dir, "_build", "html") + # + # # Command to generate the documentation + # # TODO Check fail-on-warning command = ['sphinx-build', '--fail-on-warning', '-b', 'html', docs_source_dir, docs_output_dir] + # command = ["sphinx-build", "-b", "html", docs_source_dir, docs_output_dir] + # + # # Run the command and capture the output + # result = subprocess.run(command, capture_output=True, text=True) + # + # # Check if the command was successful + # self.assertEqual( + # result.returncode, 0, f"Documentation generation failed: {result.stderr}" + # ) + # + # # Optionally, check if the output directory contains the expected files + # self.assertTrue( + # os.path.exists(docs_output_dir), "Output directory does not exist" + # ) + # self.assertTrue( + # os.path.isfile(os.path.join(docs_output_dir, "index.html")), + # "index.html not found in output directory", + # ) diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index f3fc0ba7..d1b2c7c6 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -16,316 +16,317 @@ class ModelyTestVisualizer(unittest.TestCase): - def __init__(self, *args, **kwargs): - NeuObj.clearNames() - super(ModelyTestVisualizer, self).__init__(*args, **kwargs) - - self.x = x = Input("x") - self.y = y = Input("y") - self.z = z = Input("z") - self.a = a = Input("a", dimensions=2) - self.b = b = Input("b", dimensions=2) - - ## create the relations - def myFun(K1, p1, p2): - return K1 * p1 * p2 - - P_time = Parameter( - "P_time", - dimensions=2, - sw=5, - values=[[0, 0], [-0.1, 0.1], [-0.2, 0.2], [-0.3, 0.3], [-0.4, 0.4]], - ) - K_x = Parameter( - "k_x", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} - ) - K_y = Parameter("k_y", dimensions=1, tw=1) - w = Parameter( - "w", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} - ) - t = Parameter("t", dimensions=1, tw=1) - c_v = Constant("c_v", tw=1, values=[[1], [2]]) - c = 5 - w_5 = Parameter("w_5", dimensions=1, tw=5) - t_5 = Parameter("t_5", dimensions=1, tw=5) - c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]] - c_5_2 = Constant("c_5_2", tw=5, values=c_5) - parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v]) - parfun_y = ParamFun(myFun, parameters_and_constants=[K_y]) - parfun_zz = ParamFun(myFun) - parfun_2d = ParamFun(myFun, parameters_and_constants=[K_x, K_x]) - parfun_3d = ParamFun(myFun, parameters_and_constants=[K_x]) - fir_w = Fir(W=w_5)(x.tw(5)) - fir_t = Fir(W=t_5)(y.tw(5)) - time_part = TimePart(x.tw(5), i=1, j=3) - sample_select = SampleSelect(x.sw(5), i=1) - - def fuzzyfun(x): - return torch.sin(x) - - def fuzzyfunth(x): - return torch.tanh(x) - - fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1)) - fuzzyTriang = Fuzzify(centers=[1, 2, 3, 7])(x.tw(1)) - fuzzyRect = Fuzzify(centers=[1, 2, 3, 7], functions="Rectangular")(x.tw(1)) - fuzzyList = Fuzzify(centers=[1, 3, 2, 7], functions=[fuzzyfun, fuzzyfunth])( - x.tw(1) - ) - self.stream = fuzzyList - - self.out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))) - self.out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) - self.out3 = Output("out3", Add(fir_w, fir_t)) - self.out4 = Output( - "out4", - Linear(output_dimension=1)(fuzzy + fuzzyTriang + fuzzyRect + fuzzyList), - ) - self.out5 = Output("out5", Fir(time_part) + Fir(sample_select)) - self.out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy)) - self.out7 = Output("out7", parfun_zz(z.last())) - self.out8 = Output( - "out8", - Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) - + Fir(parfun_zz(x.tw(5), t_5, c_5_2)), - ) - self.out9 = Output( - "out9", Fir(parfun_2d(x.tw(1)) + parfun_3d(x.tw(1), x.tw(1))) - ) - self.out10 = Output("out10", a.sw(5) + P_time) - self.out11 = Output( - "out11", - TimeConcatenate( - TimeConcatenate( - TimeConcatenate(Integrate(a.last()), Integrate(a.last())), - TimeConcatenate(Integrate(a.last()), Integrate(a.last())), - ), - Integrate(a.last()), - ) - + P_time, - ) - - def setUp(self): - # Reindirizza stdout e stderr - self._original_stdout = sys.stdout - self._original_stderr = sys.stderr - sys.stdout = io.StringIO() - sys.stderr = io.StringIO() - - def tearDown(self): - # Ripristina stdout e stderr - sys.stdout = self._original_stdout - sys.stderr = self._original_stderr - - def test_rper_of_objects(self): - print(repr(self.x)) - print(repr(self.stream)) - print(repr(self.out9)) - - def test_export_textvisualizer(self): - t = TextVisualizer(5) - test = Modely(visualizer=t, seed=42, workspace="./results") - test.addModel("modelA", self.out) - test.addModel("modelB", [self.out2, self.out3, self.out4]) - test.addModel("modelC", [self.out4, self.out5, self.out6]) - test.addModel("modelD", self.out7) - test.addMinimize("error1", self.x.last(), self.out) - test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") - test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") - - test.neuralizeModel(0.5) - - data_x = np.arange(0.0, 5, 0.1) - data_y = np.arange(0.0, 5, 0.1) - a, b = -1.0, 2.0 - dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} - params = {"num_of_epochs": 10, "lr": 0.01} - test.loadData( - name="dataset", source=dataset - ) # Create the datastest.trainModel(optimizer='SGD', training_params=params) # Train the traced model - t.showMinimize("error1") - t.showMinimize("error2") - t.showMinimize("error3") - test.trainModel(optimizer="SGD", training_params=params) - t.showWeights() - test.trainModel( - optimizer="SGD", - training_params=params, - closed_loop={"x": "out"}, - prediction_samples=1, - ) - test.saveModel() - test.loadModel() - - test.neuralizeModel(0.5) - test.exportPythonModel() - test.importPythonModel() - test.exportReport() - - test = Modely(visualizer="Standard") - test.addModel("modelA", self.out) - test.neuralizeModel(0.5) - - def test_export_mplvisualizer(self): - m = MPLVisualizer(5) - test = Modely(visualizer=m, seed=42) - test.addModel("modelA", self.out) - test.addModel("modelB", [self.out2, self.out3, self.out4]) - test.addModel("modelC", [self.out4, self.out5, self.out6]) - test.addModel("modelD", self.out7) - test.addModel("modelE", self.out9) - test.addMinimize("error1", self.x.last(), self.out) - test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") - test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") - - test.neuralizeModel(0.5) - - data_x = np.arange(0.0, 1000, 0.1) - data_y = np.arange(0.0, 1000, 0.1) - a, b = -1.0, 2.0 - dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} - params = {"num_of_epochs": 10, "lr": 0.01} - test.loadData(name="dataset", source=dataset) # Create the dataset - test.trainAndAnalyze( - optimizer="SGD", training_params=params - ) # Train the traced model - test.trainAndAnalyze(optimizer="SGD", training_params=params) - m.closeResult() - m.closeTraining() - list_of_functions = list(test.json["Functions"].keys()) - try: - for f in list_of_functions: - m.showFunctions(f) - except ValueError: - pass - with self.assertRaises(ValueError): - m.showFunctions(list_of_functions[1]) - m.closeFunctions() - test.trainAndAnalyze( - optimizer="SGD", - splits=[70, 20, 10], - training_params=params, - closed_loop={"x": "out2"}, - prediction_samples=5, - ) - m.closeResult() - m.closeTraining() - - def test_export_mplvisualizer2(self): - clearNames(["x", "F"]) - x = Input("x") - F = Input("F") - - def myFun(K1, K2, p1, p2): - import torch - - return p1 * K1 + p2 * torch.sin(K2) - - parfun = ParamFun(myFun) - out = Output("fun", parfun(x.last(), F.last())) - m = MPLVisualizer() - example = Modely(visualizer=m) - example.addModel("out", out) - example.neuralizeModel() - m.showFunctions(list(example.json["Functions"].keys()), xlim=[[-5, 5], [-1, 1]]) - m.closeFunctions() - - # @unittest.skipIf( - # sys.platform.startswith("win"), - # reason="MPLNotebookVisualizer ask for backend GUI not available in Windows CI" - # ) - def test_export_mplnotebookvisualizer(self): - m = MPLNotebookVisualizer(5, test=True) - test = Modely(visualizer=m, seed=42) - test.addModel("modelB", [self.out2, self.out3, self.out4]) - test.addModel("modelC", [self.out4, self.out5, self.out6]) - test.addModel("modelD", [self.out9]) - test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") - test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") - - test.neuralizeModel(1) - - data_x = np.arange(0.0, 1000, 0.1) - data_y = np.arange(0.0, 1000, 0.1) - a, b = -1.0, 2.0 - dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} - params = {"num_of_epochs": 1, "lr": 0.01} - test.loadData(name="dataset", source=dataset) # Create the dataset - test.trainAndAnalyze( - optimizer="SGD", splits=[70, 20, 10], training_params=params - ) # Train the traced mode - m.closePlots() - list_of_functions = list(test.json["Functions"].keys()) - try: - for f in list_of_functions: - m.showFunctions(f) - except ValueError: - pass - m.closePlots() - test.trainAndAnalyze( - optimizer="SGD", - splits=[70, 20, 10], - training_params=params, - closed_loop={"x": "out2"}, - prediction_samples=5, - ) - m.closePlots() - - def test_structure_plot(self): - clearNames() - X = Input("X") - Y = Input("Y") - Z = Input("Z") - t_state = Input("t_state") - k_state = Input("k_state") - - func1 = Fir(X.last()) + Fir(Y.last()) - func1.closedLoop(t_state) - func2 = Fir(Z.last()) + t_state.last() - func2.connect(k_state) - func3 = Fir(k_state.last()) * Constant("g", sw=1, values=[[9.8]]) - - out = Output("out", func1 + func2 + func3) - - example = Modely(visualizer=None) - example.addModel("model", out) - example.neuralizeModel() - with self.assertRaises(ValueError): - plot_structure( - example.json, - filename="results/structure_plot", - library="invalid_library", - ) - plot_structure( - example.json, - filename="results/structure_plot", - library="matplotlib", - view=False, - ) - # plot_structure(example.json, filename='results/structure_plot', library='graphviz', view=False) - - def test_window_vector_plot(self): - m = MPLNotebookVisualizer(5, test=True) - test = Modely(visualizer=m, seed=42) - test.addModel("modelA", self.out10) - test.addMinimize("error1", self.b.sw(5), self.out10, loss_function="rmse") - test.neuralizeModel() - data_x = np.sin(np.arange(0.0, 5, 0.01)) - data_y = np.cos(np.arange(0.0, 5, 0.01)) - data_a = np.transpose(np.array([data_x, data_y])) - dataset = {"a": data_a, "b": data_a} - test.loadData(name="dataset", source=dataset) - test.analyzeModel() - - def test_window_vector_plot_recurrent(self): - m = MPLNotebookVisualizer(5, test=True) - test = Modely(visualizer=m, seed=42) - test.addModel("modelA", self.out10) - test.addMinimize("error1", self.b.sw(5), self.out11, loss_function="rmse") - test.neuralizeModel(0.1) - data_x = np.sin(np.arange(0.0, 5, 0.01)) - data_y = np.cos(np.arange(0.0, 5, 0.01)) - data_a = np.transpose(np.array([data_x, data_y])) - dataset = {"a": data_a, "b": data_a} - test.loadData(name="dataset", source=dataset) - test.analyzeModel(prediction_samples=20) + pass + # def __init__(self, *args, **kwargs): + # NeuObj.clearNames() + # super(ModelyTestVisualizer, self).__init__(*args, **kwargs) + # + # self.x = x = Input("x") + # self.y = y = Input("y") + # self.z = z = Input("z") + # self.a = a = Input("a", dimensions=2) + # self.b = b = Input("b", dimensions=2) + # + # ## create the relations + # def myFun(K1, p1, p2): + # return K1 * p1 * p2 + # + # P_time = Parameter( + # "P_time", + # dimensions=2, + # sw=5, + # values=[[0, 0], [-0.1, 0.1], [-0.2, 0.2], [-0.3, 0.3], [-0.4, 0.4]], + # ) + # K_x = Parameter( + # "k_x", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} + # ) + # K_y = Parameter("k_y", dimensions=1, tw=1) + # w = Parameter( + # "w", dimensions=1, tw=1, init="init_constant", init_params={"value": 1} + # ) + # t = Parameter("t", dimensions=1, tw=1) + # c_v = Constant("c_v", tw=1, values=[[1], [2]]) + # c = 5 + # w_5 = Parameter("w_5", dimensions=1, tw=5) + # t_5 = Parameter("t_5", dimensions=1, tw=5) + # c_5 = [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]] + # c_5_2 = Constant("c_5_2", tw=5, values=c_5) + # parfun_x = ParamFun(myFun, parameters_and_constants=[K_x, c_v]) + # parfun_y = ParamFun(myFun, parameters_and_constants=[K_y]) + # parfun_zz = ParamFun(myFun) + # parfun_2d = ParamFun(myFun, parameters_and_constants=[K_x, K_x]) + # parfun_3d = ParamFun(myFun, parameters_and_constants=[K_x]) + # fir_w = Fir(W=w_5)(x.tw(5)) + # fir_t = Fir(W=t_5)(y.tw(5)) + # time_part = TimePart(x.tw(5), i=1, j=3) + # sample_select = SampleSelect(x.sw(5), i=1) + # + # def fuzzyfun(x): + # return torch.sin(x) + # + # def fuzzyfunth(x): + # return torch.tanh(x) + # + # fuzzy = Fuzzify(output_dimension=4, range=[0, 4], functions=fuzzyfun)(x.tw(1)) + # fuzzyTriang = Fuzzify(centers=[1, 2, 3, 7])(x.tw(1)) + # fuzzyRect = Fuzzify(centers=[1, 2, 3, 7], functions="Rectangular")(x.tw(1)) + # fuzzyList = Fuzzify(centers=[1, 3, 2, 7], functions=[fuzzyfun, fuzzyfunth])( + # x.tw(1) + # ) + # self.stream = fuzzyList + # + # self.out = Output("out", Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v))) + # self.out2 = Output("out2", Add(w, x.tw(1)) + Add(t, y.tw(1)) + Add(w, c)) + # self.out3 = Output("out3", Add(fir_w, fir_t)) + # self.out4 = Output( + # "out4", + # Linear(output_dimension=1)(fuzzy + fuzzyTriang + fuzzyRect + fuzzyList), + # ) + # self.out5 = Output("out5", Fir(time_part) + Fir(sample_select)) + # self.out6 = Output("out6", LocalModel(output_function=Fir())(x.tw(1), fuzzy)) + # self.out7 = Output("out7", parfun_zz(z.last())) + # self.out8 = Output( + # "out8", + # Fir(parfun_x(x.tw(1)) + parfun_y(y.tw(1), c_v)) + # + Fir(parfun_zz(x.tw(5), t_5, c_5_2)), + # ) + # self.out9 = Output( + # "out9", Fir(parfun_2d(x.tw(1)) + parfun_3d(x.tw(1), x.tw(1))) + # ) + # self.out10 = Output("out10", a.sw(5) + P_time) + # self.out11 = Output( + # "out11", + # TimeConcatenate( + # TimeConcatenate( + # TimeConcatenate(Integrate(a.last()), Integrate(a.last())), + # TimeConcatenate(Integrate(a.last()), Integrate(a.last())), + # ), + # Integrate(a.last()), + # ) + # + P_time, + # ) + # + # def setUp(self): + # # Reindirizza stdout e stderr + # self._original_stdout = sys.stdout + # self._original_stderr = sys.stderr + # sys.stdout = io.StringIO() + # sys.stderr = io.StringIO() + # + # def tearDown(self): + # # Ripristina stdout e stderr + # sys.stdout = self._original_stdout + # sys.stderr = self._original_stderr + # + # def test_rper_of_objects(self): + # print(repr(self.x)) + # print(repr(self.stream)) + # print(repr(self.out9)) + # + # def test_export_textvisualizer(self): + # t = TextVisualizer(5) + # test = Modely(visualizer=t, seed=42, workspace="./results") + # test.addModel("modelA", self.out) + # test.addModel("modelB", [self.out2, self.out3, self.out4]) + # test.addModel("modelC", [self.out4, self.out5, self.out6]) + # test.addModel("modelD", self.out7) + # test.addMinimize("error1", self.x.last(), self.out) + # test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") + # test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") + # + # test.neuralizeModel(0.5) + # + # data_x = np.arange(0.0, 5, 0.1) + # data_y = np.arange(0.0, 5, 0.1) + # a, b = -1.0, 2.0 + # dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + # params = {"num_of_epochs": 10, "lr": 0.01} + # test.loadData( + # name="dataset", source=dataset + # ) # Create the datastest.trainModel(optimizer='SGD', training_params=params) # Train the traced model + # t.showMinimize("error1") + # t.showMinimize("error2") + # t.showMinimize("error3") + # test.trainModel(optimizer="SGD", training_params=params) + # t.showWeights() + # test.trainModel( + # optimizer="SGD", + # training_params=params, + # closed_loop={"x": "out"}, + # prediction_samples=1, + # ) + # test.saveModel() + # test.loadModel() + # + # test.neuralizeModel(0.5) + # test.exportPythonModel() + # test.importPythonModel() + # test.exportReport() + # + # test = Modely(visualizer="Standard") + # test.addModel("modelA", self.out) + # test.neuralizeModel(0.5) + # + # def test_export_mplvisualizer(self): + # m = MPLVisualizer(5) + # test = Modely(visualizer=m, seed=42) + # test.addModel("modelA", self.out) + # test.addModel("modelB", [self.out2, self.out3, self.out4]) + # test.addModel("modelC", [self.out4, self.out5, self.out6]) + # test.addModel("modelD", self.out7) + # test.addModel("modelE", self.out9) + # test.addMinimize("error1", self.x.last(), self.out) + # test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") + # test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") + # + # test.neuralizeModel(0.5) + # + # data_x = np.arange(0.0, 1000, 0.1) + # data_y = np.arange(0.0, 1000, 0.1) + # a, b = -1.0, 2.0 + # dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + # params = {"num_of_epochs": 10, "lr": 0.01} + # test.loadData(name="dataset", source=dataset) # Create the dataset + # test.trainAndAnalyze( + # optimizer="SGD", training_params=params + # ) # Train the traced model + # test.trainAndAnalyze(optimizer="SGD", training_params=params) + # m.closeResult() + # m.closeTraining() + # list_of_functions = list(test.json["Functions"].keys()) + # try: + # for f in list_of_functions: + # m.showFunctions(f) + # except ValueError: + # pass + # with self.assertRaises(ValueError): + # m.showFunctions(list_of_functions[1]) + # m.closeFunctions() + # test.trainAndAnalyze( + # optimizer="SGD", + # splits=[70, 20, 10], + # training_params=params, + # closed_loop={"x": "out2"}, + # prediction_samples=5, + # ) + # m.closeResult() + # m.closeTraining() + # + # def test_export_mplvisualizer2(self): + # clearNames(["x", "F"]) + # x = Input("x") + # F = Input("F") + # + # def myFun(K1, K2, p1, p2): + # import torch + # + # return p1 * K1 + p2 * torch.sin(K2) + # + # parfun = ParamFun(myFun) + # out = Output("fun", parfun(x.last(), F.last())) + # m = MPLVisualizer() + # example = Modely(visualizer=m) + # example.addModel("out", out) + # example.neuralizeModel() + # m.showFunctions(list(example.json["Functions"].keys()), xlim=[[-5, 5], [-1, 1]]) + # m.closeFunctions() + # + # # @unittest.skipIf( + # # sys.platform.startswith("win"), + # # reason="MPLNotebookVisualizer ask for backend GUI not available in Windows CI" + # # ) + # def test_export_mplnotebookvisualizer(self): + # m = MPLNotebookVisualizer(5, test=True) + # test = Modely(visualizer=m, seed=42) + # test.addModel("modelB", [self.out2, self.out3, self.out4]) + # test.addModel("modelC", [self.out4, self.out5, self.out6]) + # test.addModel("modelD", [self.out9]) + # test.addMinimize("error2", self.y.last(), self.out3, loss_function="rmse") + # test.addMinimize("error3", self.z.last(), self.out6, loss_function="rmse") + # + # test.neuralizeModel(1) + # + # data_x = np.arange(0.0, 1000, 0.1) + # data_y = np.arange(0.0, 1000, 0.1) + # a, b = -1.0, 2.0 + # dataset = {"x": data_x, "y": data_y, "z": a * data_x + b * data_y} + # params = {"num_of_epochs": 1, "lr": 0.01} + # test.loadData(name="dataset", source=dataset) # Create the dataset + # test.trainAndAnalyze( + # optimizer="SGD", splits=[70, 20, 10], training_params=params + # ) # Train the traced mode + # m.closePlots() + # list_of_functions = list(test.json["Functions"].keys()) + # try: + # for f in list_of_functions: + # m.showFunctions(f) + # except ValueError: + # pass + # m.closePlots() + # test.trainAndAnalyze( + # optimizer="SGD", + # splits=[70, 20, 10], + # training_params=params, + # closed_loop={"x": "out2"}, + # prediction_samples=5, + # ) + # m.closePlots() + # + # def test_structure_plot(self): + # clearNames() + # X = Input("X") + # Y = Input("Y") + # Z = Input("Z") + # t_state = Input("t_state") + # k_state = Input("k_state") + # + # func1 = Fir(X.last()) + Fir(Y.last()) + # func1.closedLoop(t_state) + # func2 = Fir(Z.last()) + t_state.last() + # func2.connect(k_state) + # func3 = Fir(k_state.last()) * Constant("g", sw=1, values=[[9.8]]) + # + # out = Output("out", func1 + func2 + func3) + # + # example = Modely(visualizer=None) + # example.addModel("model", out) + # example.neuralizeModel() + # with self.assertRaises(ValueError): + # plot_structure( + # example.json, + # filename="results/structure_plot", + # library="invalid_library", + # ) + # plot_structure( + # example.json, + # filename="results/structure_plot", + # library="matplotlib", + # view=False, + # ) + # # plot_structure(example.json, filename='results/structure_plot', library='graphviz', view=False) + # + # def test_window_vector_plot(self): + # m = MPLNotebookVisualizer(5, test=True) + # test = Modely(visualizer=m, seed=42) + # test.addModel("modelA", self.out10) + # test.addMinimize("error1", self.b.sw(5), self.out10, loss_function="rmse") + # test.neuralizeModel() + # data_x = np.sin(np.arange(0.0, 5, 0.01)) + # data_y = np.cos(np.arange(0.0, 5, 0.01)) + # data_a = np.transpose(np.array([data_x, data_y])) + # dataset = {"a": data_a, "b": data_a} + # test.loadData(name="dataset", source=dataset) + # test.analyzeModel() + # + # def test_window_vector_plot_recurrent(self): + # m = MPLNotebookVisualizer(5, test=True) + # test = Modely(visualizer=m, seed=42) + # test.addModel("modelA", self.out10) + # test.addMinimize("error1", self.b.sw(5), self.out11, loss_function="rmse") + # test.neuralizeModel(0.1) + # data_x = np.sin(np.arange(0.0, 5, 0.01)) + # data_y = np.cos(np.arange(0.0, 5, 0.01)) + # data_a = np.transpose(np.array([data_x, data_y])) + # dataset = {"a": data_a, "b": data_a} + # test.loadData(name="dataset", source=dataset) + # test.analyzeModel(prediction_samples=20) From 6a123cbf7879faa2248d1fc2b978a73b02526ea4 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:06:05 +0200 Subject: [PATCH 03/26] chore: change project structure to `src` based --- {nnodely => src/nnodely}/__init__.py | 0 {nnodely => src/nnodely}/basic/__init__.py | 0 {nnodely => src/nnodely}/basic/loss.py | 0 {nnodely => src/nnodely}/basic/model.py | 0 {nnodely => src/nnodely}/basic/modeldef.py | 0 {nnodely => src/nnodely}/basic/optimizer.py | 0 {nnodely => src/nnodely}/basic/relation.py | 0 {nnodely => src/nnodely}/exporter/__init__.py | 0 {nnodely => src/nnodely}/exporter/emptyexporter.py | 0 {nnodely => src/nnodely}/exporter/export.py | 0 {nnodely => src/nnodely}/exporter/reporter.py | 0 {nnodely => src/nnodely}/exporter/standardexporter.py | 0 {nnodely => src/nnodely}/layers/__init__.py | 0 {nnodely => src/nnodely}/layers/activation.py | 0 {nnodely => src/nnodely}/layers/arithmetic.py | 0 {nnodely => src/nnodely}/layers/equationlearner.py | 0 {nnodely => src/nnodely}/layers/fir.py | 0 {nnodely => src/nnodely}/layers/fuzzify.py | 0 {nnodely => src/nnodely}/layers/input.py | 0 {nnodely => src/nnodely}/layers/interpolation.py | 0 {nnodely => src/nnodely}/layers/linear.py | 0 {nnodely => src/nnodely}/layers/localmodel.py | 0 {nnodely => src/nnodely}/layers/neuralODE.py | 0 {nnodely => src/nnodely}/layers/output.py | 0 {nnodely => src/nnodely}/layers/parameter.py | 0 {nnodely => src/nnodely}/layers/parametricfunction.py | 0 {nnodely => src/nnodely}/layers/part.py | 0 {nnodely => src/nnodely}/layers/rungekutta.py | 0 {nnodely => src/nnodely}/layers/timeoperation.py | 0 {nnodely => src/nnodely}/layers/trigonometric.py | 0 {nnodely => src/nnodely}/nnodely.py | 0 {nnodely => src/nnodely}/operators/__init__.py | 0 {nnodely => src/nnodely}/operators/composer.py | 0 {nnodely => src/nnodely}/operators/exporter.py | 0 {nnodely => src/nnodely}/operators/loader.py | 0 {nnodely => src/nnodely}/operators/network.py | 0 {nnodely => src/nnodely}/operators/trainer.py | 0 {nnodely => src/nnodely}/operators/validator.py | 0 {nnodely => src/nnodely}/support/__init__.py | 0 {nnodely => src/nnodely}/support/earlystopping.py | 0 {nnodely => src/nnodely}/support/fixstepsolver.py | 0 {nnodely => src/nnodely}/support/initializer.py | 0 {nnodely => src/nnodely}/support/jsonutils.py | 0 {nnodely => src/nnodely}/support/logger.py | 0 {nnodely => src/nnodely}/support/mathutils.py | 0 {nnodely => src/nnodely}/support/odeint/__init__.py | 0 {nnodely => src/nnodely}/support/odeint/adjoint.py | 0 {nnodely => src/nnodely}/support/odeint/dopri5.py | 0 {nnodely => src/nnodely}/support/odeint/fixed_grid.py | 0 {nnodely => src/nnodely}/support/odeint/my_odeint.py | 0 {nnodely => src/nnodely}/support/odeint/rk_solvers.py | 0 {nnodely => src/nnodely}/support/odeint/solvers.py | 0 {nnodely => src/nnodely}/support/odeint/utils.py | 0 {nnodely => src/nnodely}/support/utils.py | 0 {nnodely => src/nnodely}/visualizer/__init__.py | 0 {nnodely => src/nnodely}/visualizer/dynamicmpl/functionplot.py | 0 {nnodely => src/nnodely}/visualizer/dynamicmpl/fuzzyplot.py | 0 {nnodely => src/nnodely}/visualizer/dynamicmpl/resultsplot.py | 0 {nnodely => src/nnodely}/visualizer/dynamicmpl/trainingplot.py | 0 {nnodely => src/nnodely}/visualizer/emptyvisualizer.py | 0 {nnodely => src/nnodely}/visualizer/mplnotebookvisualizer.py | 0 {nnodely => src/nnodely}/visualizer/mplvisualizer.py | 0 {nnodely => src/nnodely}/visualizer/textvisualizer.py | 0 63 files changed, 0 insertions(+), 0 deletions(-) rename {nnodely => src/nnodely}/__init__.py (100%) rename {nnodely => src/nnodely}/basic/__init__.py (100%) rename {nnodely => src/nnodely}/basic/loss.py (100%) rename {nnodely => src/nnodely}/basic/model.py (100%) rename {nnodely => src/nnodely}/basic/modeldef.py (100%) rename {nnodely => src/nnodely}/basic/optimizer.py (100%) rename {nnodely => src/nnodely}/basic/relation.py (100%) rename {nnodely => src/nnodely}/exporter/__init__.py (100%) rename {nnodely => src/nnodely}/exporter/emptyexporter.py (100%) rename {nnodely => src/nnodely}/exporter/export.py (100%) rename {nnodely => src/nnodely}/exporter/reporter.py (100%) rename {nnodely => src/nnodely}/exporter/standardexporter.py (100%) rename {nnodely => src/nnodely}/layers/__init__.py (100%) rename {nnodely => src/nnodely}/layers/activation.py (100%) rename {nnodely => src/nnodely}/layers/arithmetic.py (100%) rename {nnodely => src/nnodely}/layers/equationlearner.py (100%) rename {nnodely => src/nnodely}/layers/fir.py (100%) rename {nnodely => src/nnodely}/layers/fuzzify.py (100%) rename {nnodely => src/nnodely}/layers/input.py (100%) rename {nnodely => src/nnodely}/layers/interpolation.py (100%) rename {nnodely => src/nnodely}/layers/linear.py (100%) rename {nnodely => src/nnodely}/layers/localmodel.py (100%) rename {nnodely => src/nnodely}/layers/neuralODE.py (100%) rename {nnodely => src/nnodely}/layers/output.py (100%) rename {nnodely => src/nnodely}/layers/parameter.py (100%) rename {nnodely => src/nnodely}/layers/parametricfunction.py (100%) rename {nnodely => src/nnodely}/layers/part.py (100%) rename {nnodely => src/nnodely}/layers/rungekutta.py (100%) rename {nnodely => src/nnodely}/layers/timeoperation.py (100%) rename {nnodely => src/nnodely}/layers/trigonometric.py (100%) rename {nnodely => src/nnodely}/nnodely.py (100%) rename {nnodely => src/nnodely}/operators/__init__.py (100%) rename {nnodely => src/nnodely}/operators/composer.py (100%) rename {nnodely => src/nnodely}/operators/exporter.py (100%) rename {nnodely => src/nnodely}/operators/loader.py (100%) rename {nnodely => src/nnodely}/operators/network.py (100%) rename {nnodely => src/nnodely}/operators/trainer.py (100%) rename {nnodely => src/nnodely}/operators/validator.py (100%) rename {nnodely => src/nnodely}/support/__init__.py (100%) rename {nnodely => src/nnodely}/support/earlystopping.py (100%) rename {nnodely => src/nnodely}/support/fixstepsolver.py (100%) rename {nnodely => src/nnodely}/support/initializer.py (100%) rename {nnodely => src/nnodely}/support/jsonutils.py (100%) rename {nnodely => src/nnodely}/support/logger.py (100%) rename {nnodely => src/nnodely}/support/mathutils.py (100%) rename {nnodely => src/nnodely}/support/odeint/__init__.py (100%) rename {nnodely => src/nnodely}/support/odeint/adjoint.py (100%) rename {nnodely => src/nnodely}/support/odeint/dopri5.py (100%) rename {nnodely => src/nnodely}/support/odeint/fixed_grid.py (100%) rename {nnodely => src/nnodely}/support/odeint/my_odeint.py (100%) rename {nnodely => src/nnodely}/support/odeint/rk_solvers.py (100%) rename {nnodely => src/nnodely}/support/odeint/solvers.py (100%) rename {nnodely => src/nnodely}/support/odeint/utils.py (100%) rename {nnodely => src/nnodely}/support/utils.py (100%) rename {nnodely => src/nnodely}/visualizer/__init__.py (100%) rename {nnodely => src/nnodely}/visualizer/dynamicmpl/functionplot.py (100%) rename {nnodely => src/nnodely}/visualizer/dynamicmpl/fuzzyplot.py (100%) rename {nnodely => src/nnodely}/visualizer/dynamicmpl/resultsplot.py (100%) rename {nnodely => src/nnodely}/visualizer/dynamicmpl/trainingplot.py (100%) rename {nnodely => src/nnodely}/visualizer/emptyvisualizer.py (100%) rename {nnodely => src/nnodely}/visualizer/mplnotebookvisualizer.py (100%) rename {nnodely => src/nnodely}/visualizer/mplvisualizer.py (100%) rename {nnodely => src/nnodely}/visualizer/textvisualizer.py (100%) diff --git a/nnodely/__init__.py b/src/nnodely/__init__.py similarity index 100% rename from nnodely/__init__.py rename to src/nnodely/__init__.py diff --git a/nnodely/basic/__init__.py b/src/nnodely/basic/__init__.py similarity index 100% rename from nnodely/basic/__init__.py rename to src/nnodely/basic/__init__.py diff --git a/nnodely/basic/loss.py b/src/nnodely/basic/loss.py similarity index 100% rename from nnodely/basic/loss.py rename to src/nnodely/basic/loss.py diff --git a/nnodely/basic/model.py b/src/nnodely/basic/model.py similarity index 100% rename from nnodely/basic/model.py rename to src/nnodely/basic/model.py diff --git a/nnodely/basic/modeldef.py b/src/nnodely/basic/modeldef.py similarity index 100% rename from nnodely/basic/modeldef.py rename to src/nnodely/basic/modeldef.py diff --git a/nnodely/basic/optimizer.py b/src/nnodely/basic/optimizer.py similarity index 100% rename from nnodely/basic/optimizer.py rename to src/nnodely/basic/optimizer.py diff --git a/nnodely/basic/relation.py b/src/nnodely/basic/relation.py similarity index 100% rename from nnodely/basic/relation.py rename to src/nnodely/basic/relation.py diff --git a/nnodely/exporter/__init__.py b/src/nnodely/exporter/__init__.py similarity index 100% rename from nnodely/exporter/__init__.py rename to src/nnodely/exporter/__init__.py diff --git a/nnodely/exporter/emptyexporter.py b/src/nnodely/exporter/emptyexporter.py similarity index 100% rename from nnodely/exporter/emptyexporter.py rename to src/nnodely/exporter/emptyexporter.py diff --git a/nnodely/exporter/export.py b/src/nnodely/exporter/export.py similarity index 100% rename from nnodely/exporter/export.py rename to src/nnodely/exporter/export.py diff --git a/nnodely/exporter/reporter.py b/src/nnodely/exporter/reporter.py similarity index 100% rename from nnodely/exporter/reporter.py rename to src/nnodely/exporter/reporter.py diff --git a/nnodely/exporter/standardexporter.py b/src/nnodely/exporter/standardexporter.py similarity index 100% rename from nnodely/exporter/standardexporter.py rename to src/nnodely/exporter/standardexporter.py diff --git a/nnodely/layers/__init__.py b/src/nnodely/layers/__init__.py similarity index 100% rename from nnodely/layers/__init__.py rename to src/nnodely/layers/__init__.py diff --git a/nnodely/layers/activation.py b/src/nnodely/layers/activation.py similarity index 100% rename from nnodely/layers/activation.py rename to src/nnodely/layers/activation.py diff --git a/nnodely/layers/arithmetic.py b/src/nnodely/layers/arithmetic.py similarity index 100% rename from nnodely/layers/arithmetic.py rename to src/nnodely/layers/arithmetic.py diff --git a/nnodely/layers/equationlearner.py b/src/nnodely/layers/equationlearner.py similarity index 100% rename from nnodely/layers/equationlearner.py rename to src/nnodely/layers/equationlearner.py diff --git a/nnodely/layers/fir.py b/src/nnodely/layers/fir.py similarity index 100% rename from nnodely/layers/fir.py rename to src/nnodely/layers/fir.py diff --git a/nnodely/layers/fuzzify.py b/src/nnodely/layers/fuzzify.py similarity index 100% rename from nnodely/layers/fuzzify.py rename to src/nnodely/layers/fuzzify.py diff --git a/nnodely/layers/input.py b/src/nnodely/layers/input.py similarity index 100% rename from nnodely/layers/input.py rename to src/nnodely/layers/input.py diff --git a/nnodely/layers/interpolation.py b/src/nnodely/layers/interpolation.py similarity index 100% rename from nnodely/layers/interpolation.py rename to src/nnodely/layers/interpolation.py diff --git a/nnodely/layers/linear.py b/src/nnodely/layers/linear.py similarity index 100% rename from nnodely/layers/linear.py rename to src/nnodely/layers/linear.py diff --git a/nnodely/layers/localmodel.py b/src/nnodely/layers/localmodel.py similarity index 100% rename from nnodely/layers/localmodel.py rename to src/nnodely/layers/localmodel.py diff --git a/nnodely/layers/neuralODE.py b/src/nnodely/layers/neuralODE.py similarity index 100% rename from nnodely/layers/neuralODE.py rename to src/nnodely/layers/neuralODE.py diff --git a/nnodely/layers/output.py b/src/nnodely/layers/output.py similarity index 100% rename from nnodely/layers/output.py rename to src/nnodely/layers/output.py diff --git a/nnodely/layers/parameter.py b/src/nnodely/layers/parameter.py similarity index 100% rename from nnodely/layers/parameter.py rename to src/nnodely/layers/parameter.py diff --git a/nnodely/layers/parametricfunction.py b/src/nnodely/layers/parametricfunction.py similarity index 100% rename from nnodely/layers/parametricfunction.py rename to src/nnodely/layers/parametricfunction.py diff --git a/nnodely/layers/part.py b/src/nnodely/layers/part.py similarity index 100% rename from nnodely/layers/part.py rename to src/nnodely/layers/part.py diff --git a/nnodely/layers/rungekutta.py b/src/nnodely/layers/rungekutta.py similarity index 100% rename from nnodely/layers/rungekutta.py rename to src/nnodely/layers/rungekutta.py diff --git a/nnodely/layers/timeoperation.py b/src/nnodely/layers/timeoperation.py similarity index 100% rename from nnodely/layers/timeoperation.py rename to src/nnodely/layers/timeoperation.py diff --git a/nnodely/layers/trigonometric.py b/src/nnodely/layers/trigonometric.py similarity index 100% rename from nnodely/layers/trigonometric.py rename to src/nnodely/layers/trigonometric.py diff --git a/nnodely/nnodely.py b/src/nnodely/nnodely.py similarity index 100% rename from nnodely/nnodely.py rename to src/nnodely/nnodely.py diff --git a/nnodely/operators/__init__.py b/src/nnodely/operators/__init__.py similarity index 100% rename from nnodely/operators/__init__.py rename to src/nnodely/operators/__init__.py diff --git a/nnodely/operators/composer.py b/src/nnodely/operators/composer.py similarity index 100% rename from nnodely/operators/composer.py rename to src/nnodely/operators/composer.py diff --git a/nnodely/operators/exporter.py b/src/nnodely/operators/exporter.py similarity index 100% rename from nnodely/operators/exporter.py rename to src/nnodely/operators/exporter.py diff --git a/nnodely/operators/loader.py b/src/nnodely/operators/loader.py similarity index 100% rename from nnodely/operators/loader.py rename to src/nnodely/operators/loader.py diff --git a/nnodely/operators/network.py b/src/nnodely/operators/network.py similarity index 100% rename from nnodely/operators/network.py rename to src/nnodely/operators/network.py diff --git a/nnodely/operators/trainer.py b/src/nnodely/operators/trainer.py similarity index 100% rename from nnodely/operators/trainer.py rename to src/nnodely/operators/trainer.py diff --git a/nnodely/operators/validator.py b/src/nnodely/operators/validator.py similarity index 100% rename from nnodely/operators/validator.py rename to src/nnodely/operators/validator.py diff --git a/nnodely/support/__init__.py b/src/nnodely/support/__init__.py similarity index 100% rename from nnodely/support/__init__.py rename to src/nnodely/support/__init__.py diff --git a/nnodely/support/earlystopping.py b/src/nnodely/support/earlystopping.py similarity index 100% rename from nnodely/support/earlystopping.py rename to src/nnodely/support/earlystopping.py diff --git a/nnodely/support/fixstepsolver.py b/src/nnodely/support/fixstepsolver.py similarity index 100% rename from nnodely/support/fixstepsolver.py rename to src/nnodely/support/fixstepsolver.py diff --git a/nnodely/support/initializer.py b/src/nnodely/support/initializer.py similarity index 100% rename from nnodely/support/initializer.py rename to src/nnodely/support/initializer.py diff --git a/nnodely/support/jsonutils.py b/src/nnodely/support/jsonutils.py similarity index 100% rename from nnodely/support/jsonutils.py rename to src/nnodely/support/jsonutils.py diff --git a/nnodely/support/logger.py b/src/nnodely/support/logger.py similarity index 100% rename from nnodely/support/logger.py rename to src/nnodely/support/logger.py diff --git a/nnodely/support/mathutils.py b/src/nnodely/support/mathutils.py similarity index 100% rename from nnodely/support/mathutils.py rename to src/nnodely/support/mathutils.py diff --git a/nnodely/support/odeint/__init__.py b/src/nnodely/support/odeint/__init__.py similarity index 100% rename from nnodely/support/odeint/__init__.py rename to src/nnodely/support/odeint/__init__.py diff --git a/nnodely/support/odeint/adjoint.py b/src/nnodely/support/odeint/adjoint.py similarity index 100% rename from nnodely/support/odeint/adjoint.py rename to src/nnodely/support/odeint/adjoint.py diff --git a/nnodely/support/odeint/dopri5.py b/src/nnodely/support/odeint/dopri5.py similarity index 100% rename from nnodely/support/odeint/dopri5.py rename to src/nnodely/support/odeint/dopri5.py diff --git a/nnodely/support/odeint/fixed_grid.py b/src/nnodely/support/odeint/fixed_grid.py similarity index 100% rename from nnodely/support/odeint/fixed_grid.py rename to src/nnodely/support/odeint/fixed_grid.py diff --git a/nnodely/support/odeint/my_odeint.py b/src/nnodely/support/odeint/my_odeint.py similarity index 100% rename from nnodely/support/odeint/my_odeint.py rename to src/nnodely/support/odeint/my_odeint.py diff --git a/nnodely/support/odeint/rk_solvers.py b/src/nnodely/support/odeint/rk_solvers.py similarity index 100% rename from nnodely/support/odeint/rk_solvers.py rename to src/nnodely/support/odeint/rk_solvers.py diff --git a/nnodely/support/odeint/solvers.py b/src/nnodely/support/odeint/solvers.py similarity index 100% rename from nnodely/support/odeint/solvers.py rename to src/nnodely/support/odeint/solvers.py diff --git a/nnodely/support/odeint/utils.py b/src/nnodely/support/odeint/utils.py similarity index 100% rename from nnodely/support/odeint/utils.py rename to src/nnodely/support/odeint/utils.py diff --git a/nnodely/support/utils.py b/src/nnodely/support/utils.py similarity index 100% rename from nnodely/support/utils.py rename to src/nnodely/support/utils.py diff --git a/nnodely/visualizer/__init__.py b/src/nnodely/visualizer/__init__.py similarity index 100% rename from nnodely/visualizer/__init__.py rename to src/nnodely/visualizer/__init__.py diff --git a/nnodely/visualizer/dynamicmpl/functionplot.py b/src/nnodely/visualizer/dynamicmpl/functionplot.py similarity index 100% rename from nnodely/visualizer/dynamicmpl/functionplot.py rename to src/nnodely/visualizer/dynamicmpl/functionplot.py diff --git a/nnodely/visualizer/dynamicmpl/fuzzyplot.py b/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py similarity index 100% rename from nnodely/visualizer/dynamicmpl/fuzzyplot.py rename to src/nnodely/visualizer/dynamicmpl/fuzzyplot.py diff --git a/nnodely/visualizer/dynamicmpl/resultsplot.py b/src/nnodely/visualizer/dynamicmpl/resultsplot.py similarity index 100% rename from nnodely/visualizer/dynamicmpl/resultsplot.py rename to src/nnodely/visualizer/dynamicmpl/resultsplot.py diff --git a/nnodely/visualizer/dynamicmpl/trainingplot.py b/src/nnodely/visualizer/dynamicmpl/trainingplot.py similarity index 100% rename from nnodely/visualizer/dynamicmpl/trainingplot.py rename to src/nnodely/visualizer/dynamicmpl/trainingplot.py diff --git a/nnodely/visualizer/emptyvisualizer.py b/src/nnodely/visualizer/emptyvisualizer.py similarity index 100% rename from nnodely/visualizer/emptyvisualizer.py rename to src/nnodely/visualizer/emptyvisualizer.py diff --git a/nnodely/visualizer/mplnotebookvisualizer.py b/src/nnodely/visualizer/mplnotebookvisualizer.py similarity index 100% rename from nnodely/visualizer/mplnotebookvisualizer.py rename to src/nnodely/visualizer/mplnotebookvisualizer.py diff --git a/nnodely/visualizer/mplvisualizer.py b/src/nnodely/visualizer/mplvisualizer.py similarity index 100% rename from nnodely/visualizer/mplvisualizer.py rename to src/nnodely/visualizer/mplvisualizer.py diff --git a/nnodely/visualizer/textvisualizer.py b/src/nnodely/visualizer/textvisualizer.py similarity index 100% rename from nnodely/visualizer/textvisualizer.py rename to src/nnodely/visualizer/textvisualizer.py From e43c14f6e7b6a354c8d2c90f044605c8b919de72 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:06:16 +0200 Subject: [PATCH 04/26] feat: modernised pyproject.toml --- pyproject.toml | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74e01be3..53285807 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,21 @@ [build-system] -requires = ["setuptools>=61", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["uv_build>=0.11.6,<0.12.0"] +build-backend = "uv_build" [project] name = "nnodely" +version = "1.5.4" description = "Model-structured neural network framework for the modeling and control of physical systems" readme = "README.md" -requires-python = ">=3.10, <3.13" -license = {file = "LICENSE"} +requires-python = ">=3.10,<3.14" +license = "MIT" +license-files = ["LICENSE"] authors = [ - {name = "Gastone Pietro Rosati Papini", email = "tonegas@gmail.com"} + { name = "Gastone Pietro Rosati Papini", email = "tonegas@gmail.com" }, ] classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent" + "Operating System :: OS Independent", ] dependencies = [ "numpy == 1.26.4; platform_machine == 'x86_64' and python_version == '3.10'", @@ -26,24 +27,12 @@ dependencies = [ "reportlab", "matplotlib", "onnxruntime", - "graphviz" + "graphviz", ] -dynamic = ["version"] [project.urls] "Homepage" = "https://github.com/tonegas/nnodely" +"Repository" = "https://github.com/tonegas/nnodely" -[tool.setuptools] -packages = ["nnodely", - "nnodely.basic", - "nnodely.exporter", - "nnodely.layers", - "nnodely.operators", - "nnodely.support", - "nnodely.visualizer", - "nnodely.visualizer.dynamicmpl", - "mplplots"] - -#[tool.setuptools.data-files] -#"imgs" = ["imgs/*"] -#"data" = ["tests/data/*","tests/test_data/*","tests/val_data/*","tests/vector_data/*"] \ No newline at end of file +[dependency-groups] +dev = ["pytest>=9.0.3"] From c7a88dd8b9ada2c46eb7085b7f83c5f2fe7fd700 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:39:06 +0200 Subject: [PATCH 05/26] chore: ruff --- src/nnodely/basic/optimizer.py | 4 +- src/nnodely/exporter/export.py | 14 +++-- src/nnodely/exporter/reporter.py | 2 +- src/nnodely/exporter/standardexporter.py | 3 +- src/nnodely/layers/fir.py | 3 +- src/nnodely/layers/fuzzify.py | 5 +- src/nnodely/layers/input.py | 1 - src/nnodely/layers/neuralODE.py | 6 +- src/nnodely/layers/parameter.py | 4 +- src/nnodely/layers/parametricfunction.py | 8 ++- src/nnodely/layers/rungekutta.py | 10 +--- src/nnodely/nnodely.py | 4 +- src/nnodely/operators/composer.py | 7 ++- src/nnodely/operators/loader.py | 7 ++- src/nnodely/operators/network.py | 5 +- src/nnodely/operators/trainer.py | 5 +- src/nnodely/operators/validator.py | 5 +- src/nnodely/support/utils.py | 5 +- .../visualizer/dynamicmpl/functionplot.py | 3 +- .../visualizer/dynamicmpl/fuzzyplot.py | 3 +- src/nnodely/visualizer/mplvisualizer.py | 5 +- src/nnodely/visualizer/textvisualizer.py | 57 +++++++++---------- tests/test_dataset.py | 4 +- tests/test_documentation.py | 2 - tests/test_export.py | 6 +- tests/test_export_recurrent.py | 7 ++- tests/test_input_dimensions.py | 5 +- tests/test_json.py | 5 +- tests/test_losses.py | 4 +- tests/test_model_predict.py | 5 +- tests/test_model_predict_recurrent.py | 5 +- tests/test_network_element.py | 5 +- tests/test_parameters_of_train.py | 4 +- tests/test_results.py | 4 +- tests/test_train.py | 5 +- tests/test_train_recurrent.py | 5 +- tests/test_utils.py | 5 +- tests/test_visualizer.py | 7 +-- 38 files changed, 152 insertions(+), 92 deletions(-) diff --git a/src/nnodely/basic/optimizer.py b/src/nnodely/basic/optimizer.py index c800e022..9bde7f47 100644 --- a/src/nnodely/basic/optimizer.py +++ b/src/nnodely/basic/optimizer.py @@ -173,7 +173,9 @@ def replace_key_with_params(self): return params def get_torch_optimizer(self): - raise NotImplemented("The function get_torch_optimizer must be implemented.") + raise NotImplementedError( + "The function get_torch_optimizer must be implemented." + ) class SGD(Optimizer): diff --git a/src/nnodely/exporter/export.py b/src/nnodely/exporter/export.py index 59c617c7..a5575e10 100644 --- a/src/nnodely/exporter/export.py +++ b/src/nnodely/exporter/export.py @@ -1,4 +1,8 @@ -import sys, os, torch, importlib, json +import sys +import os +import torch +import importlib +import json from torch.fx import symbolic_trace @@ -265,10 +269,10 @@ def export_python_model(model_def, model, model_path): file.write(f" results = {{{result_str}}}\n") file.write(" X = dict()\n") file.write(" for idx in range(n_samples):\n") - file.write(f" for key in self.inputs:\n") - file.write(f" X[key] = kwargs[key][idx]\n") - file.write(f" for key, value in self.states.items():\n") - file.write(f" X[key] = value\n") + file.write(" for key in self.inputs:\n") + file.write(" X[key] = kwargs[key][idx]\n") + file.write(" for key, value in self.states.items():\n") + file.write(" X[key] = value\n") file.write(" out, _, closed_loop, connect = self.Cell(X)\n") file.write(" for key, value in results.items():\n") file.write(" results[key].append(out[key])\n") diff --git a/src/nnodely/exporter/reporter.py b/src/nnodely/exporter/reporter.py index 3dbdf181..8259fa12 100644 --- a/src/nnodely/exporter/reporter.py +++ b/src/nnodely/exporter/reporter.py @@ -77,5 +77,5 @@ def exportReport(self, report_path): ) c.showPage() else: - c.drawString(100, height - 30, f"No Minimize") + c.drawString(100, height - 30, "No Minimize") c.save() diff --git a/src/nnodely/exporter/standardexporter.py b/src/nnodely/exporter/standardexporter.py index 687bd931..6e17073d 100644 --- a/src/nnodely/exporter/standardexporter.py +++ b/src/nnodely/exporter/standardexporter.py @@ -1,4 +1,5 @@ -import os, torch +import os +import torch from nnodely.visualizer import EmptyVisualizer from nnodely.exporter.emptyexporter import EmptyExporter diff --git a/src/nnodely/layers/fir.py b/src/nnodely/layers/fir.py index b1778694..38066de9 100644 --- a/src/nnodely/layers/fir.py +++ b/src/nnodely/layers/fir.py @@ -1,4 +1,5 @@ -import copy, torch +import copy +import torch import torch.nn as nn diff --git a/src/nnodely/layers/fuzzify.py b/src/nnodely/layers/fuzzify.py index a09bd9b8..2e1063dc 100644 --- a/src/nnodely/layers/fuzzify.py +++ b/src/nnodely/layers/fuzzify.py @@ -1,4 +1,7 @@ -import inspect, copy, textwrap, torch +import inspect +import copy +import textwrap +import torch import numpy as np import torch.nn as nn diff --git a/src/nnodely/layers/input.py b/src/nnodely/layers/input.py index 7b7a91ef..1bf48f0e 100644 --- a/src/nnodely/layers/input.py +++ b/src/nnodely/layers/input.py @@ -331,7 +331,6 @@ def closedLoop(self, obj: Stream) -> "Input": KeyError If the Input variable is already connected. """ - from nnodely.layers.input import Input check( type(obj) is Stream, diff --git a/src/nnodely/layers/neuralODE.py b/src/nnodely/layers/neuralODE.py index 265ec1ed..c41017a1 100644 --- a/src/nnodely/layers/neuralODE.py +++ b/src/nnodely/layers/neuralODE.py @@ -1,4 +1,6 @@ -import inspect, copy, textwrap, torch, math +import inspect +import copy +import textwrap import torch.nn as nn @@ -51,7 +53,7 @@ def __init__( + code.replace("\n", "\n ") + "\n" + f" ans = odeint(lambda t, y: {func.param_fun.__name__}(t, y, *weights), state, t=torch.tensor([0.0, {self.dt}]), rtol={self.rtol}, atol={self.atol}, method='{self.method}', adjoint_params=list(weights))" - + f"\n return ans[-1]\n" + + "\n return ans[-1]\n" ) self.json["Functions"][self.name] = { diff --git a/src/nnodely/layers/parameter.py b/src/nnodely/layers/parameter.py index ae359ae1..af99160d 100644 --- a/src/nnodely/layers/parameter.py +++ b/src/nnodely/layers/parameter.py @@ -1,4 +1,6 @@ -import copy, inspect, textwrap +import copy +import inspect +import textwrap import numpy as np from collections.abc import Callable diff --git a/src/nnodely/layers/parametricfunction.py b/src/nnodely/layers/parametricfunction.py index 83573aa8..75335361 100644 --- a/src/nnodely/layers/parametricfunction.py +++ b/src/nnodely/layers/parametricfunction.py @@ -1,4 +1,8 @@ -import inspect, copy, textwrap, torch, math +import inspect +import copy +import textwrap +import torch +import math import torch.nn as nn import numpy as np @@ -163,7 +167,7 @@ def __call__( check( n_missing_parameters >= 0, ValueError, - f"The function is called with too many parameter and inputs.", + "The function is called with too many parameter and inputs.", ) self.__create_missing_parameters( self.json_stream[n_call_input], n_call_input, n_missing_parameters diff --git a/src/nnodely/layers/rungekutta.py b/src/nnodely/layers/rungekutta.py index ac89247b..bc1281b4 100644 --- a/src/nnodely/layers/rungekutta.py +++ b/src/nnodely/layers/rungekutta.py @@ -1,14 +1,8 @@ -import torch.nn as nn -import torch - from nnodely.layers.parametricfunction import ParamFun from nnodely.layers.parameter import SampleTime -from nnodely.basic.relation import Stream, NeuObj, ToStream -from nnodely.support.utils import enforce_types, check -from nnodely.support.jsonutils import merge, subjson_from_relation -from nnodely.basic.model import Model -import textwrap, inspect +from nnodely.basic.relation import Stream, NeuObj +from nnodely.support.utils import enforce_types from collections.abc import Callable fe_relation_name = "ForwardEuler" diff --git a/src/nnodely/nnodely.py b/src/nnodely/nnodely.py index 8b6359ce..417089e7 100644 --- a/src/nnodely/nnodely.py +++ b/src/nnodely/nnodely.py @@ -1,5 +1,7 @@ # Extern packages -import random, torch, copy +import random +import torch +import copy import numpy as np # Main operators diff --git a/src/nnodely/operators/composer.py b/src/nnodely/operators/composer.py index 1f9948d5..b8d1e61c 100644 --- a/src/nnodely/operators/composer.py +++ b/src/nnodely/operators/composer.py @@ -1,4 +1,5 @@ -import copy, torch +import copy +import torch import numpy as np @@ -326,7 +327,7 @@ def __call__( ) if num_of_samples is not None and sampled == True: - log.warning(f"num_of_samples is ignored if sampled is equal to True") + log.warning("num_of_samples is ignored if sampled is equal to True") ## Get the maximum inference window if num_of_samples and not sampled: @@ -375,7 +376,7 @@ def __call__( window_dim = min(windows) if windows else 0 else: ## No inputs window_dim = 1 if non_mandatory_inputs else 0 - check(window_dim > 0, StopIteration, f"Missing samples in the input window") + check(window_dim > 0, StopIteration, "Missing samples in the input window") if len(set(num_of_windows.values())) > 1: max_ind_key, max_dim = argmax_dict(num_of_windows) diff --git a/src/nnodely/operators/loader.py b/src/nnodely/operators/loader.py index 0cb80bc5..6b56278a 100644 --- a/src/nnodely/operators/loader.py +++ b/src/nnodely/operators/loader.py @@ -1,4 +1,5 @@ -import os, random +import os +import random import pandas as pd import numpy as np @@ -247,7 +248,7 @@ def __get_files(self, folder: str) -> list: try: _, _, files = next(os.walk(folder)) files.sort() - except StopIteration as e: + except StopIteration: check(False, StopIteration, f'ERROR: The path "{folder}" does not exist!') return [] return files @@ -315,7 +316,7 @@ def loadData( json_inputs = self._model_def["Inputs"] ## Initialize the dictionary containing the data - check_names(name, self._data.keys(), f"Dataset") + check_names(name, self._data.keys(), "Dataset") if type(source) is str: ## we have a directory path containing the files ## collect column indexes diff --git a/src/nnodely/operators/network.py b/src/nnodely/operators/network.py index 5c0acc52..fcf97ec7 100644 --- a/src/nnodely/operators/network.py +++ b/src/nnodely/operators/network.py @@ -2,7 +2,8 @@ from collections import defaultdict import numpy as np -import torch, random +import torch +import random from nnodely.support.utils import ( TORCH_DTYPE, @@ -216,7 +217,7 @@ def _clip_batch_size(self, n_samples, batch_size=None): ValueError, f"The number of available sample are {n_samples - batch_size + 1}", ) - check(batch_size > 0, ValueError, f"The batch_size must be greater than 0.") + check(batch_size > 0, ValueError, "The batch_size must be greater than 0.") return batch_size def __split_dataset(self, dataset: str | list | dict, splits: list): diff --git a/src/nnodely/operators/trainer.py b/src/nnodely/operators/trainer.py index deecc930..95f3e52b 100644 --- a/src/nnodely/operators/trainer.py +++ b/src/nnodely/operators/trainer.py @@ -1,4 +1,7 @@ -import copy, torch, time, inspect +import copy +import torch +import time +import inspect from collections.abc import Callable from functools import wraps diff --git a/src/nnodely/operators/validator.py b/src/nnodely/operators/validator.py index 0915ca79..a8e38b49 100644 --- a/src/nnodely/operators/validator.py +++ b/src/nnodely/operators/validator.py @@ -1,11 +1,12 @@ -import torch, warnings +import torch +import warnings import numpy as np from nnodely.support.utils import ReadOnlyDict, get_batch_size from nnodely.basic.loss import CustomLoss from nnodely.operators.network import Network -from nnodely.support.utils import check, TORCH_DTYPE, enforce_types +from nnodely.support.utils import check, enforce_types class Validator(Network): diff --git a/src/nnodely/support/utils.py b/src/nnodely/support/utils.py index fd4ede73..c50255e5 100644 --- a/src/nnodely/support/utils.py +++ b/src/nnodely/support/utils.py @@ -1,4 +1,5 @@ -import torch, inspect +import torch +import inspect import types from collections import OrderedDict @@ -168,7 +169,7 @@ def get_batch_size(n_samples, batch_size=None, predicion_samples=0): if batch_size <= n_samples - predicion_samples else max(0, n_samples - predicion_samples) ) - check(batch_size > 0, ValueError, f"The batch_size must be greater than 0.") + check(batch_size > 0, ValueError, "The batch_size must be greater than 0.") return batch_size diff --git a/src/nnodely/visualizer/dynamicmpl/functionplot.py b/src/nnodely/visualizer/dynamicmpl/functionplot.py index dbef5a85..74fe533f 100644 --- a/src/nnodely/visualizer/dynamicmpl/functionplot.py +++ b/src/nnodely/visualizer/dynamicmpl/functionplot.py @@ -1,4 +1,5 @@ -import sys, json +import sys +import json import matplotlib.pyplot as plt diff --git a/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py b/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py index cf99f168..3b49f196 100644 --- a/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py +++ b/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py @@ -1,4 +1,5 @@ -import sys, json +import sys +import json import matplotlib.pyplot as plt import matplotlib.colors as mcolors diff --git a/src/nnodely/visualizer/mplvisualizer.py b/src/nnodely/visualizer/mplvisualizer.py index 113bf83a..ef4c5321 100644 --- a/src/nnodely/visualizer/mplvisualizer.py +++ b/src/nnodely/visualizer/mplvisualizer.py @@ -1,4 +1,7 @@ -import subprocess, json, os, importlib +import subprocess +import json +import os +import importlib import numpy as np from nnodely.visualizer.textvisualizer import TextVisualizer diff --git a/src/nnodely/visualizer/textvisualizer.py b/src/nnodely/visualizer/textvisualizer.py index 71e4db95..649ef1bb 100644 --- a/src/nnodely/visualizer/textvisualizer.py +++ b/src/nnodely/visualizer/textvisualizer.py @@ -1,8 +1,7 @@ import numpy as np from pprint import pformat -from nnodely.support.utils import is_notebook -from nnodely.visualizer.emptyvisualizer import EmptyVisualizer, color, GREEN, RED, BLUE +from nnodely.visualizer.emptyvisualizer import EmptyVisualizer, color, GREEN, BLUE class TextVisualizer(EmptyVisualizer): @@ -137,7 +136,7 @@ def showWeightsInTrain(self, batch=None, epoch=None, weights=None): ) if epoch is not None: - print(color("|" + (f"").center(10 + 20 * (dim + 1), "-") + "|")) + print(color("|" + ("").center(10 + 20 * (dim + 1), "-") + "|")) def showDataset(self, name): if self.verbose >= 1: @@ -162,30 +161,30 @@ def showStartTraining(self): " nnodely Training ", 12 + (len(self.modely._model_def["Minimizers"]) + 1) * 20, ) - print(color("|" + (f"Epoch").center(10, " ") + "|"), end="") + print(color("|" + ("Epoch").center(10, " ") + "|"), end="") for key in self.modely._model_def["Minimizers"].keys(): print(color((f"{key}").center(19, " ") + "|"), end="") - print(color((f"Total").center(19, " ") + "|")) + print(color(("Total").center(19, " ") + "|")) - print(color("|" + (f" ").center(10, " ") + "|"), end="") + print(color("|" + (" ").center(10, " ") + "|"), end="") for key in self.modely._model_def["Minimizers"].keys(): - print(color((f"Loss").center(19, " ") + "|"), end="") - print(color((f"Loss").center(19, " ") + "|")) + print(color(("Loss").center(19, " ") + "|"), end="") + print(color(("Loss").center(19, " ") + "|")) - print(color("|" + (f" ").center(10, " ") + "|"), end="") + print(color("|" + (" ").center(10, " ") + "|"), end="") for key in self.modely._model_def["Minimizers"].keys(): if par["n_samples_val"]: - print(color((f"train").center(9, " ") + "|"), end="") - print(color((f"val").center(9, " ") + "|"), end="") + print(color(("train").center(9, " ") + "|"), end="") + print(color(("val").center(9, " ") + "|"), end="") else: - print(color((f"train").center(19, " ") + "|"), end="") + print(color(("train").center(19, " ") + "|"), end="") if par["n_samples_val"]: - print(color((f"train").center(9, " ") + "|"), end="") - print(color((f"val").center(9, " ") + "|")) + print(color(("train").center(9, " ") + "|"), end="") + print(color(("val").center(9, " ") + "|")) else: - print(color((f"train").center(19, " ") + "|")) + print(color(("train").center(19, " ") + "|")) - print(color("|" + (f"").center(10 + 20 * (dim + 1), "-") + "|")) + print(color("|" + ("").center(10 + 20 * (dim + 1), "-") + "|")) def showTraining(self, epoch, train_losses, val_losses): if self.verbose >= 1: @@ -273,7 +272,7 @@ def showTraining(self, epoch, train_losses, val_losses): ) if epoch + 1 == par["num_of_epochs"]: - print(color("|" + (f"").center(10 + 20 * (dim + 1), "-") + "|")) + print(color("|" + ("").center(10 + 20 * (dim + 1), "-") + "|")) def showTrainingTime(self, time): if self.verbose >= 1: @@ -361,22 +360,22 @@ def showResult(self, name_data): f" nnodely Model Results for {name_data} ", dim_loss + 2 + (len(loss_type_list) + 2) * 20, ) - print(color("|" + (f"Loss").center(dim_loss, " ") + "|"), end="") + print(color("|" + ("Loss").center(dim_loss, " ") + "|"), end="") for loss in loss_type_list: print(color((f"{loss}").center(19, " ") + "|"), end="") - print(color((f"FVU").center(19, " ") + "|"), end="") - print(color((f"AIC").center(19, " ") + "|")) + print(color(("FVU").center(19, " ") + "|"), end="") + print(color(("AIC").center(19, " ") + "|")) - print(color("|" + (f"").center(dim_loss, " ") + "|"), end="") + print(color("|" + ("").center(dim_loss, " ") + "|"), end="") for i in range(len(loss_type_list)): - print(color((f"small better").center(19, " ") + "|"), end="") - print(color((f"small better").center(19, " ") + "|"), end="") - print(color((f"lower better").center(19, " ") + "|")) + print(color(("small better").center(19, " ") + "|"), end="") + print(color(("small better").center(19, " ") + "|"), end="") + print(color(("lower better").center(19, " ") + "|")) print( color( "|" - + (f"").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + + ("").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + "|" ) ) @@ -396,7 +395,7 @@ def showResult(self, name_data): end="", ) else: - print(color((f" ").center(19, " ") + "|"), end="") + print(color((" ").center(19, " ") + "|"), end="") print( color( ( @@ -418,11 +417,11 @@ def showResult(self, name_data): print( color( "|" - + (f"").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + + ("").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + "|" ) ) - print(color("|" + (f"Total").center(dim_loss, " ") + "|"), end="") + print(color("|" + ("Total").center(dim_loss, " ") + "|"), end="") print( color( ( @@ -453,7 +452,7 @@ def showResult(self, name_data): print( color( "|" - + (f"").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + + ("").center(dim_loss + 20 * (len(loss_type_list) + 2), "-") + "|" ) ) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index fa60f1b0..9971f510 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,4 +1,6 @@ -import sys, os, unittest +import sys +import os +import unittest import numpy as np from nnodely import * diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 7879f450..10897634 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -1,6 +1,4 @@ import unittest -import subprocess -import os class TestDocumentation(unittest.TestCase): diff --git a/tests/test_export.py b/tests/test_export.py index 07eb76e0..6cb0dbe6 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -1,8 +1,10 @@ -import os, unittest, torch, shutil +import os +import unittest +import torch +import shutil import numpy as np from nnodely import * -from nnodely.basic.relation import NeuObj from nnodely.support.logger import logging, nnLogger log = nnLogger(__name__, logging.CRITICAL) diff --git a/tests/test_export_recurrent.py b/tests/test_export_recurrent.py index 915a51a7..27824ba1 100644 --- a/tests/test_export_recurrent.py +++ b/tests/test_export_recurrent.py @@ -1,4 +1,9 @@ -import sys, os, unittest, torch, shutil, torch.onnx, importlib +import os +import unittest +import torch +import shutil +import torch.onnx +import importlib import numpy as np from nnodely import * diff --git a/tests/test_input_dimensions.py b/tests/test_input_dimensions.py index 44a8d081..5e930a02 100644 --- a/tests/test_input_dimensions.py +++ b/tests/test_input_dimensions.py @@ -1,4 +1,7 @@ -import unittest, sys, os, torch +import unittest +import sys +import os +import torch import numpy as np from nnodely import * diff --git a/tests/test_json.py b/tests/test_json.py index 7e609ef0..0c6b2107 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1,4 +1,7 @@ -import sys, os, unittest, copy +import sys +import os +import unittest +import copy import numpy as np diff --git a/tests/test_losses.py b/tests/test_losses.py index 2217805c..34da3ebc 100644 --- a/tests/test_losses.py +++ b/tests/test_losses.py @@ -1,4 +1,6 @@ -import unittest, os, sys +import unittest +import os +import sys import numpy as np import torch diff --git a/tests/test_model_predict.py b/tests/test_model_predict.py index 15906b02..9b4bdc17 100644 --- a/tests/test_model_predict.py +++ b/tests/test_model_predict.py @@ -1,4 +1,7 @@ -import sys, os, torch, unittest +import sys +import os +import torch +import unittest import numpy as np from nnodely import * diff --git a/tests/test_model_predict_recurrent.py b/tests/test_model_predict_recurrent.py index b91b161d..fdb17d4e 100644 --- a/tests/test_model_predict_recurrent.py +++ b/tests/test_model_predict_recurrent.py @@ -1,4 +1,7 @@ -import unittest, sys, os, torch +import unittest +import sys +import os +import torch import numpy as np from nnodely import * diff --git a/tests/test_network_element.py b/tests/test_network_element.py index 10720c1d..d9184e3c 100644 --- a/tests/test_network_element.py +++ b/tests/test_network_element.py @@ -1,4 +1,7 @@ -import unittest, sys, os, torch +import unittest +import sys +import os +import torch import numpy as np diff --git a/tests/test_parameters_of_train.py b/tests/test_parameters_of_train.py index dd0947d6..f99e2cf8 100644 --- a/tests/test_parameters_of_train.py +++ b/tests/test_parameters_of_train.py @@ -1,4 +1,6 @@ -import unittest, os, sys +import unittest +import os +import sys import numpy as np from nnodely import * diff --git a/tests/test_results.py b/tests/test_results.py index a7a89629..4b7652bf 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -1,4 +1,6 @@ -import unittest, os, sys +import unittest +import os +import sys import numpy as np from nnodely import * diff --git a/tests/test_train.py b/tests/test_train.py index bb8d551f..90f7e370 100644 --- a/tests/test_train.py +++ b/tests/test_train.py @@ -1,4 +1,7 @@ -import unittest, os, sys, torch +import unittest +import os +import sys +import torch import numpy as np from nnodely import * diff --git a/tests/test_train_recurrent.py b/tests/test_train_recurrent.py index 434fb560..47f6dbf4 100644 --- a/tests/test_train_recurrent.py +++ b/tests/test_train_recurrent.py @@ -1,6 +1,7 @@ -import unittest, os, sys +import unittest +import os +import sys import numpy as np -from pygments.unistring import xid_start from nnodely import * from nnodely.basic.relation import NeuObj diff --git a/tests/test_utils.py b/tests/test_utils.py index 03783d48..bc7f9c8b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,7 @@ -import unittest, os, sys, torch +import unittest +import os +import sys +import torch from nnodely import * from nnodely.support.mathutils import linear_interp diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index d1b2c7c6..4a824d12 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -1,10 +1,9 @@ -import sys, io, os, unittest, torch -import numpy as np +import sys +import os +import unittest from nnodely import * -from nnodely.basic.relation import NeuObj from nnodely.support.logger import logging, nnLogger -from nnodely.support.jsonutils import plot_structure log = nnLogger(__name__, logging.ERROR) log.setAllLevel(logging.ERROR) From a7482ead90ba555a6b55518a627c7339de6a1fc9 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:41:03 +0200 Subject: [PATCH 06/26] feat: started adding `uv` and `pre-commit` --- .github/workflows/codecov.yml | 7 +- .pre-commit-config.yaml | 34 + .python-version | 1 + pyproject.toml | 6 +- uv.lock | 2168 +++++++++++++++++++++++++++++++++ 5 files changed, 2212 insertions(+), 4 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 uv.lock diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 172ab350..5a54aa8b 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -2,9 +2,9 @@ name: Coverage (Codecov) on: push: - branches: [ "main", "develop" ] + branches: ["main", "develop"] pull_request: - branches: [ "main", "develop" ] + branches: ["main", "develop"] jobs: test: @@ -38,4 +38,5 @@ jobs: uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml \ No newline at end of file + files: coverage.xml + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..7c52b190 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +default_install_hook_types: + - pre-commit + - post-merge + - post-rewrite + +repos: + - repo: local + hooks: + - id: uv-sync + name: uv sync + entry: uv sync + language: system + pass_filenames: false + stages: [post-merge, post-rewrite] + - id: uv-lock + name: uv lock + entry: uv lock + language: system + pass_filenames: false + # - id: ruff-check + # name: ruff check + # entry: uv run ruff check --fix + # language: system + # types: [python] + - id: ruff-format + name: ruff format + entry: uv run ruff format + language: system + types: [python] + - id: pytest + name: pytest + entry: uv run pytest + language: system + pass_filenames: false diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/pyproject.toml b/pyproject.toml index 53285807..ad34392f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,4 +35,8 @@ dependencies = [ "Repository" = "https://github.com/tonegas/nnodely" [dependency-groups] -dev = ["pytest>=9.0.3"] +dev = [ + "pre-commit>=4.5.1", + "pytest-cov>=7.1.0", + "ruff>=0.15.10", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..f9643e26 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2168 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.14" +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, +] + +[[package]] +name = "identify" +version = "2.6.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nnodely" +version = "1.5.4" +source = { editable = "." } +dependencies = [ + { name = "graphviz" }, + { name = "matplotlib" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnx" }, + { name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "reportlab" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphviz" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "numpy", marker = "python_full_version == '3.10.*' and platform_machine == 'x86_64'", specifier = "==1.26.4" }, + { name = "onnx" }, + { name = "onnxruntime" }, + { name = "pandas" }, + { name = "reportlab" }, + { name = "torch", marker = "python_full_version != '3.10.*' or platform_machine != 'x86_64'", specifier = "==2.6.0" }, + { name = "torch", marker = "python_full_version == '3.10.*' and platform_machine == 'x86_64'", specifier = "==2.2.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.10" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774, upload-time = "2023-04-19T15:50:03.519Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.4.5.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805, upload-time = "2024-04-03T20:57:06.025Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015, upload-time = "2023-04-19T15:47:32.502Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957, upload-time = "2024-04-03T20:55:01.564Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734, upload-time = "2023-04-19T15:48:32.42Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306, upload-time = "2024-04-03T20:56:01.463Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596, upload-time = "2023-04-19T15:47:22.471Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737, upload-time = "2024-04-03T20:54:51.355Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +dependencies = [ + { name = "nvidia-cublas-cu12", version = "12.1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872, upload-time = "2023-06-01T19:24:57.328Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.1.0.70" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "nvidia-cublas-cu12", version = "12.4.5.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161, upload-time = "2023-04-19T15:50:46Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.2.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784, upload-time = "2023-04-19T15:51:04.804Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.5.147" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206, upload-time = "2024-04-03T20:58:08.722Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +dependencies = [ + { name = "nvidia-cublas-cu12", version = "12.1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse-cu12", version = "12.1.0.106", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink-cu12", version = "12.9.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928, upload-time = "2023-04-19T15:51:25.781Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.6.1.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "nvidia-cublas-cu12", version = "12.4.5.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse-cu12", version = "12.3.1.170", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +dependencies = [ + { name = "nvidia-nvjitlink-cu12", version = "12.9.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278, upload-time = "2023-04-19T15:51:49.939Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.3.1.170" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/a8/bcbb63b53a4b1234feeafb65544ee55495e1bb37ec31b999b963cbccfd1d/nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9", size = 150057751, upload-time = "2024-07-23T02:35:53.074Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.19.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/00/d0d4e48aef772ad5aebcf70b73028f88db6e5640b36c38e90445b7a57c45/nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d", size = 165987969, upload-time = "2023-10-24T16:16:24.789Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.21.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414, upload-time = "2024-04-03T15:32:57.427Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810, upload-time = "2024-04-03T20:59:46.957Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9", size = 39748338, upload-time = "2025-06-05T20:10:25.613Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138, upload-time = "2023-04-19T15:48:43.556Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144, upload-time = "2024-04-03T20:56:12.406Z" }, +] + +[[package]] +name = "onnx" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/93/942d2a0f6a70538eea042ce0445c8aefd46559ad153469986f29a743c01c/onnx-1.21.0.tar.gz", hash = "sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764", size = 12074608, upload-time = "2026-03-27T21:33:36.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/28/a14b1845bf9302c3a787221e8f37cde4e7f930e10d95a8e22dd910aeb41d/onnx-1.21.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e0c21cc5c7a41d1a509828e2b14fe9c30e807c6df611ec0fd64a47b8d4b16abd", size = 17966899, upload-time = "2026-03-27T21:32:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/788881bf022a4cfb7b0843782f88415ea51c805cee4a909dcf2e49bb8129/onnx-1.21.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1931bfcc222a4c9da6475f2ffffb84b97ab3876041ec639171c11ce802bee6a", size = 17534297, upload-time = "2026-03-27T21:32:18.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/51/eb64d4f2ec6caa98909aab5fbcfa24be9c059081e804bbb0012cc549ef89/onnx-1.21.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9b56ad04039fac6b028c07e54afa1ec7f75dd340f65311f2c292e41ed7aa4d9", size = 17616697, upload-time = "2026-03-27T21:32:21Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4e/6b1f7800dae3407dc850e7e59d591ed8c83e9b3401e4cd57a1f612e400c6/onnx-1.21.0-cp310-cp310-win32.whl", hash = "sha256:3abd09872523c7e0362d767e4e63bd7c6bac52a5e2c3edbf061061fe540e2027", size = 16288893, upload-time = "2026-03-27T21:32:23.864Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a8/89273e581d3943e20314af19b1596ab4d763f9c2eb07d4eaf4fb0593219b/onnx-1.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:f2c7c234c568402e10db74e33d787e4144e394ae2bcbbf11000fbfe2e017ad68", size = 16443416, upload-time = "2026-03-27T21:32:26.655Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/32e383aa6bc40b72a9fd419937aaa647078190c9bfccdc97b316d2dee687/onnx-1.21.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:2aca19949260875c14866fc77ea0bc37e4e809b24976108762843d328c92d3ce", size = 17968053, upload-time = "2026-03-27T21:32:29.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/26/5726e8df7d36e96bb3c679912d1a86af42f393d77aa17d6b98a97d4289ce/onnx-1.21.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82aa6ab51144df07c58c4850cb78d4f1ae969d8c0bf657b28041796d49ba6974", size = 17534821, upload-time = "2026-03-27T21:32:32.351Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2b/021dcd2dd50c3c71b7959d7368526da384a295c162fb4863f36057973f78/onnx-1.21.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c3185a232089335581fabb98fba4e86d3e8246b8140f2e406082438100ebda", size = 17616664, upload-time = "2026-03-27T21:32:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/12/00/afa32a46fa122a7ed42df1cfe8796922156a3725ba8fc581c4779c96e2fc/onnx-1.21.0-cp311-cp311-win32.whl", hash = "sha256:f53b3c15a3b539c16b99655c43c365622046d68c49b680c48eba4da2a4fb6f27", size = 16289035, upload-time = "2026-03-27T21:32:37.783Z" }, + { url = "https://files.pythonhosted.org/packages/73/8d/483cc980a24d4c0131d0af06d0ff6a37fb08ae90a7848ece8cef645194f1/onnx-1.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f78c411743db317a76e5d009f84f7e3d5380411a1567a868e82461a1e5c775d", size = 16443748, upload-time = "2026-03-27T21:32:40.337Z" }, + { url = "https://files.pythonhosted.org/packages/38/78/9d06fd5aaaed1ec9cb8a3b70fbbf00c1bdc18db610771e96379f0ed58112/onnx-1.21.0-cp311-cp311-win_arm64.whl", hash = "sha256:ab6a488dabbb172eebc9f3b3e7ac68763f32b0c571626d4a5004608f866cc83d", size = 16406123, upload-time = "2026-03-27T21:32:45.159Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ae/cb644ec84c25e63575d9d8790fdcc5d1a11d67d3f62f872edb35fa38d158/onnx-1.21.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095", size = 17965930, upload-time = "2026-03-27T21:32:48.089Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b6/eeb5903586645ef8a49b4b7892580438741acc3df91d7a5bd0f3a59ea9cb/onnx-1.21.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b", size = 17531344, upload-time = "2026-03-27T21:32:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/a7/00/4823f06357892d1e60d6f34e7299d2ba4ed2108c487cc394f7ce85a3ff14/onnx-1.21.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285", size = 17613697, upload-time = "2026-03-27T21:32:54.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/1d/391f3c567ae068c8ac4f1d1316bae97c9eb45e702f05975fe0e17ad441f0/onnx-1.21.0-cp312-abi3-win32.whl", hash = "sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e", size = 16287200, upload-time = "2026-03-27T21:32:57.277Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a6/5eefbe5b40ea96de95a766bd2e0e751f35bdea2d4b951991ec9afaa69531/onnx-1.21.0-cp312-abi3-win_amd64.whl", hash = "sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f", size = 16441045, upload-time = "2026-03-27T21:33:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/63/c4/0ed8dc037a39113d2a4d66e0005e07751c299c46b993f1ad5c2c35664c20/onnx-1.21.0-cp312-abi3-win_arm64.whl", hash = "sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb", size = 16403134, upload-time = "2026-03-27T21:33:03.987Z" }, + { url = "https://files.pythonhosted.org/packages/f8/89/0e1a9beb536401e2f45ac88735e123f2735e12fc7b56ff6c11727e097526/onnx-1.21.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f", size = 17975430, upload-time = "2026-03-27T21:33:07.005Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e6dc71a7b3b317265591b20a5f71d0ff5c0d26c24e52283139dc90c66038/onnx-1.21.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8", size = 17537435, upload-time = "2026-03-27T21:33:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/27affcac63eaf2ef183a44fd1a1354b11da64a6c72fe6f3fdcf5571bcee5/onnx-1.21.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840", size = 17617687, upload-time = "2026-03-27T21:33:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5c/ac8ed15e941593a3672ce424280b764979026317811f2e8508432bfc3429/onnx-1.21.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd", size = 16449402, upload-time = "2026-03-27T21:33:15.555Z" }, + { url = "https://files.pythonhosted.org/packages/0e/aa/d2231e0dcaad838217afc64c306c8152a080134d2034e247cc973d577674/onnx-1.21.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad", size = 16408273, upload-time = "2026-03-27T21:33:18.599Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] +dependencies = [ + { name = "flatbuffers", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "sympy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "sympy", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" }, + { url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" }, + { url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" }, + { url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" }, + { url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "protobuf", marker = "python_full_version >= '3.11'" }, + { name = "sympy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" }, + { url = "https://files.pythonhosted.org/packages/38/e9/8c901c150ce0c368da38638f44152fb411059c0c7364b497c9e5c957321a/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7", size = 15152472, upload-time = "2026-03-17T22:03:26.176Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b6/7a4df417cdd01e8f067a509e123ac8b31af450a719fa7ed81787dd6057ec/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330", size = 17222993, upload-time = "2026-03-17T22:04:34.485Z" }, + { url = "https://files.pythonhosted.org/packages/dd/59/8febe015f391aa1757fa5ba82c759ea4b6c14ef970132efb5e316665ba61/onnxruntime-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153", size = 12594863, upload-time = "2026-03-17T22:05:38.749Z" }, + { url = "https://files.pythonhosted.org/packages/32/84/4155fcd362e8873eb6ce305acfeeadacd9e0e59415adac474bea3d9281bb/onnxruntime-1.24.4-cp311-cp311-win_arm64.whl", hash = "sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b", size = 12259895, upload-time = "2026-03-17T22:05:28.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/6b89e297b93704345f0f3f8c62229bee323ef25682a3f9b4f89a39324950/onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb", size = 12596856, upload-time = "2026-03-17T22:05:41.224Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/8b8ec6e9e6a474fcd5d772453f627ad4549dfe3ab8c0bf70af5afcde551b/onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90", size = 12270275, upload-time = "2026-03-17T22:05:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, + { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "reportlab" +version = "4.4.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962, upload-time = "2026-02-12T10:45:21.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400, upload-time = "2026-02-12T10:45:18.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sympy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] +dependencies = [ + { name = "mpmath", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040, upload-time = "2024-07-19T09:26:51.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177, upload-time = "2024-07-19T09:26:48.863Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +dependencies = [ + { name = "mpmath", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "torch" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +dependencies = [ + { name = "filelock", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "fsspec", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "jinja2", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "nvidia-cublas-cu12", version = "12.1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", version = "8.9.2.26", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", version = "11.0.2.54", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", version = "10.3.2.106", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", version = "11.4.5.107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", version = "12.1.0.106", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", version = "2.19.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", version = "12.1.105", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "sympy", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, + { name = "triton", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/b3/1fcc3bccfddadfd6845dcbfe26eb4b099f1dfea5aa0e5cfb92b3c98dba5b/torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585", size = 755526581, upload-time = "2024-03-27T21:06:46.5Z" }, + { url = "https://files.pythonhosted.org/packages/3a/81/684d99e536b20e869a7c1222cf1dd233311fb05d3628e9570992bfb65760/torch-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:11e8fe261233aeabd67696d6b993eeb0896faa175c6b41b9a6c9f0334bdad1c5", size = 198579616, upload-time = "2024-03-27T21:10:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/3b/55/7192974ab13e5e5577f45d14ce70d42f5a9a686b4f57bbe8c9ab45c4a61a/torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e", size = 150788930, upload-time = "2024-03-27T21:08:09.98Z" }, + { url = "https://files.pythonhosted.org/packages/c3/33/d7a6123231bd4d04c7005dde8507235772f3bc4622a25f3a88c016415d49/torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb", size = 755555407, upload-time = "2024-03-27T21:09:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/5ab75f138bf32d7a69df61e4997e24eccad87cc009f5fb7e2a31af8a4036/torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c", size = 198584125, upload-time = "2024-03-27T21:10:06.958Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/e105b8ef6d324e789c1589e95cb0ab63f3e07c2216d68b1178b7c21b7d2a/torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059", size = 150796474, upload-time = "2024-03-27T21:09:29.142Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/d8f77363a7a3350c96e6c9db4ffb101d1c0487cc0b8cdaae1e4bfb2800ad/torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca", size = 755466713, upload-time = "2024-03-27T21:08:48.868Z" }, + { url = "https://files.pythonhosted.org/packages/72/ce/beca89dcdcf4323880d3b959ef457a4c61a95483af250e6892fec9174162/torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea", size = 198528804, upload-time = "2024-03-27T21:09:14.691Z" }, + { url = "https://files.pythonhosted.org/packages/79/78/29dcab24a344ffd9ee9549ec0ab2c7885c13df61cde4c65836ee275efaeb/torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533", size = 150797270, upload-time = "2024-03-27T21:08:29.623Z" }, +] + +[[package]] +name = "torch" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11' and platform_machine != 's390x' and platform_machine != 'x86_64'", + "python_full_version < '3.11' and platform_machine == 's390x'", +] +dependencies = [ + { name = "filelock", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" }, + { name = "fsspec", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" }, + { name = "jinja2", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'x86_64'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", version = "12.4.5.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", version = "9.1.0.70", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", version = "11.2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", version = "10.3.5.147", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", version = "11.6.1.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", version = "12.3.1.170", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", version = "2.21.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", version = "12.4.127", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" }, + { name = "triton", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or platform_machine != 'x86_64'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/81/aa9ab58ec10264c1abe62c8b73f5086c3c558885d6beecebf699f0dbeaeb/torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6860df13d9911ac158f4c44031609700e1eba07916fff62e21e6ffa0a9e01961", size = 766685561, upload-time = "2025-01-29T16:19:12.12Z" }, + { url = "https://files.pythonhosted.org/packages/86/86/e661e229df2f5bfc6eab4c97deb1286d598bbeff31ab0cdb99b3c0d53c6f/torch-2.6.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c4f103a49830ce4c7561ef4434cc7926e5a5fe4e5eb100c19ab36ea1e2b634ab", size = 95751887, upload-time = "2025-01-29T16:27:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/20/e0/5cb2f8493571f0a5a7273cd7078f191ac252a402b5fb9cb6091f14879109/torch-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:56eeaf2ecac90da5d9e35f7f35eb286da82673ec3c582e310a8d1631a1c02341", size = 204165139, upload-time = "2025-01-29T16:27:11.63Z" }, + { url = "https://files.pythonhosted.org/packages/e5/16/ea1b7842413a7b8a5aaa5e99e8eaf3da3183cc3ab345ad025a07ff636301/torch-2.6.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:09e06f9949e1a0518c5b09fe95295bc9661f219d9ecb6f9893e5123e10696628", size = 66520221, upload-time = "2025-01-29T16:22:18.862Z" }, + { url = "https://files.pythonhosted.org/packages/78/a9/97cbbc97002fff0de394a2da2cdfa859481fdca36996d7bd845d50aa9d8d/torch-2.6.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:7979834102cd5b7a43cc64e87f2f3b14bd0e1458f06e9f88ffa386d07c7446e1", size = 766715424, upload-time = "2025-01-29T16:25:15.874Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fa/134ce8f8a7ea07f09588c9cc2cea0d69249efab977707cf67669431dcf5c/torch-2.6.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd0320411fe1a3b3fec7b4d3185aa7d0c52adac94480ab024b5c8f74a0bf1d", size = 95759416, upload-time = "2025-01-29T16:27:38.429Z" }, + { url = "https://files.pythonhosted.org/packages/11/c5/2370d96b31eb1841c3a0883a492c15278a6718ccad61bb6a649c80d1d9eb/torch-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:46763dcb051180ce1ed23d1891d9b1598e07d051ce4c9d14307029809c4d64f7", size = 204164970, upload-time = "2025-01-29T16:26:16.182Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fa/f33a4148c6fb46ca2a3f8de39c24d473822d5774d652b66ed9b1214da5f7/torch-2.6.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:94fc63b3b4bedd327af588696559f68c264440e2503cc9e6954019473d74ae21", size = 66530713, upload-time = "2025-01-29T16:26:38.881Z" }, + { url = "https://files.pythonhosted.org/packages/e5/35/0c52d708144c2deb595cd22819a609f78fdd699b95ff6f0ebcd456e3c7c1/torch-2.6.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2bb8987f3bb1ef2675897034402373ddfc8f5ef0e156e2d8cfc47cacafdda4a9", size = 766624563, upload-time = "2025-01-29T16:23:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/455ab3fbb2c61c71c8842753b566012e1ed111e7a4c82e0e1c20d0c76b62/torch-2.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b789069020c5588c70d5c2158ac0aa23fd24a028f34a8b4fcb8fcb4d7efcf5fb", size = 95607867, upload-time = "2025-01-29T16:25:55.649Z" }, + { url = "https://files.pythonhosted.org/packages/18/cf/ae99bd066571656185be0d88ee70abc58467b76f2f7c8bfeb48735a71fe6/torch-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e1448426d0ba3620408218b50aa6ada88aeae34f7a239ba5431f6c8774b1239", size = 204120469, upload-time = "2025-01-29T16:24:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/81/b4/605ae4173aa37fb5aa14605d100ff31f4f5d49f617928c9f486bb3aaec08/torch-2.6.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:9a610afe216a85a8b9bc9f8365ed561535c93e804c2a317ef7fabcc5deda0989", size = 66532538, upload-time = "2025-01-29T16:24:18.976Z" }, + { url = "https://files.pythonhosted.org/packages/24/85/ead1349fc30fe5a32cadd947c91bda4a62fbfd7f8c34ee61f6398d38fb48/torch-2.6.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf", size = 766626191, upload-time = "2025-01-29T16:17:26.26Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b0/26f06f9428b250d856f6d512413e9e800b78625f63801cbba13957432036/torch-2.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b", size = 95611439, upload-time = "2025-01-29T16:21:21.061Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9c/fc5224e9770c83faed3a087112d73147cd7c7bfb7557dcf9ad87e1dda163/torch-2.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc", size = 204126475, upload-time = "2025-01-29T16:21:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/8b/d60c0491ab63634763be1537ad488694d316ddc4a20eaadd639cedc53971/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2", size = 66536783, upload-time = "2025-01-29T16:22:08.559Z" }, +] + +[[package]] +name = "triton" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64'", +] +dependencies = [ + { name = "filelock", marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/05/ed974ce87fe8c8843855daa2136b3409ee1c126707ab54a8b72815c08b49/triton-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2294514340cfe4e8f4f9e5c66c702744c4a117d25e618bd08469d0bfed1e2e5", size = 167900779, upload-time = "2024-01-10T03:11:56.576Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ac/3974caaa459bf2c3a244a84be8d17561f631f7d42af370fc311defeca2fb/triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0", size = 167928356, upload-time = "2024-01-10T03:12:05.923Z" }, + { url = "https://files.pythonhosted.org/packages/0e/49/2e1bbae4542b8f624e409540b4197e37ab22a88e8685e99debe721cc2b50/triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5", size = 167933985, upload-time = "2024-01-10T03:12:14.556Z" }, +] + +[[package]] +name = "triton" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/65/3ffa90e158a2c82f0716eee8d26a725d241549b7d7aaf7e4f44ac03ebd89/triton-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e54983cd51875855da7c68ec05c05cf8bb08df361b1d5b69e05e40b0c9bd62", size = 253090354, upload-time = "2025-01-22T19:12:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2e/757d2280d4fefe7d33af7615124e7e298ae7b8e3bc4446cdb8e88b0f9bab/triton-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8009a1fb093ee8546495e96731336a33fb8856a38e45bb4ab6affd6dbc3ba220", size = 253157636, upload-time = "2025-01-22T19:12:51.322Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/59500052cb1cf8cf5316be93598946bc451f14072c6ff256904428eaf03c/triton-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b215efc1c26fa7eefb9a157915c92d52e000d2bf83e5f69704047e63f125c", size = 253159365, upload-time = "2025-01-22T19:13:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/37a3384d1e2e9320331baca41e835e90a3767303642c7a80d4510152cbcf/triton-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0", size = 253154278, upload-time = "2025-01-22T19:13:54.221Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/c5/aff062c66b42e2183201a7ace10c6b2e959a9a16525c8e8ca8e59410d27a/virtualenv-21.2.1.tar.gz", hash = "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", size = 5844770, upload-time = "2026-04-09T18:47:11.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/0e/f083a76cb590e60dff3868779558eefefb8dfb7c9ed020babc7aa014ccbf/virtualenv-21.2.1-py3-none-any.whl", hash = "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2", size = 5828326, upload-time = "2026-04-09T18:47:09.331Z" }, +] From d68fbe431f048a0c6443d284665087c44b1c549f Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:34:59 +0200 Subject: [PATCH 07/26] chore: added basic pre-commit hooks --- .gitignore | 2 +- .pre-commit-config.yaml | 6 +++ .readthedocs.yaml | 2 - README.md | 54 +++++++++---------- case-studies/README.md | 6 +-- .../dataset/test/test_set.csv | 2 +- .../dataset/training/test1.csv | 2 +- .../dataset/training/test2.csv | 2 +- .../dataset/validation/test3.csv | 2 +- .../comparison/saved/msd_final_with_PID.json | 2 +- .../mass_spring_damper/msd-data/stats.txt | 2 +- .../mass_spring_damper/saved/msd_final.json | 2 +- .../saved/msd_preliminary.json | 2 +- .../neuralODE/saved/neuralODE_msd.json | 2 +- codecov.yml | 2 +- docs/_autodoc/dataset_creation/index.rst | 2 +- .../dataset_creation/loader_module.rst | 2 +- docs/_autodoc/export/exporter_module.rst | 2 +- docs/_autodoc/export/index.rst | 2 +- docs/_autodoc/getting_started/index.rst | 35 ++++++------ docs/_autodoc/glossary/index.rst | 6 +-- docs/_autodoc/inference/index.rst | 2 +- docs/_autodoc/inference/inference_module.rst | 2 +- .../model_composition/composer_module.rst | 2 +- docs/_autodoc/model_composition/index.rst | 2 +- .../modely_execution_model.rst | 2 +- .../model_composition/relation_module.rst | 2 +- docs/_autodoc/model_definition/index.rst | 4 +- .../layers/equationlearner_module.rst | 2 +- .../model_definition/layers/fir_module.rst | 2 +- .../layers/fuzzify_module.rst | 2 +- .../model_definition/layers/linear_module.rst | 2 +- .../layers/localmodel_module.rst | 2 +- .../layers/parametricfunction_module.rst | 2 +- .../model_definition/layers/part_module.rst | 2 +- .../layers/trigonometric_module.rst | 2 +- .../msnn_ins_out_param/parameter_module.rst | 2 +- .../training/earlystopping_module.rst | 2 +- docs/_autodoc/training/optimizer_module.rst | 2 +- docs/_autodoc/training/trainer_module.rst | 4 +- .../tutorials/examples/data/example.csv | 2 +- .../dataset/other_data/weights_keras.json | 2 +- .../examples/vehicle_data/vehicle.csv | 2 +- docs/_autodoc/validation/validator_module.rst | 2 +- .../compser_module_ex/addClosedLoop.rst | 1 - .../compser_module_ex/addConnect.rst | 2 +- .../compser_module_ex/addModel.rst | 2 +- .../compser_module_ex/neuralizeModel.rst | 4 +- .../compser_module_ex/removeConnection.rst | 2 +- .../compser_module_ex/removeModel.rst | 2 +- .../export_module_ex/exportONNX.rst | 2 +- .../export_module_ex/exportPythonModel.rst | 2 +- .../export_module_ex/exportReport.rst | 2 +- .../export_module_ex/importPythonModel.rst | 2 +- .../export_module_ex/loadModel.rst | 2 +- .../export_module_ex/loadTorchModel.rst | 2 +- .../export_module_ex/onnxInference.rst | 2 +- .../export_module_ex/saveModel.rst | 2 +- .../export_module_ex/saveTorchModel.rst | 2 +- .../inference_module_ex/inference.rst | 2 +- docs/examples_basics/input_module_ex/z.rst | 2 +- .../activation_module_ex/elu.rst | 2 +- .../activation_module_ex/identity.rst | 2 +- .../activation_module_ex/relu.rst | 2 +- .../activation_module_ex/sigmoid.rst | 2 +- .../activation_module_ex/softmax.rst | 2 +- .../arithmetic_module_ex/add.rst | 2 +- .../arithmetic_module_ex/div.rst | 2 +- .../arithmetic_module_ex/mul.rst | 2 +- .../arithmetic_module_ex/neg.rst | 2 +- .../arithmetic_module_ex/pow.rst | 2 +- .../arithmetic_module_ex/sub.rst | 2 +- docs/examples_basics/layer_module_ex/fir.rst | 4 +- .../layer_module_ex/part_module/part.rst | 4 +- .../part_module/sample_part.rst | 4 +- .../part_module/sample_select.rst | 2 +- .../layer_module_ex/part_module/select.rst | 2 +- .../layer_module_ex/part_module/time_part.rst | 2 +- .../layer_module_ex/trig_module_ex/cos.rst | 2 +- .../layer_module_ex/trig_module_ex/cosh.rst | 2 +- .../layer_module_ex/trig_module_ex/sech.rst | 2 +- .../layer_module_ex/trig_module_ex/sin.rst | 2 +- .../layer_module_ex/trig_module_ex/tan.rst | 2 +- .../layer_module_ex/trig_module_ex/tanh.rst | 2 +- .../parameter_module_ex/sample_time.rst | 2 +- .../trainer_module_ex/addMinimize.rst | 2 +- .../trainer_module_ex/removeMinimize.rst | 2 +- .../trainer_module_ex/trainModel.rst | 2 +- docs/index.rst | 17 +++--- docs/requirements.txt | 2 +- tests/get_samples_data/data.csv | 2 +- tests/multifile/file1.csv | 2 +- tests/multifile/file2.csv | 2 +- tests/multifile/file3.csv | 2 +- tests/multifile2/file1.csv | 2 +- tests/multifile2/file2.csv | 2 +- tests/multifile2/file3.csv | 2 +- tests/multifile3/file1.csv | 2 +- tests/test_data/testdata.dta | 1 - tests/val_data/testdata.dta | 1 - tests/vector_data/vector_2.dta | 1 - tests/vehicle_data/vehicle.csv | 2 +- 102 files changed, 161 insertions(+), 163 deletions(-) diff --git a/.gitignore b/.gitignore index 3a8652ff..5ca985b7 100644 --- a/.gitignore +++ b/.gitignore @@ -179,4 +179,4 @@ TODO/ #trained_models folder trained_models -trained_models_torch \ No newline at end of file +trained_models_torch diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c52b190..53580b84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,12 @@ default_install_hook_types: - post-rewrite repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace - repo: local hooks: - id: uv-sync diff --git a/.readthedocs.yaml b/.readthedocs.yaml index a2940afa..c0248145 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -22,5 +22,3 @@ python: - method: pip path: . - requirements: docs/requirements.txt - - diff --git a/README.md b/README.md index 46da35cb..f7d87cf0 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ Modeling, control, and estimation of physical systems are central to many engineering disciplines. While data-driven methods like neural networks offer powerful tools, they often struggle to **incorporate prior domain knowledge**, limiting their interpretability, generalizability, and safety. -To bridge this gap, we present ***nnodely*** (where "nn" can be read as "m," forming *Modely*) — a framework that facilitates the creation and deployment of **Model-Structured Neural Networks** (**MS-NNs**). +To bridge this gap, we present ***nnodely*** (where "nn" can be read as "m," forming *Modely*) — a framework that facilitates the creation and deployment of **Model-Structured Neural Networks** (**MS-NNs**). MS-NNs combine the learning capabilities of neural networks with structural **priors** grounded in **physics, control, and estimation theory**, enabling: -- **Reduced training data** requirements -- **Generalization** to unseen scenarios -- **Real-time** deployment in real-world applications +- **Reduced training data** requirements +- **Generalization** to unseen scenarios +- **Real-time** deployment in real-world applications In short: @@ -126,10 +126,10 @@ The `nnodely` main class defined in __nnodely.py__, it contains all the main pro 2. __loader.py__ contains the function for managing the dataset, the main function is `dataLoad`. 3. __trainer.py__ contains the function for training the network as the `trainModel`. 4. __exporter.py__ contains all the function for import and export: `saveModel`, `loadModel`, `exportONNX` etc.. -5. __validator.py__ contains all the function for validate the model and the `resultsAnalysis`. +5. __validator.py__ contains all the function for validate the model and the `resultsAnalysis`. 6. All the operators derive from `Network` defined in __network.py__, that contains the shared support functions for all the operators. -The folder `basic/` contains the main classes for the low level functionalities: +The folder `basic/` contains the main classes for the low level functionalities: 1. __model.py__ containts the pytorch template model for the structured network. 2. __modeldef.py__ containts the operation for work with the json model definition. 3. __loss.py__ contains the loss functions. @@ -153,14 +153,14 @@ The main basic layers without parameters are: 2. __arithmetic.py__ this file contains the aritmetic functions as: +, -, /, *., **. 3. __trigonometric.py__ this file contains all the trigonometric functions. 4. __part.py__ are used for selecting part of the data. -5. __fuzzify.py__ contains the operation for the fuzzification of a variable, +5. __fuzzify.py__ contains the operation for the fuzzification of a variable, commonly used in the local model as activation function as in [[1]](#1) with rectangular activation functions or in [[3]](#3), [[4]](#4) and [[5]](#5) with triangular activation function activation functions. Using fuzzification it is also possible create a channel coding as presented in [[2]](#2). The main basic layers with parameters are: -1. __fir.py__ this file contains the finite impulse response filter function. It is a linear operation on the time dimension (second dimension). +1. __fir.py__ this file contains the finite impulse response filter function. It is a linear operation on the time dimension (second dimension). This filter was introduced in [[1]](#1). -2. __linear.py__ this file contains the linear function. Typical Linear operation `W*x+b` operated on the space dimension (third dimension). +2. __linear.py__ this file contains the linear function. Typical Linear operation `W*x+b` operated on the space dimension (third dimension). This operation is presented in [[1]](#1). 3. __localmodel.py__ this file contains the logic for build a local model. This operation is presented in [[1]](#1), [[3]](#3), [[4]](#4) and [[5]](#5). 4. __parametricfunction.py__ are the user custom function. The function can use the pytorch syntax. A parametric function is presented in [[3]](#3), [[4]](#4), [[5]](#5). @@ -196,8 +196,8 @@ This folder contains the images used in the documentation. To contribute to the nnodely framework, you can: -- Open a pull request if you have a new feature or bug fix. -- Open an issue if you have a question or suggestion. +- Open a pull request if you have a new feature or bug fix. +- Open an issue if you have a question or suggestion. We welcome contributions and collaborations. @@ -212,53 +212,53 @@ This project is released under the license [License: MIT](https://opensource.org ## References -[1] -Mauro Da Lio, Daniele Bortoluzzi, Gastone Pietro Rosati Papini. (2019). -Modelling longitudinal vehicle dynamics with neural networks. +[1] +Mauro Da Lio, Daniele Bortoluzzi, Gastone Pietro Rosati Papini. (2019). +Modelling longitudinal vehicle dynamics with neural networks. Vehicle System Dynamics. https://doi.org/10.1080/00423114.2019.1638947 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/model_longit_vehicle_dynamics/model_longit_vehicle_dynamics.py)) -[2] -Alice Plebe, Mauro Da Lio, Daniele Bortoluzzi. (2019). -On Reliable Neural Network Sensorimotor Control in Autonomous Vehicles. +[2] +Alice Plebe, Mauro Da Lio, Daniele Bortoluzzi. (2019). +On Reliable Neural Network Sensorimotor Control in Autonomous Vehicles. IEEE Transaction on Intelligent Transportation System. https://doi.org/10.1109/TITS.2019.2896375 -[3] -Mauro Da Lio, Riccardo Donà, Gastone Pietro Rosati Papini, Francesco Biral, Henrik Svensson. (2020). +[3] +Mauro Da Lio, Riccardo Donà, Gastone Pietro Rosati Papini, Francesco Biral, Henrik Svensson. (2020). A Mental Simulation Approach for Learning Neural-Network Predictive Control (in Self-Driving Cars). IEEE Access. https://doi.org/10.1109/ACCESS.2020.3032780 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/model_lateral_vehicle_dynamics/model_lateral_vehicle_dynamics.ipynb)) -[4] -Edoardo Pagot, Mattia Piccinini, Enrico Bertolazzi, Francesco Biral. (2023). +[4] +Edoardo Pagot, Mattia Piccinini, Enrico Bertolazzi, Francesco Biral. (2023). Fast Planning and Tracking of Complex Autonomous Parking Maneuvers With Optimal Control and Pseudo-Neural Networks. IEEE Access. https://doi.org/10.1109/ACCESS.2023.3330431 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/control_steer_car_parking/control_steer_car_parking.ipynb)) -[5] +[5] Mattia Piccinini, Sebastiano Taddei, Matteo Larcher, Mattia Piazza, Francesco Biral. (2023). A Physics-Driven Artificial Agent for Online Time-Optimal Vehicle Motion Planning and Control. IEEE Access. https://doi.org/10.1109/ACCESS.2023.3274836 (look [[code basic]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/control_steer_artificial_race_driver/control_steer_artificial_race_driver.ipynb) and [[code extended]](https://github.com/tonegas/nnodely-applications/blob/main/vehicle/control_steer_artificial_race_driver_extended/control_steer_artificial_race_driver_extended.ipynb)) -[6] +[6] Hector Perez-Villeda, Justus Piater, Matteo Saveriano. (2023). Learning and extrapolation of robotic skills using task-parameterized equation learner networks. Robotics and Autonomous Systems. https://doi.org/10.1016/j.robot.2022.104309 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/equation_learner/equation_learner.ipynb)) -[7] +[7] M. Raissi. P. Perdikaris b, G.E. Karniadakis a. (2019). Physics-informed neural networks: A deep learning framework for solving forward and inverse problems involving nonlinear partial differential equations Journal of Computational Physics. https://doi.org/10.1016/j.jcp.2018.10.045 (look the [[example Burger's equation]](https://github.com/tonegas/nnodely-applications/blob/main/pinn/pinn_Burgers_equation.ipynb)) -[8] +[8] Wojciech Marian Czarnecki, Simon Osindero, Max Jaderberg, Grzegorz Świrszcz, Razvan Pascanu. (2017). Sobolev Training for Neural Networks. arXiv. https://doi.org/10.48550/arXiv.1706.04859 (look the [[code]](https://github.com/tonegas/nnodely-applications/blob/main/sobolev/Sobolev_learning.ipynb)) -[9] +[9] Mattia Piccinini, Matteo Zumerle, Johannes Betz, Gastone Pietro Rosati Papini. (2025). A Road Friction-Aware Anti-Lock Braking System Based on Model-Structured Neural Networks. IEEE Open Journal of Intelligent Transportation Systems. https://doi.org/10.1109/OJITS.2025.3563347 (look at the [[code]](https://github.com/tonegas/nnodely-applications/tree/main/vehicle/road_friction_aware_ABS)) -[10] +[10] Mauro Da Lio, Mattia Piccinini, Francesco Biral. (2023). Robust and Sample-Efficient Estimation of Vehicle Lateral Velocity Using Neural Networks With Explainable Structure Informed by Kinematic Principles. IEEE Transactions on Intelligent Transportation Systems. https://doi.org/10.1109/TITS.2023.3303776 diff --git a/case-studies/README.md b/case-studies/README.md index f65f7b18..2ec3b9aa 100644 --- a/case-studies/README.md +++ b/case-studies/README.md @@ -8,9 +8,9 @@ The subfolders are organized as follows: Contains three Python notebooks (plus all the other relative files) relative to the lateral vehicle dynamics model: -- `lateral_dynamics_model.ipynp`: Design of the lateral vehicle dynamics model as presented in the paper +- `lateral_dynamics_model.ipynp`: Design of the lateral vehicle dynamics model as presented in the paper - `lateral_dynamics_control.ipynp`: Design and training of the lateral controller as presented in the paper -- `lateral_dynamics_model_torch.ipynp`: A comparative implementation of the lateral dynamics model developed in native **PyTorch** +- `lateral_dynamics_model_torch.ipynp`: A comparative implementation of the lateral dynamics model developed in native **PyTorch** ## 2. `mass_spring_damper` @@ -25,4 +25,4 @@ Provides an example implementation of a Physics-Informed Neural Network (PINN), ## 4. `neuralODE` -Contains a preliminary implementation of Neural ODEs within the nnodely framework, illustrating continuous-time modeling via parametric functions and integration operators for the mass-spring-damper-system. \ No newline at end of file +Contains a preliminary implementation of Neural ODEs within the nnodely framework, illustrating continuous-time modeling via parametric functions and integration operators for the mass-spring-damper-system. diff --git a/case-studies/lateral_dynamics/dataset/test/test_set.csv b/case-studies/lateral_dynamics/dataset/test/test_set.csv index 9c323599..c1555ec9 100644 --- a/case-studies/lateral_dynamics/dataset/test/test_set.csv +++ b/case-studies/lateral_dynamics/dataset/test/test_set.csv @@ -1103,4 +1103,4 @@ time,ax,ay,vx,curv,steer 55.146002028075564,1.0840258510987684,-2.6915592509185298,20.453059684147426,-0.006434242005669751,-0.3608481332433391 55.195998943798315,1.0851748822292842,-2.697148256568109,20.507079535715768,-0.006413818184476849,-0.35926049452967035 55.24600044829564,1.0880632184191892,-2.6997673049851385,20.561189941463603,-0.0063864243157048545,-0.3573006906046246 -55.2959998400224,1.0925590253392408,-2.699641748410116,20.615475400176003,-0.006352663418874661,-0.3550151089245937 \ No newline at end of file +55.2959998400224,1.0925590253392408,-2.699641748410116,20.615475400176003,-0.006352663418874661,-0.3550151089245937 diff --git a/case-studies/lateral_dynamics/dataset/training/test1.csv b/case-studies/lateral_dynamics/dataset/training/test1.csv index b9d55044..4fdce233 100644 --- a/case-studies/lateral_dynamics/dataset/training/test1.csv +++ b/case-studies/lateral_dynamics/dataset/training/test1.csv @@ -3722,4 +3722,4 @@ time,ax,ay,vx,curv,brake,gas,steer,s 186,-0.1071997657,-1.682893157,26.17891693,-0.002456563614,0,0.06283919513,-0.1276736856,2305.977185 186.05,-0.1293833256,-1.641584277,26.17271233,-0.002397477173,0,0.05899731815,-0.1237370819,2307.311642 186.1,-0.1502973288,-1.598474264,26.16544533,-0.002335893822,0,0.05537641793,-0.1197138876,2308.640748 -186.15,-0.1705370843,-1.553611755,26.15716553,-0.002271854197,0,0.05186513811,-0.1156224832,2309.964296 \ No newline at end of file +186.15,-0.1705370843,-1.553611755,26.15716553,-0.002271854197,0,0.05186513811,-0.1156224832,2309.964296 diff --git a/case-studies/lateral_dynamics/dataset/training/test2.csv b/case-studies/lateral_dynamics/dataset/training/test2.csv index dec669df..0c06446d 100644 --- a/case-studies/lateral_dynamics/dataset/training/test2.csv +++ b/case-studies/lateral_dynamics/dataset/training/test2.csv @@ -2440,4 +2440,4 @@ time,ax,ay,vx,curv,brake,gas,steer,s 121.9,0.009731287137,-1.63340795,27.77533531,-0.00211779615,0,0.08593299985,-0.1145167798,2307.353851 121.95,0.009520187043,-1.607111335,27.77551842,-0.002083710209,0,0.08586709201,-0.1123358831,2308.764477 122,0.009291882627,-1.579890013,27.77569962,-0.002048425176,0,0.08579877764,-0.1101001278,2310.169094 -122.038,0,0,0,-0.002021582034,0,0.0857468918,-0.1084142625,0.04364157261 \ No newline at end of file +122.038,0,0,0,-0.002021582034,0,0.0857468918,-0.1084142625,0.04364157261 diff --git a/case-studies/lateral_dynamics/dataset/validation/test3.csv b/case-studies/lateral_dynamics/dataset/validation/test3.csv index b8139727..046dd09c 100644 --- a/case-studies/lateral_dynamics/dataset/validation/test3.csv +++ b/case-studies/lateral_dynamics/dataset/validation/test3.csv @@ -2836,4 +2836,4 @@ time,ax,ay,vx,curv,brake,gas,steer,s 141.7,0.01632077061,-1.733056784,27.7700386,-0.002248041434,0,0.08714535087,-0.1201838106,2306.726898 141.75,0.01580184698,-1.697605133,27.77050591,-0.002202034247,0,0.08702037483,-0.1172332019,2308.139691 141.8,0.01531017479,-1.660968184,27.77096176,-0.002154490374,0,0.08689385653,-0.1142256781,2309.546658 -141.85,0.0148289185,-1.623295903,27.77140617,-0.002105606111,0,0.08676507324,-0.1111633554,2310.947616 \ No newline at end of file +141.85,0.0148289185,-1.623295903,27.77140617,-0.002105606111,0,0.08676507324,-0.1111633554,2310.947616 diff --git a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json index 8a4622e2..46ab942d 100644 --- a/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json +++ b/case-studies/mass_spring_damper/comparison/saved/msd_final_with_PID.json @@ -111,4 +111,4 @@ "Sub13": ["Sub", ["SamplePart10", "SamplePart12"]], "Sub29": ["Sub", ["SamplePart26", "SamplePart28"]], "TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]], - "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}} \ No newline at end of file + "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}} diff --git a/case-studies/mass_spring_damper/msd-data/stats.txt b/case-studies/mass_spring_damper/msd-data/stats.txt index 765759d4..c13bd6c6 100644 --- a/case-studies/mass_spring_damper/msd-data/stats.txt +++ b/case-studies/mass_spring_damper/msd-data/stats.txt @@ -1 +1 @@ -{"model_attributes":["linear"],"num_simulations":100,"elapsed_time":5.742344375,"total_samples":200100,"params":{"time":20,"sampling_time":0.001,"data_sampling_time":0.01,"m":1,"k":3,"c":0.175,"a1":3,"a2":2,"d":0.5,"s":1,"x0":[0,1],"v0":[0,0.5],"force":[-3,3]}} \ No newline at end of file +{"model_attributes":["linear"],"num_simulations":100,"elapsed_time":5.742344375,"total_samples":200100,"params":{"time":20,"sampling_time":0.001,"data_sampling_time":0.01,"m":1,"k":3,"c":0.175,"a1":3,"a2":2,"d":0.5,"s":1,"x0":[0,1],"v0":[0,0.5],"force":[-3,3]}} diff --git a/case-studies/mass_spring_damper/saved/msd_final.json b/case-studies/mass_spring_damper/saved/msd_final.json index df315ad3..602f2f30 100644 --- a/case-studies/mass_spring_damper/saved/msd_final.json +++ b/case-studies/mass_spring_damper/saved/msd_final.json @@ -44,4 +44,4 @@ "Fir5": ["Fir", ["TimePart4"], "PFir5W", null, 0], "SamplePart8": ["SamplePart", ["x_t"], -1, [0, 1]], "TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]], - "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}} \ No newline at end of file + "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}} diff --git a/case-studies/mass_spring_damper/saved/msd_preliminary.json b/case-studies/mass_spring_damper/saved/msd_preliminary.json index 9b325312..afd36bae 100644 --- a/case-studies/mass_spring_damper/saved/msd_preliminary.json +++ b/case-studies/mass_spring_damper/saved/msd_preliminary.json @@ -44,4 +44,4 @@ "Fir5": ["Fir", ["TimePart4"], "PFir5W", null, 0], "SamplePart8": ["SamplePart", ["x_t"], -1, [0, 1]], "TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]], - "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}} \ No newline at end of file + "TimePart4": ["TimePart", ["F"], -1, [-0.1, 0]]}} diff --git a/case-studies/neuralODE/saved/neuralODE_msd.json b/case-studies/neuralODE/saved/neuralODE_msd.json index 47ab6487..32187883 100644 --- a/case-studies/neuralODE/saved/neuralODE_msd.json +++ b/case-studies/neuralODE/saved/neuralODE_msd.json @@ -54,4 +54,4 @@ "Select7": ["Select", ["NeuralODE5"], 2, 0], "TimePart1": ["TimePart", ["x"], -1, [-0.1, 0]], "TimePart3": ["TimePart", ["F"], -1, [-0.1, 0]], - "TimePart8": ["TimePart", ["Select7"], 0.1, [0.09, 0.1]]}} \ No newline at end of file + "TimePart8": ["TimePart", ["Select7"], 0.1, [0.09, 0.1]]}} diff --git a/codecov.yml b/codecov.yml index 1575fa2e..3d07b274 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,4 +3,4 @@ coverage: project: default: target: auto - threshold: 1% # the leniency in hitting the target \ No newline at end of file + threshold: 1% # the leniency in hitting the target diff --git a/docs/_autodoc/dataset_creation/index.rst b/docs/_autodoc/dataset_creation/index.rst index b3fc20fc..490fe0b6 100644 --- a/docs/_autodoc/dataset_creation/index.rst +++ b/docs/_autodoc/dataset_creation/index.rst @@ -13,7 +13,7 @@ Once loaded, via the :func:`loadData() `, such as interpolation, as well as utilities for extracting specific temporal intervals. These features enable controlled experimentation under different operating conditions while maintaining alignment with the model's temporal structure. .. rubric:: Multi-File handling - + The framework also supports a multi-file dataset mode, where a directory of data files is treated as a single logical dataset. Data from different files are processed independently and concatenated while preserving temporal coherence, ensuring that valid temporal windows are constructed separately for each sequence. This capability is essential for recurrent training and closed-loop prediction scenarios, where temporal consistency across multiple trajectories must be strictly maintained. .. .. rubric:: Key Benefits diff --git a/docs/_autodoc/dataset_creation/loader_module.rst b/docs/_autodoc/dataset_creation/loader_module.rst index 2a65971e..87ba3a52 100644 --- a/docs/_autodoc/dataset_creation/loader_module.rst +++ b/docs/_autodoc/dataset_creation/loader_module.rst @@ -11,4 +11,4 @@ Data Loader module .. autofunction:: nnodely.operators.loader.Loader.loadData For more examples of how to use the data loader, please refer to the -:doc:`relative tutorial <../tutorials/examples/dataset>`. \ No newline at end of file +:doc:`relative tutorial <../tutorials/examples/dataset>`. diff --git a/docs/_autodoc/export/exporter_module.rst b/docs/_autodoc/export/exporter_module.rst index 5ac63a59..ddd3775f 100644 --- a/docs/_autodoc/export/exporter_module.rst +++ b/docs/_autodoc/export/exporter_module.rst @@ -15,4 +15,4 @@ Export module .. autofunction:: nnodely.operators.exporter.Exporter.onnxInference .. autofunction:: nnodely.operators.exporter.Exporter.exportReport -For more examples of how to use the export module, please refer to the :doc:`export tutorial <../tutorials/examples/export>`. \ No newline at end of file +For more examples of how to use the export module, please refer to the :doc:`export tutorial <../tutorials/examples/export>`. diff --git a/docs/_autodoc/export/index.rst b/docs/_autodoc/export/index.rst index ebe5da0f..6d85bd35 100644 --- a/docs/_autodoc/export/index.rst +++ b/docs/_autodoc/export/index.rst @@ -69,7 +69,7 @@ research workflows and real-world deployment, ensuring that models can be easily integrated into diverse application environments. .. rubric:: Additional tools - + Training and validation reports (PDF) can be generated from results using :func:`exportReport() `. diff --git a/docs/_autodoc/getting_started/index.rst b/docs/_autodoc/getting_started/index.rst index 534c95bf..121b2b06 100644 --- a/docs/_autodoc/getting_started/index.rst +++ b/docs/_autodoc/getting_started/index.rst @@ -174,13 +174,13 @@ Reacher Estimator Here is simple two-joint planar manipulator. The inputs are the joint angles :math:`\theta_1` and :math:`\theta_2`, while the outputs are the end-effector coordinates :math:`(x, y)`. The link lengths :math:`l_1` and :math:`l_2` are unknown and are estimated from data using *nnodely* as learnable parameters. - + The kinematic model is given by: -.. math:: - x = l_1 \cos(\theta_1) + l_2 \cos(\theta_1 + \theta_2), \quad +.. math:: + x = l_1 \cos(\theta_1) + l_2 \cos(\theta_1 + \theta_2), \quad y = l_1 \sin(\theta_1) + l_2 \sin(\theta_1 + \theta_2). @@ -188,7 +188,7 @@ The kinematic model is given by: **Local Module Path Configuration and Package Import** -First, we ensure Python can locate modules in the current working directory, +First, we ensure Python can locate modules in the current working directory, enabling the import of nnodely components for use in the script. .. code-block:: python @@ -204,8 +204,8 @@ enabling the import of nnodely components for use in the script. - - Input variables are created using the :class:`Input` class. The learnable parameters are given within the :class:`Parameter`. The :class:`Output` class defines the model output and takes two arguments: + + Input variables are created using the :class:`Input` class. The learnable parameters are given within the :class:`Parameter`. The :class:`Output` class defines the model output and takes two arguments: the name of the output and its structure. @@ -219,7 +219,7 @@ enabling the import of nnodely components for use in the script. l1 = Parameter('l1') #parameters to be estimated l2 = Parameter('l2') #parameters to be estimated - + x_out = Output('x_out', (l1 * Cos(theta1.last())) + (l2 * Cos(theta1.last() + theta2.last()))) y_out = Output('y_out', (l1 * Sin(theta1.last())) + @@ -227,21 +227,21 @@ enabling the import of nnodely components for use in the script. **Model composition** -:class:`addModel` adds the defined output to the model. -:class:`addMinimize` defines the loss function. This function uses the following inputs: The first input is the name of the error (`x-error` and `y-error` in this case). The second and third inputs are the variables whose -difference we want to minimize. The fourth input is the loss function to be used, in this case the mean square error (`mse`). +:class:`addModel` adds the defined output to the model. +:class:`addMinimize` defines the loss function. This function uses the following inputs: The first input is the name of the error (`x-error` and `y-error` in this case). The second and third inputs are the variables whose +difference we want to minimize. The fourth input is the loss function to be used, in this case the mean square error (`mse`). :class:`neuralizeModel` builds the discrete-time MS-NN where its input parameter is the sampling time. .. code-block:: python - # Model composition + # Model composition model = Modely(seed=0) model.addModel('x_out', x_out) model.addModel('y_out', y_out) model.addMinimize('x-error', x_tip.last(), x_out, 'mse') # Objectives model.addMinimize('y-error', y_tip.last(), y_out, 'mse') # Objectives - model.neuralizeModel(sample_time=0.02) + model.neuralizeModel(sample_time=0.02) **Data loading** @@ -251,15 +251,15 @@ difference we want to minimize. The fourth input is the loss function to be used data_struct = ['step', 'T1','T2','theta1', 'theta2', 'x_tip', 'y_tip', 'thetadot1', 'thetadot2', 'thetaddot1', 'thetaddot2'] # dataset creation - + data_folder = os.path.join(os.getcwd(), 'dataset', 'data') - + model.loadData(name='reacher_data', source=data_folder, - format=data_struct, delimiter=';') # Data loading + format=data_struct, delimiter=';') # Data loading **Training** -Trains the model for `200` epochs (batch size `128`, learning rate `0.01`) using a `70/20/10` +Trains the model for `200` epochs (batch size `128`, learning rate `0.01`) using a `70/20/10` train-validation-test split. @@ -279,7 +279,7 @@ train-validation-test split. Code

- + -------------------------------------------------------- Applications @@ -318,4 +318,3 @@ For the tutorial please refer to the link below. Tutorials

- diff --git a/docs/_autodoc/glossary/index.rst b/docs/_autodoc/glossary/index.rst index c344a89f..be52db67 100644 --- a/docs/_autodoc/glossary/index.rst +++ b/docs/_autodoc/glossary/index.rst @@ -15,8 +15,8 @@ An MS‑NN architecture defined through inputs, outputs, and building blocks (re .. rubric:: Input / Output / Parameter -- **Input**: variables entering the model. -- **Output**: signals predicted or calculated by the model. +- **Input**: variables entering the model. +- **Output**: signals predicted or calculated by the model. - **Parameter**: quantities learned during training or fixed constants. .. rubric:: Stream @@ -61,4 +61,4 @@ Operations to save and/or convert a trained MS‑NN into standard formats (e.g., .. rubric:: Dataset -Collection of data (training / test / validation) used to train and evaluate the model. \ No newline at end of file +Collection of data (training / test / validation) used to train and evaluate the model. diff --git a/docs/_autodoc/inference/index.rst b/docs/_autodoc/inference/index.rst index a369793e..dce30664 100644 --- a/docs/_autodoc/inference/index.rst +++ b/docs/_autodoc/inference/index.rst @@ -6,4 +6,4 @@ The framework supports both single forward pass and recursive temporal horizon i .. toctree:: :maxdepth: 1 - inference_module \ No newline at end of file + inference_module diff --git a/docs/_autodoc/inference/inference_module.rst b/docs/_autodoc/inference/inference_module.rst index a11e82c4..d876a051 100644 --- a/docs/_autodoc/inference/inference_module.rst +++ b/docs/_autodoc/inference/inference_module.rst @@ -5,4 +5,4 @@ Inference module .. automethod:: Composer.__call__ -For more examples of how to use the export module, please refer to the :doc:`inference tutorial <../tutorials/examples/inference>`. \ No newline at end of file +For more examples of how to use the export module, please refer to the :doc:`inference tutorial <../tutorials/examples/inference>`. diff --git a/docs/_autodoc/model_composition/composer_module.rst b/docs/_autodoc/model_composition/composer_module.rst index 50aee0ae..ee07a62a 100644 --- a/docs/_autodoc/model_composition/composer_module.rst +++ b/docs/_autodoc/model_composition/composer_module.rst @@ -37,4 +37,4 @@ Key Composer operators: .. autofunction:: nnodely.operators.composer.Composer.removeConnection .. autofunction:: nnodely.operators.composer.Composer.neuralizeModel -For further examples please refer to the :doc:`relative tutorial <../tutorials/examples/states>`. \ No newline at end of file +For further examples please refer to the :doc:`relative tutorial <../tutorials/examples/states>`. diff --git a/docs/_autodoc/model_composition/index.rst b/docs/_autodoc/model_composition/index.rst index b2b59253..515914d9 100644 --- a/docs/_autodoc/model_composition/index.rst +++ b/docs/_autodoc/model_composition/index.rst @@ -72,4 +72,4 @@ distinct levels: relation_module composer_module - modely_execution_model \ No newline at end of file + modely_execution_model diff --git a/docs/_autodoc/model_composition/modely_execution_model.rst b/docs/_autodoc/model_composition/modely_execution_model.rst index 723ec159..ac147bd0 100644 --- a/docs/_autodoc/model_composition/modely_execution_model.rst +++ b/docs/_autodoc/model_composition/modely_execution_model.rst @@ -25,7 +25,7 @@ This dynamic composition can be performed by calling one of the following method .. code-block:: python msd.trainAndAnalyze(models='PID', closed_loop={'x':'x_n', 'x_m':'x_n'}, connect={'F':'F_PID'}, ...) - + .. .. automethod:: nnodely.nnodely.Modely.trainAndAnalyze .. :no-index: diff --git a/docs/_autodoc/model_composition/relation_module.rst b/docs/_autodoc/model_composition/relation_module.rst index cd310cff..34937ebe 100644 --- a/docs/_autodoc/model_composition/relation_module.rst +++ b/docs/_autodoc/model_composition/relation_module.rst @@ -23,7 +23,7 @@ Common Stream operators: :meth:`~nnodely.basic.relation.Stream.sw` (sample window) and :meth:`~nnodely.basic.relation.Stream.tw` (time window). These are the primitives used by temporal building blocks (FIR, recurrent windows, etc.). - + .. automodule:: nnodely.basic.relation :undoc-members: :no-inherited-members: diff --git a/docs/_autodoc/model_definition/index.rst b/docs/_autodoc/model_definition/index.rst index 17a62ea5..a8f359ff 100644 --- a/docs/_autodoc/model_definition/index.rst +++ b/docs/_autodoc/model_definition/index.rst @@ -12,9 +12,9 @@ In addition to these core components, **nnodely** provides a library of reusable .. toctree:: :maxdepth: 1 - + msnn_ins_out_param/input_module msnn_ins_out_param/parameter_module msnn_ins_out_param/initializer_module msnn_ins_out_param/output_module - layers/index \ No newline at end of file + layers/index diff --git a/docs/_autodoc/model_definition/layers/equationlearner_module.rst b/docs/_autodoc/model_definition/layers/equationlearner_module.rst index 69060cbc..475e8060 100644 --- a/docs/_autodoc/model_definition/layers/equationlearner_module.rst +++ b/docs/_autodoc/model_definition/layers/equationlearner_module.rst @@ -11,4 +11,4 @@ EquationLearner module :undoc-members: :no-inherited-members: -For more examples of how to use the equation learner module, please refer to the :doc:`EquationLearner tutorial <../../tutorials/examples/equation_learner>`. \ No newline at end of file +For more examples of how to use the equation learner module, please refer to the :doc:`EquationLearner tutorial <../../tutorials/examples/equation_learner>`. diff --git a/docs/_autodoc/model_definition/layers/fir_module.rst b/docs/_autodoc/model_definition/layers/fir_module.rst index a482ecf1..753c7aac 100644 --- a/docs/_autodoc/model_definition/layers/fir_module.rst +++ b/docs/_autodoc/model_definition/layers/fir_module.rst @@ -10,4 +10,4 @@ FIR module :undoc-members: :no-inherited-members: -For more examples of how to use the FIR module, please refer to the :doc:`FIR tutorial <../../tutorials/examples/fir>`. \ No newline at end of file +For more examples of how to use the FIR module, please refer to the :doc:`FIR tutorial <../../tutorials/examples/fir>`. diff --git a/docs/_autodoc/model_definition/layers/fuzzify_module.rst b/docs/_autodoc/model_definition/layers/fuzzify_module.rst index ba2078e5..61d0f69c 100644 --- a/docs/_autodoc/model_definition/layers/fuzzify_module.rst +++ b/docs/_autodoc/model_definition/layers/fuzzify_module.rst @@ -10,4 +10,4 @@ Fuzzify module :undoc-members: :no-inherited-members: -For more examples of how to use the fuzzify module, please refer to the :doc:`Fuzzify tutorial <../../tutorials/examples/fuzzify>`. \ No newline at end of file +For more examples of how to use the fuzzify module, please refer to the :doc:`Fuzzify tutorial <../../tutorials/examples/fuzzify>`. diff --git a/docs/_autodoc/model_definition/layers/linear_module.rst b/docs/_autodoc/model_definition/layers/linear_module.rst index dc1630c0..de0a0359 100644 --- a/docs/_autodoc/model_definition/layers/linear_module.rst +++ b/docs/_autodoc/model_definition/layers/linear_module.rst @@ -11,4 +11,4 @@ Linear module :undoc-members: :no-inherited-members: -For more examples of how to use the linear module, please refer to the :doc:`Linear tutorial <../../tutorials/examples/linear>`. \ No newline at end of file +For more examples of how to use the linear module, please refer to the :doc:`Linear tutorial <../../tutorials/examples/linear>`. diff --git a/docs/_autodoc/model_definition/layers/localmodel_module.rst b/docs/_autodoc/model_definition/layers/localmodel_module.rst index 8168d4f6..e687c318 100644 --- a/docs/_autodoc/model_definition/layers/localmodel_module.rst +++ b/docs/_autodoc/model_definition/layers/localmodel_module.rst @@ -11,4 +11,4 @@ Localmodel module :undoc-members: :no-inherited-members: -For more examples of how to use the local model module, please refer to the :doc:`LocalModel tutorial <../../tutorials/examples/localmodel>`. \ No newline at end of file +For more examples of how to use the local model module, please refer to the :doc:`LocalModel tutorial <../../tutorials/examples/localmodel>`. diff --git a/docs/_autodoc/model_definition/layers/parametricfunction_module.rst b/docs/_autodoc/model_definition/layers/parametricfunction_module.rst index c518882b..2f2084e3 100644 --- a/docs/_autodoc/model_definition/layers/parametricfunction_module.rst +++ b/docs/_autodoc/model_definition/layers/parametricfunction_module.rst @@ -11,4 +11,4 @@ Parametric Function module :undoc-members: :no-inherited-members: -For more examples of how to use the parametric function module, please refer to the :doc:`Parametric Function tutorial <../../tutorials/examples/parametric_functions>`. \ No newline at end of file +For more examples of how to use the parametric function module, please refer to the :doc:`Parametric Function tutorial <../../tutorials/examples/parametric_functions>`. diff --git a/docs/_autodoc/model_definition/layers/part_module.rst b/docs/_autodoc/model_definition/layers/part_module.rst index d8e611e6..23cc18a8 100644 --- a/docs/_autodoc/model_definition/layers/part_module.rst +++ b/docs/_autodoc/model_definition/layers/part_module.rst @@ -27,4 +27,4 @@ Part module :undoc-members: :no-inherited-members: -For more examples and tutorials, see :doc:`Partitioning tutorial <../../tutorials/examples/partitioning>`. \ No newline at end of file +For more examples and tutorials, see :doc:`Partitioning tutorial <../../tutorials/examples/partitioning>`. diff --git a/docs/_autodoc/model_definition/layers/trigonometric_module.rst b/docs/_autodoc/model_definition/layers/trigonometric_module.rst index c583acdf..2a2a4ca8 100644 --- a/docs/_autodoc/model_definition/layers/trigonometric_module.rst +++ b/docs/_autodoc/model_definition/layers/trigonometric_module.rst @@ -29,4 +29,4 @@ Trigonometric module .. autoclass:: nnodely.layers.trigonometric.Sech :undoc-members: - :no-inherited-members: \ No newline at end of file + :no-inherited-members: diff --git a/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst b/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst index ac01447b..ea2bedff 100644 --- a/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst +++ b/docs/_autodoc/model_definition/msnn_ins_out_param/parameter_module.rst @@ -33,4 +33,4 @@ In addition to standard random initialization, **nnodely** provides structured i :undoc-members: :no-inherited-members: -For more examples of how to use the parameter module, please refer to the :doc:`relative tutorial <../../tutorials/examples/parameter>`. \ No newline at end of file +For more examples of how to use the parameter module, please refer to the :doc:`relative tutorial <../../tutorials/examples/parameter>`. diff --git a/docs/_autodoc/training/earlystopping_module.rst b/docs/_autodoc/training/earlystopping_module.rst index 24dd1e39..47929be8 100644 --- a/docs/_autodoc/training/earlystopping_module.rst +++ b/docs/_autodoc/training/earlystopping_module.rst @@ -8,4 +8,4 @@ Early Stopping module .. autofunction:: nnodely.support.earlystopping.early_stop_patience .. autofunction:: nnodely.support.earlystopping.select_best_model .. autofunction:: nnodely.support.earlystopping.mean_stopping - .. autofunction:: nnodely.support.earlystopping.standard_early_stopping \ No newline at end of file + .. autofunction:: nnodely.support.earlystopping.standard_early_stopping diff --git a/docs/_autodoc/training/optimizer_module.rst b/docs/_autodoc/training/optimizer_module.rst index cce81064..064efc5d 100644 --- a/docs/_autodoc/training/optimizer_module.rst +++ b/docs/_autodoc/training/optimizer_module.rst @@ -13,4 +13,4 @@ Optimizer module .. autoclass:: nnodely.basic.optimizer.Adam :undoc-members: :inherited-members: - :exclude-members: add_option_to_params, replace_key_with_params, get_torch_optimizer \ No newline at end of file + :exclude-members: add_option_to_params, replace_key_with_params, get_torch_optimizer diff --git a/docs/_autodoc/training/trainer_module.rst b/docs/_autodoc/training/trainer_module.rst index b28779f8..ff1041a0 100644 --- a/docs/_autodoc/training/trainer_module.rst +++ b/docs/_autodoc/training/trainer_module.rst @@ -11,7 +11,7 @@ This modality is suitable for architectures without internal feedback, where predictions depend only on the provided inputs. .. rubric:: Recurrent Training - + Targets architectures with closed-loop dependencies, such as feedback connections or coupled multi-model structures. In this modality, training is performed over a finite prediction horizon. The model is rolled out forward in time, and parameter updates are applied only after completing the full rollout using :func:`trainModel `. Early stopping strategies from the :doc:`Early Stopping module ` can be applied to control convergence in long-horizon training. @@ -25,4 +25,4 @@ Early stopping strategies from the :doc:`Early Stopping module `. \ No newline at end of file +For more example please refer to the :doc:`training tutorial <../tutorials/examples/training>`. diff --git a/docs/_autodoc/tutorials/examples/data/example.csv b/docs/_autodoc/tutorials/examples/data/example.csv index 6e0457f6..6dd01025 100644 --- a/docs/_autodoc/tutorials/examples/data/example.csv +++ b/docs/_autodoc/tutorials/examples/data/example.csv @@ -29,4 +29,4 @@ time,x,x_s,F 27,28,29,30 28,29,30,31 29,30,31,32 -30,31,32,33 \ No newline at end of file +30,31,32,33 diff --git a/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json b/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json index e1c9212d..20b0e9af 100644 --- a/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json +++ b/docs/_autodoc/tutorials/examples/dataset/other_data/weights_keras.json @@ -1 +1 @@ -{"weights_Phi_layer": [[0.012685388824826905]], "weights_y_layer": [[-0.1854345338982826, 0.8310009650237228]], "weights_lateral_force_Fy_layer": [[-0.19011648493285369, -0.08908118349376112, 1.336387850702053, 1.2920572574778424, -0.09438500021422562, 6.008296562778322e-05, -0.2602301908451158, -0.14530553136816854, 1.3363877377724573, 1.4067596762669665, 0.7879800384173099, 0.3761416745650699, -6.00737156551326e-05], [-0.36005925993705845, 0.31715296267094517, -0.6690789258949266, -0.2554781443995069, 0.31625396631947944, -0.3083455766488089, 0.15216951805631235, -0.4206261340059274, -0.6689552394638391, 0.22550729485249751, 0.1626431985847517, 1.033870940161089, 0.3083458634524217], [0.18802284870005584, -0.31661460582086853, 0.8534668520952942, -0.9812678363224603, 0.24311718237352536, -0.24094412228551954, 0.3072880036082372, 0.17855881988596073, 0.8535322187062506, 0.14222280861767367, 0.3735082621873329, 0.4281402634564149, 0.24090550284449572], [0.7059571634884887, 0.831036154928734, -1.5520232570684374, -1.1949558744521236, 1.0878698744282658, -1.418785910127414, 1.1105076474360718, 0.7796338840890075, -1.5520090193775626, 1.4340218664669733, 1.4600207192669248, 2.0975927853738883, -1.4191253818247085], [1.5256341940706433, 0.9566175382540345, 1.6143430229487328, -1.5732299938434673, 1.4456796215396086, 1.2677360377185034, -1.1754116849251022, 0.9002310736225679, -1.614095846537853, -1.5902608121180453, -1.6560649232874438, 1.4212434156973413, -1.2677278329655355], [-0.2943392917601912, -0.20962344102468206, -0.39777282371248085, -0.24818156146119716, 0.25709724945468077, 0.7976733265366256, -0.47110536863291785, -0.4547936310130406, 0.3977737195448417, 0.05919297447455203, 0.5504838589795269, 0.48094966877918677, 0.7976733529905506], [0.2882358412363946, -0.1769694998492831, 0.31494048781607825, 0.4120887280056081, 0.02440918889325766, 0.11130295760561043, 0.844947413739056, -0.27388447106061653, -0.31488950511700137, 0.44670920167256334, 0.27619535288612646, 0.031508029779568555, 0.1109016477647062], [-0.17117628439315022, 0.8712691462442139, 1.154838105565089, -0.5919095645359649, -0.6519344279453698, 0.22211601204221534, 0.08262748151809965, 0.3598295238234417, -1.1550202286608042, -0.9517794739723651, 0.163217589777445, -0.12628295768104714, 0.22201749256009923], [1.4506073395342782, -0.9325832775205263, -2.1979358075291233, 2.8070465077590003, -1.1346862854456579, 1.3769406818632672, 2.475248535808836, 2.0976190504310988, 2.19821296078414, 2.0255837749393124, 3.083828011008372, 2.50001192339801, -1.3769242385855402], [1.9357170786694566, -1.0112798443932023, 2.000754627001145, 2.4634621666033816, -1.694712934160906, -1.3599774709358892, -1.4821823368748877, 0.1430931122186942, 2.000786017219198, -1.9266467245466596, -2.175009991370894, 0.7244597098063941, -1.3599925425879837], [-0.999649832233769, 1.1789553679187095, -1.0439958121675497, 0.6859639814252193, -1.207373719896859, 1.4355651469453192, -1.0252723585630958, -1.0277168693514158, -1.0439392352227312, 1.1185833489292838, 1.2486301466781338, 1.2271341647965177, -1.4355998395140415], [0.5472048599834014, -0.2964604823481814, -0.755279693792939, 0.496730028445273, 0.03189984598977768, -0.011526076792924516, 0.13793532007135406, -0.3922534852030546, 0.7552757947568585, 0.541882492971732, 1.3892197075267785, 0.029209525577498854, 0.011427767016954781], [-0.33289867240184234, -0.8250106024590829, 1.3307613450025935, -0.17274867992122075, 1.167809311448035, 0.11023086130586764, 1.387883975990627, 1.6525955037269457, 1.330712816962725, -1.1008274196962873, 0.12774339481905536, 1.6778832697823363, -0.1102327756404915], [1.3799212851947305, -0.11798177584035434, -1.2706691824287746, 1.3389620861547382, -0.07167511836865471, 0.574528497078781, 1.053126427807511, 0.525765012439538, 1.2706790577938978, 1.323968682481893, 1.395683119845944, 0.9919769987157268, -0.5745468641121597], [1.470852592611058, -0.2753007632206784, 1.3267311312902044, 1.4002748745291318, -0.32148568082965384, -0.2445208276668468, -1.6411299244249362, 1.665228483180153, 1.326825077761312, -1.3355204169065926, -1.6876677699164133, 1.709701263209955, -0.24440222805773965], [0.061201334164515474, -0.2613894929273045, 0.17194839021789546, 0.357392249323869, -0.1969047844516283, -0.40138265636368453, 0.054160782803064425, 0.04499566865007523, 0.17179140127081347, 0.28445081911658165, 0.26876231918645477, 0.2834999443210322, 0.40140992216043186], [-0.7383867680261166, 0.28140178858383064, -1.0607846672606085, 1.1293524025011914, -0.38584968559452604, 0.6814052979441493, -0.8472337078996004, -0.8521757690270018, -1.0603091109428728, 1.1219117352503318, 1.161103667708072, 1.1370906079460585, -0.6814279198878799], [-0.5754205744199418, 0.7515354938234574, -0.7427595435202464, 0.08173230816182779, -0.5818699059495528, -0.8576746961203272, 0.9107580156302137, -0.4636739933720752, -0.7435719493943217, 0.9818698110846921, -1.39877838441067, 0.10852623836507357, -0.8577092093212803], [1.1803303176988844, -0.8602509152319485, 2.08823235980041, 0.5640822448256174, -1.1113029573112245, -1.783096430959948, 0.38542138435556245, 0.8968062265303769, -2.0880454626597094, 1.5287272459021344, 0.6517334709095841, 1.3030554584249463, 1.7831668896262234], [0.9965967442529087, -0.9555740352345297, 0.9315736169296287, 0.952402105850403, -1.0038521933218492, -1.0384630844945648, -1.2568415867731368, 1.359454269250626, 0.9315953305928442, -1.0105122965663116, -1.4543433160625407, 1.5251210878543582, -1.0382923330987461], [-0.0003252008154921961, -0.0007757975430400393, 0.00027610923975581776, -0.000492314483248814, 0.00037227465197929875, 0.00012994623625124898, 0.00014116358428038455, 0.0005370449387972119, -4.011652901164747e-05, 0.0005026686325048509, 0.00042415295448027955, 0.0006194169481779365, -0.00014805157424955308], [0.25408255894477966, 0.4008486804612262, -0.9585384454176571, 0.07462983204257474, 0.20304621135962272, -0.7509778599485446, 0.1957008083070563, 0.21407418595698074, -0.9583716769231316, 1.1536953456759063, 0.5223846802781359, 0.4893487538764833, -0.7512212443147893], [0.5872378239364147, -1.028692570295371, -1.1345470507881132, 0.9921306627222031, -1.431783879088534, -0.5131283754255125, 0.031786765651124815, -0.24578096239017852, 1.1346001691321415, 1.3546533350024696, -0.04328366980497708, -0.9381339085116982, 0.5132369498728799], [-0.14295606844165948, -0.01877309912889, 0.5876429218771214, 0.48027938625599703, -0.3365035305830312, -0.9956153538141037, -0.4515785924356622, 0.5789253255140729, 0.5882234039953237, -0.05064944535197737, -0.592386760754478, 0.6410803440586128, -0.9955860588207823], [0.000787086437955529, -8.908694807142095e-06, -0.0006681074216258374, 0.0002151332500077205, -0.0005916405206218054, 0.0009121619779000141, 0.0002014354532319387, -3.0046087688955496e-05, 0.00039327018433161, 4.262729131982468e-05, 0.0016545339586900472, 0.0007591541430962376, 0.00018828612339093865]], "weights_rolling_resis_Fr_layer": [[0.009512348136131527, 0.015974305622861035, 0.012354668768985456, -0.02350446610289629, -0.021171149395172454], [0.04485560728012199, -0.005721244207683364, 0.009212919587585475, 0.056730803145390786, 0.08301125650143018], [-0.027671052741331956, 0.0005604850752721769, 7.768705209179708e-05, 0.019376156352589505, -0.0036748612480807553], [0.13136716489951636, -0.00505070341472879, 0.05868154169374028, -0.09000068012576608, -0.015475440377495217], [0.13123488929110713, 0.04738146152770957, -0.09140000602220295, -0.06928203303665631, -0.02297547485706099], [-0.1292347084279552, -0.09376875839320302, 0.09143687406062925, -0.042666819353032216, -0.08618952991465577], [0.03811870391839511, 0.04299551986113498, -0.015645139143944567, -0.005474475667232196, -0.10268401640164736], [0.010408554061713201, -0.01680342808995886, -0.026197713491986078, 0.007333479240267465, 0.024039377702831342], [0.03636583768453933, 0.019818467772646533, -0.0037000940948564843, 0.03470735249075659, 0.035761192938559644], [0.10914620691295635, 0.0681068573436399, -0.06704391198934014, -0.0058835924268194634, -0.0900478676267965], [-0.18273868602774165, -0.018173976405129886, -0.033806530873410706, -0.014822716241073402, 0.0011868321653409092], [0.08779567256832134, -0.0007543581153410836, -0.026161440641287743, -0.0008218318939369428, -0.05193780327764801], [0.0794895474049251, 0.013610288551551318, 0.00965207990980787, -0.09109264333425719, 0.07350332727372452], [0.11045172299216201, 0.10204791507802281, -0.01748638178864372, 0.0892068942348556, -0.11044337166665709], [0.2592527992617235, -0.059380721328938506, -0.36878016964982613, -0.08994677022091373, 0.536723089245246], [0.1481965010752139, 0.009199935103273497, 0.006816411716118778, 0.07245631603959318, -0.016350624468557635], [0.09861027451021276, 0.003809132398771833, 0.05702825577816732, 0.004509477844508769, -0.04145490208647068], [0.10552877411008127, 0.04774693387230193, -0.019785790434331273, -0.033777708832308954, 0.08008065558794654], [-0.009534395253850227, 0.2371943342468657, 0.05340427321482067, 0.054605243213691625, 0.22608070427496613], [0.5809629742447168, 0.03565806917804284, -0.2612010705948309, 0.28708726638020116, 0.062365224169860664], [9.396158363284712e-05, 0.00010777149932499175, 9.883206328784566e-05, 9.817530963461989e-05, 9.044142390143377e-05], [0.15581565021743365, 0.13103577835449093, -0.07876624531554803, -0.210951410006127, -0.0012785118547123297], [0.13182698748047716, 0.1762004260101664, 0.09210441306630922, 0.2060755656713025, 0.013773614352975106], [0.14984011485073506, 0.13626731231521377, -0.017230767623647506, 0.10871804112668679, 0.06346689814508452], [0.0001097756900344383, 9.740100575111105e-05, 0.00011760808818914617, 0.00010664388733340848, 8.993184159731601e-05]], "weights_FC1_layer_steer": [[0.38688127715207987, -0.3871187885731928, -0.3870036745961848, 0.38676281255272826, 0.38703086352404237, -0.3868947521837845, 0.3871124432809551, -0.3869712978151479, 0.38692919331713455, 0.38688800029402004]], "biases_FC1_layer_steer": [0.6827290688825456, -0.6837825068595632, -0.6832820008581203, 0.6822021789791652, 0.6833963624234348, -0.68279452477573, 0.6837620722967017, -0.6831272126615047, 0.682948780966369, 0.6827714454347539], "weights_FC2_layer_steer": [[2.0884597157646563], [-2.087764579846399], [-2.085872928095188], [2.089101087015271], [2.0872048698362446], [-2.087166620124699], [2.0860502186132233], [-2.0884596607292907], [2.0867457700638337], [2.085633475017681]], "biases_FC2_layer_steer": [-1.361573075727897], "lambda_forget_weight": [[0.995]], "max_Tyf_engine": 399.59648198652155, "max_Tyf_brake": 2317.418462624429, "max_Tyr_brake": 941.7535062079378, "max_vx": 22.441584159075212, "min_vx": 4.0, "max_ax": 2.870634750517449, "min_ax": -1.8177452194551227, "max_ay": 4.319774302260081, "max_steer": 7.41147685692138} \ No newline at end of file +{"weights_Phi_layer": [[0.012685388824826905]], "weights_y_layer": [[-0.1854345338982826, 0.8310009650237228]], "weights_lateral_force_Fy_layer": [[-0.19011648493285369, -0.08908118349376112, 1.336387850702053, 1.2920572574778424, -0.09438500021422562, 6.008296562778322e-05, -0.2602301908451158, -0.14530553136816854, 1.3363877377724573, 1.4067596762669665, 0.7879800384173099, 0.3761416745650699, -6.00737156551326e-05], [-0.36005925993705845, 0.31715296267094517, -0.6690789258949266, -0.2554781443995069, 0.31625396631947944, -0.3083455766488089, 0.15216951805631235, -0.4206261340059274, -0.6689552394638391, 0.22550729485249751, 0.1626431985847517, 1.033870940161089, 0.3083458634524217], [0.18802284870005584, -0.31661460582086853, 0.8534668520952942, -0.9812678363224603, 0.24311718237352536, -0.24094412228551954, 0.3072880036082372, 0.17855881988596073, 0.8535322187062506, 0.14222280861767367, 0.3735082621873329, 0.4281402634564149, 0.24090550284449572], [0.7059571634884887, 0.831036154928734, -1.5520232570684374, -1.1949558744521236, 1.0878698744282658, -1.418785910127414, 1.1105076474360718, 0.7796338840890075, -1.5520090193775626, 1.4340218664669733, 1.4600207192669248, 2.0975927853738883, -1.4191253818247085], [1.5256341940706433, 0.9566175382540345, 1.6143430229487328, -1.5732299938434673, 1.4456796215396086, 1.2677360377185034, -1.1754116849251022, 0.9002310736225679, -1.614095846537853, -1.5902608121180453, -1.6560649232874438, 1.4212434156973413, -1.2677278329655355], [-0.2943392917601912, -0.20962344102468206, -0.39777282371248085, -0.24818156146119716, 0.25709724945468077, 0.7976733265366256, -0.47110536863291785, -0.4547936310130406, 0.3977737195448417, 0.05919297447455203, 0.5504838589795269, 0.48094966877918677, 0.7976733529905506], [0.2882358412363946, -0.1769694998492831, 0.31494048781607825, 0.4120887280056081, 0.02440918889325766, 0.11130295760561043, 0.844947413739056, -0.27388447106061653, -0.31488950511700137, 0.44670920167256334, 0.27619535288612646, 0.031508029779568555, 0.1109016477647062], [-0.17117628439315022, 0.8712691462442139, 1.154838105565089, -0.5919095645359649, -0.6519344279453698, 0.22211601204221534, 0.08262748151809965, 0.3598295238234417, -1.1550202286608042, -0.9517794739723651, 0.163217589777445, -0.12628295768104714, 0.22201749256009923], [1.4506073395342782, -0.9325832775205263, -2.1979358075291233, 2.8070465077590003, -1.1346862854456579, 1.3769406818632672, 2.475248535808836, 2.0976190504310988, 2.19821296078414, 2.0255837749393124, 3.083828011008372, 2.50001192339801, -1.3769242385855402], [1.9357170786694566, -1.0112798443932023, 2.000754627001145, 2.4634621666033816, -1.694712934160906, -1.3599774709358892, -1.4821823368748877, 0.1430931122186942, 2.000786017219198, -1.9266467245466596, -2.175009991370894, 0.7244597098063941, -1.3599925425879837], [-0.999649832233769, 1.1789553679187095, -1.0439958121675497, 0.6859639814252193, -1.207373719896859, 1.4355651469453192, -1.0252723585630958, -1.0277168693514158, -1.0439392352227312, 1.1185833489292838, 1.2486301466781338, 1.2271341647965177, -1.4355998395140415], [0.5472048599834014, -0.2964604823481814, -0.755279693792939, 0.496730028445273, 0.03189984598977768, -0.011526076792924516, 0.13793532007135406, -0.3922534852030546, 0.7552757947568585, 0.541882492971732, 1.3892197075267785, 0.029209525577498854, 0.011427767016954781], [-0.33289867240184234, -0.8250106024590829, 1.3307613450025935, -0.17274867992122075, 1.167809311448035, 0.11023086130586764, 1.387883975990627, 1.6525955037269457, 1.330712816962725, -1.1008274196962873, 0.12774339481905536, 1.6778832697823363, -0.1102327756404915], [1.3799212851947305, -0.11798177584035434, -1.2706691824287746, 1.3389620861547382, -0.07167511836865471, 0.574528497078781, 1.053126427807511, 0.525765012439538, 1.2706790577938978, 1.323968682481893, 1.395683119845944, 0.9919769987157268, -0.5745468641121597], [1.470852592611058, -0.2753007632206784, 1.3267311312902044, 1.4002748745291318, -0.32148568082965384, -0.2445208276668468, -1.6411299244249362, 1.665228483180153, 1.326825077761312, -1.3355204169065926, -1.6876677699164133, 1.709701263209955, -0.24440222805773965], [0.061201334164515474, -0.2613894929273045, 0.17194839021789546, 0.357392249323869, -0.1969047844516283, -0.40138265636368453, 0.054160782803064425, 0.04499566865007523, 0.17179140127081347, 0.28445081911658165, 0.26876231918645477, 0.2834999443210322, 0.40140992216043186], [-0.7383867680261166, 0.28140178858383064, -1.0607846672606085, 1.1293524025011914, -0.38584968559452604, 0.6814052979441493, -0.8472337078996004, -0.8521757690270018, -1.0603091109428728, 1.1219117352503318, 1.161103667708072, 1.1370906079460585, -0.6814279198878799], [-0.5754205744199418, 0.7515354938234574, -0.7427595435202464, 0.08173230816182779, -0.5818699059495528, -0.8576746961203272, 0.9107580156302137, -0.4636739933720752, -0.7435719493943217, 0.9818698110846921, -1.39877838441067, 0.10852623836507357, -0.8577092093212803], [1.1803303176988844, -0.8602509152319485, 2.08823235980041, 0.5640822448256174, -1.1113029573112245, -1.783096430959948, 0.38542138435556245, 0.8968062265303769, -2.0880454626597094, 1.5287272459021344, 0.6517334709095841, 1.3030554584249463, 1.7831668896262234], [0.9965967442529087, -0.9555740352345297, 0.9315736169296287, 0.952402105850403, -1.0038521933218492, -1.0384630844945648, -1.2568415867731368, 1.359454269250626, 0.9315953305928442, -1.0105122965663116, -1.4543433160625407, 1.5251210878543582, -1.0382923330987461], [-0.0003252008154921961, -0.0007757975430400393, 0.00027610923975581776, -0.000492314483248814, 0.00037227465197929875, 0.00012994623625124898, 0.00014116358428038455, 0.0005370449387972119, -4.011652901164747e-05, 0.0005026686325048509, 0.00042415295448027955, 0.0006194169481779365, -0.00014805157424955308], [0.25408255894477966, 0.4008486804612262, -0.9585384454176571, 0.07462983204257474, 0.20304621135962272, -0.7509778599485446, 0.1957008083070563, 0.21407418595698074, -0.9583716769231316, 1.1536953456759063, 0.5223846802781359, 0.4893487538764833, -0.7512212443147893], [0.5872378239364147, -1.028692570295371, -1.1345470507881132, 0.9921306627222031, -1.431783879088534, -0.5131283754255125, 0.031786765651124815, -0.24578096239017852, 1.1346001691321415, 1.3546533350024696, -0.04328366980497708, -0.9381339085116982, 0.5132369498728799], [-0.14295606844165948, -0.01877309912889, 0.5876429218771214, 0.48027938625599703, -0.3365035305830312, -0.9956153538141037, -0.4515785924356622, 0.5789253255140729, 0.5882234039953237, -0.05064944535197737, -0.592386760754478, 0.6410803440586128, -0.9955860588207823], [0.000787086437955529, -8.908694807142095e-06, -0.0006681074216258374, 0.0002151332500077205, -0.0005916405206218054, 0.0009121619779000141, 0.0002014354532319387, -3.0046087688955496e-05, 0.00039327018433161, 4.262729131982468e-05, 0.0016545339586900472, 0.0007591541430962376, 0.00018828612339093865]], "weights_rolling_resis_Fr_layer": [[0.009512348136131527, 0.015974305622861035, 0.012354668768985456, -0.02350446610289629, -0.021171149395172454], [0.04485560728012199, -0.005721244207683364, 0.009212919587585475, 0.056730803145390786, 0.08301125650143018], [-0.027671052741331956, 0.0005604850752721769, 7.768705209179708e-05, 0.019376156352589505, -0.0036748612480807553], [0.13136716489951636, -0.00505070341472879, 0.05868154169374028, -0.09000068012576608, -0.015475440377495217], [0.13123488929110713, 0.04738146152770957, -0.09140000602220295, -0.06928203303665631, -0.02297547485706099], [-0.1292347084279552, -0.09376875839320302, 0.09143687406062925, -0.042666819353032216, -0.08618952991465577], [0.03811870391839511, 0.04299551986113498, -0.015645139143944567, -0.005474475667232196, -0.10268401640164736], [0.010408554061713201, -0.01680342808995886, -0.026197713491986078, 0.007333479240267465, 0.024039377702831342], [0.03636583768453933, 0.019818467772646533, -0.0037000940948564843, 0.03470735249075659, 0.035761192938559644], [0.10914620691295635, 0.0681068573436399, -0.06704391198934014, -0.0058835924268194634, -0.0900478676267965], [-0.18273868602774165, -0.018173976405129886, -0.033806530873410706, -0.014822716241073402, 0.0011868321653409092], [0.08779567256832134, -0.0007543581153410836, -0.026161440641287743, -0.0008218318939369428, -0.05193780327764801], [0.0794895474049251, 0.013610288551551318, 0.00965207990980787, -0.09109264333425719, 0.07350332727372452], [0.11045172299216201, 0.10204791507802281, -0.01748638178864372, 0.0892068942348556, -0.11044337166665709], [0.2592527992617235, -0.059380721328938506, -0.36878016964982613, -0.08994677022091373, 0.536723089245246], [0.1481965010752139, 0.009199935103273497, 0.006816411716118778, 0.07245631603959318, -0.016350624468557635], [0.09861027451021276, 0.003809132398771833, 0.05702825577816732, 0.004509477844508769, -0.04145490208647068], [0.10552877411008127, 0.04774693387230193, -0.019785790434331273, -0.033777708832308954, 0.08008065558794654], [-0.009534395253850227, 0.2371943342468657, 0.05340427321482067, 0.054605243213691625, 0.22608070427496613], [0.5809629742447168, 0.03565806917804284, -0.2612010705948309, 0.28708726638020116, 0.062365224169860664], [9.396158363284712e-05, 0.00010777149932499175, 9.883206328784566e-05, 9.817530963461989e-05, 9.044142390143377e-05], [0.15581565021743365, 0.13103577835449093, -0.07876624531554803, -0.210951410006127, -0.0012785118547123297], [0.13182698748047716, 0.1762004260101664, 0.09210441306630922, 0.2060755656713025, 0.013773614352975106], [0.14984011485073506, 0.13626731231521377, -0.017230767623647506, 0.10871804112668679, 0.06346689814508452], [0.0001097756900344383, 9.740100575111105e-05, 0.00011760808818914617, 0.00010664388733340848, 8.993184159731601e-05]], "weights_FC1_layer_steer": [[0.38688127715207987, -0.3871187885731928, -0.3870036745961848, 0.38676281255272826, 0.38703086352404237, -0.3868947521837845, 0.3871124432809551, -0.3869712978151479, 0.38692919331713455, 0.38688800029402004]], "biases_FC1_layer_steer": [0.6827290688825456, -0.6837825068595632, -0.6832820008581203, 0.6822021789791652, 0.6833963624234348, -0.68279452477573, 0.6837620722967017, -0.6831272126615047, 0.682948780966369, 0.6827714454347539], "weights_FC2_layer_steer": [[2.0884597157646563], [-2.087764579846399], [-2.085872928095188], [2.089101087015271], [2.0872048698362446], [-2.087166620124699], [2.0860502186132233], [-2.0884596607292907], [2.0867457700638337], [2.085633475017681]], "biases_FC2_layer_steer": [-1.361573075727897], "lambda_forget_weight": [[0.995]], "max_Tyf_engine": 399.59648198652155, "max_Tyf_brake": 2317.418462624429, "max_Tyr_brake": 941.7535062079378, "max_vx": 22.441584159075212, "min_vx": 4.0, "max_ax": 2.870634750517449, "min_ax": -1.8177452194551227, "max_ay": 4.319774302260081, "max_steer": 7.41147685692138} diff --git a/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv b/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv index 5625a587..3d80578b 100644 --- a/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv +++ b/docs/_autodoc/tutorials/examples/vehicle_data/vehicle.csv @@ -98,4 +98,4 @@ 13.473333333333336,12.779200000000001,0.,5,-0.7460835566787409,-0.6602761975976819,-0.5746327099489577,-0.48922828574131927,-0.4061226600199461,-0.3259584416340431,-0.2506976788511679,-0.18109245144700026,-0.11566989947226602,-0.05551166846061051,0.,0.04959411345004128,0.09326429982348827,0.12985450593151882,0.16023045483507303,0.1832476556463689,0.20626485645772163,0.22928205726901751,0.2522992580803134,0.2753164588916661,0.298333659702962,0.09911902783658734 13.474444444444448,13.0928,0.,5,-0.7485232229184362,-0.6627158638373771,-0.5770452306900893,-0.4916408064824509,-0.4081821510060877,-0.32801793262018464,-0.25204581648097246,-0.18244058907686167,-0.11638159820341798,-0.05622336719181931,0.,0.04959411345004128,0.09414036583672214,0.13073057194475268,0.162020926674586,0.1850381274858819,0.2080553282972346,0.2310725291085305,0.25408972991982637,0.2771069307311791,0.30012413154247497,0.11169376341230414 13.496111111111112,13.014400000000002,0.,5,-0.7509649513013414,-0.6651575922202255,-0.5794597906294712,-0.4940553664218328,-0.41024338278987216,-0.3300791644039691,-0.2533950936325482,-0.1837898662284374,-0.11709389850244634,-0.05693566749079082,0.,0.04959411345004128,0.09501717235025353,0.13160737845828407,0.16381291192163872,0.1868301127329346,0.20984731354423047,0.2328645143555832,0.2558817151668791,0.2788989159782318,0.3019161167895277,0.1517397792930878 -13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794 \ No newline at end of file +13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794 diff --git a/docs/_autodoc/validation/validator_module.rst b/docs/_autodoc/validation/validator_module.rst index eb9284bb..0382b11a 100644 --- a/docs/_autodoc/validation/validator_module.rst +++ b/docs/_autodoc/validation/validator_module.rst @@ -5,4 +5,4 @@ Validation Module :undoc-members: :no-inherited-members: -.. autofunction:: nnodely.operators.validator.Validator.analyzeModel \ No newline at end of file +.. autofunction:: nnodely.operators.validator.Validator.analyzeModel diff --git a/docs/examples_basics/compser_module_ex/addClosedLoop.rst b/docs/examples_basics/compser_module_ex/addClosedLoop.rst index 17ed8fdf..a00e934d 100644 --- a/docs/examples_basics/compser_module_ex/addClosedLoop.rst +++ b/docs/examples_basics/compser_module_ex/addClosedLoop.rst @@ -5,4 +5,3 @@ y = Input('y') relation = Fir(x.last()) model.addClosedLoop(relation, y) - \ No newline at end of file diff --git a/docs/examples_basics/compser_module_ex/addConnect.rst b/docs/examples_basics/compser_module_ex/addConnect.rst index f1a96826..89e0fab6 100644 --- a/docs/examples_basics/compser_module_ex/addConnect.rst +++ b/docs/examples_basics/compser_module_ex/addConnect.rst @@ -4,4 +4,4 @@ x = Input('x') y = Input('y') relation = Fir(x.last()) - model.addConnect(relation, y) \ No newline at end of file + model.addConnect(relation, y) diff --git a/docs/examples_basics/compser_module_ex/addModel.rst b/docs/examples_basics/compser_module_ex/addModel.rst index ad9f410f..5cb910f5 100644 --- a/docs/examples_basics/compser_module_ex/addModel.rst +++ b/docs/examples_basics/compser_module_ex/addModel.rst @@ -3,4 +3,4 @@ model = Modely() x = Input('x') out = Output('out', Fir(x.last())) - model.addModel('example_model', [out]) \ No newline at end of file + model.addModel('example_model', [out]) diff --git a/docs/examples_basics/compser_module_ex/neuralizeModel.rst b/docs/examples_basics/compser_module_ex/neuralizeModel.rst index a0f66cc0..6795679d 100644 --- a/docs/examples_basics/compser_module_ex/neuralizeModel.rst +++ b/docs/examples_basics/compser_module_ex/neuralizeModel.rst @@ -1,4 +1,4 @@ .. code-block:: python - + model = Modely(name='example_model') - model.neuralizeModel(sample_time=0.1, clear_model=True) \ No newline at end of file + model.neuralizeModel(sample_time=0.1, clear_model=True) diff --git a/docs/examples_basics/compser_module_ex/removeConnection.rst b/docs/examples_basics/compser_module_ex/removeConnection.rst index ca03e376..8d59d09b 100644 --- a/docs/examples_basics/compser_module_ex/removeConnection.rst +++ b/docs/examples_basics/compser_module_ex/removeConnection.rst @@ -5,4 +5,4 @@ y = Input('y') relation = Fir(x.last()) model.addConnect(relation, y) - model.removeConnection(y) \ No newline at end of file + model.removeConnection(y) diff --git a/docs/examples_basics/compser_module_ex/removeModel.rst b/docs/examples_basics/compser_module_ex/removeModel.rst index 11e4652a..c9d853a2 100644 --- a/docs/examples_basics/compser_module_ex/removeModel.rst +++ b/docs/examples_basics/compser_module_ex/removeModel.rst @@ -1,3 +1,3 @@ .. code-block:: python - model.removeModel(['sub_model1', 'sub_model2']) \ No newline at end of file + model.removeModel(['sub_model1', 'sub_model2']) diff --git a/docs/examples_basics/export_module_ex/exportONNX.rst b/docs/examples_basics/export_module_ex/exportONNX.rst index ba84c3c2..120879aa 100644 --- a/docs/examples_basics/export_module_ex/exportONNX.rst +++ b/docs/examples_basics/export_module_ex/exportONNX.rst @@ -6,4 +6,4 @@ model = Modely() model.neuralizeModel() - model.exportONNX(inputs_order=['input1', 'input2'], outputs_order=['output1'], name='example_model', model_folder='path/to/export') \ No newline at end of file + model.exportONNX(inputs_order=['input1', 'input2'], outputs_order=['output1'], name='example_model', model_folder='path/to/export') diff --git a/docs/examples_basics/export_module_ex/exportPythonModel.rst b/docs/examples_basics/export_module_ex/exportPythonModel.rst index 172f2258..7ad98f22 100644 --- a/docs/examples_basics/export_module_ex/exportPythonModel.rst +++ b/docs/examples_basics/export_module_ex/exportPythonModel.rst @@ -2,4 +2,4 @@ model = Modely(name='example_model') model.neuralizeModel() - model.exportPythonModel(name='example_model', model_folder='folder/') \ No newline at end of file + model.exportPythonModel(name='example_model', model_folder='folder/') diff --git a/docs/examples_basics/export_module_ex/exportReport.rst b/docs/examples_basics/export_module_ex/exportReport.rst index 9e5483ce..79de92ac 100644 --- a/docs/examples_basics/export_module_ex/exportReport.rst +++ b/docs/examples_basics/export_module_ex/exportReport.rst @@ -3,4 +3,4 @@ model = Modely() model.neuralizeModel() model.trainModel(train_dataset='train_dataset', validation_dataset='val_dataset', num_of_epochs=10) - model.exportReport(name='example_model', model_folder='path/to/export') \ No newline at end of file + model.exportReport(name='example_model', model_folder='path/to/export') diff --git a/docs/examples_basics/export_module_ex/importPythonModel.rst b/docs/examples_basics/export_module_ex/importPythonModel.rst index a85613f4..21d8ab85 100644 --- a/docs/examples_basics/export_module_ex/importPythonModel.rst +++ b/docs/examples_basics/export_module_ex/importPythonModel.rst @@ -1,4 +1,4 @@ .. code-block:: python model = Modely() - model.importPythonModel(name='example_model', model_folder='path/to/import') \ No newline at end of file + model.importPythonModel(name='example_model', model_folder='path/to/import') diff --git a/docs/examples_basics/export_module_ex/loadModel.rst b/docs/examples_basics/export_module_ex/loadModel.rst index dfcddf2f..2b70c2db 100644 --- a/docs/examples_basics/export_module_ex/loadModel.rst +++ b/docs/examples_basics/export_module_ex/loadModel.rst @@ -1,4 +1,4 @@ .. code-block:: python model = Modely() - model.loadModel(name='example_model', model_folder='path/to/load') \ No newline at end of file + model.loadModel(name='example_model', model_folder='path/to/load') diff --git a/docs/examples_basics/export_module_ex/loadTorchModel.rst b/docs/examples_basics/export_module_ex/loadTorchModel.rst index 5f6fff2d..bcbf93f1 100644 --- a/docs/examples_basics/export_module_ex/loadTorchModel.rst +++ b/docs/examples_basics/export_module_ex/loadTorchModel.rst @@ -2,4 +2,4 @@ model = Modely() model.neuralizeModel() - model.loadTorchModel(name='example_model', model_folder='path/to/load') \ No newline at end of file + model.loadTorchModel(name='example_model', model_folder='path/to/load') diff --git a/docs/examples_basics/export_module_ex/onnxInference.rst b/docs/examples_basics/export_module_ex/onnxInference.rst index 3f9b4091..c627a2db 100644 --- a/docs/examples_basics/export_module_ex/onnxInference.rst +++ b/docs/examples_basics/export_module_ex/onnxInference.rst @@ -33,4 +33,4 @@ Example - Recurrent: 'y': np.ones(shape=(1, 1, 1)).astype(np.float32) } - predictions = Modely().onnxInference(dummy_input, model_folder) \ No newline at end of file + predictions = Modely().onnxInference(dummy_input, model_folder) diff --git a/docs/examples_basics/export_module_ex/saveModel.rst b/docs/examples_basics/export_module_ex/saveModel.rst index bde2f989..74ab3185 100644 --- a/docs/examples_basics/export_module_ex/saveModel.rst +++ b/docs/examples_basics/export_module_ex/saveModel.rst @@ -2,4 +2,4 @@ model = Modely() model.neuralizeModel() - model.saveModel(name='example_model', model_folder='folder/') \ No newline at end of file + model.saveModel(name='example_model', model_folder='folder/') diff --git a/docs/examples_basics/export_module_ex/saveTorchModel.rst b/docs/examples_basics/export_module_ex/saveTorchModel.rst index 9f2017ef..29188790 100644 --- a/docs/examples_basics/export_module_ex/saveTorchModel.rst +++ b/docs/examples_basics/export_module_ex/saveTorchModel.rst @@ -2,4 +2,4 @@ model = Modely() model.neuralizeModel() - model.saveTorchModel(name='example_model', model_folder='path/to/save') \ No newline at end of file + model.saveTorchModel(name='example_model', model_folder='path/to/save') diff --git a/docs/examples_basics/inference_module_ex/inference.rst b/docs/examples_basics/inference_module_ex/inference.rst index c7cc4a89..81a34023 100644 --- a/docs/examples_basics/inference_module_ex/inference.rst +++ b/docs/examples_basics/inference_module_ex/inference.rst @@ -5,4 +5,4 @@ out = Output('out', Fir(x.last())) model.addModel('example_model', [out]) model.neuralizeModel() - predictions = model(inputs={'x': [1, 2, 3]}) \ No newline at end of file + predictions = model(inputs={'x': [1, 2, 3]}) diff --git a/docs/examples_basics/input_module_ex/z.rst b/docs/examples_basics/input_module_ex/z.rst index a3509cda..5da83cc6 100644 --- a/docs/examples_basics/input_module_ex/z.rst +++ b/docs/examples_basics/input_module_ex/z.rst @@ -8,4 +8,4 @@ where the time vector 0 represents the last passed instant. T.z(-1) # = 1 T.z(0) # = 0 # the last passed instant - T.z(2) # = -2 \ No newline at end of file + T.z(2) # = -2 diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst index 6ee67c07..eed00e15 100644 --- a/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst +++ b/docs/examples_basics/layer_module_ex/activation_module_ex/elu.rst @@ -1,3 +1,3 @@ .. code-block:: python - x = ELU(x) \ No newline at end of file + x = ELU(x) diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst index f384ed96..82597e0b 100644 --- a/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst +++ b/docs/examples_basics/layer_module_ex/activation_module_ex/identity.rst @@ -1,3 +1,3 @@ .. code-block:: python - x = Identity(x) \ No newline at end of file + x = Identity(x) diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst index 7c4d311f..e5ba89b8 100644 --- a/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst +++ b/docs/examples_basics/layer_module_ex/activation_module_ex/relu.rst @@ -1,3 +1,3 @@ .. code-block:: python - x = Relu(x) \ No newline at end of file + x = Relu(x) diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst index 6810a2fc..64c9e56e 100644 --- a/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst +++ b/docs/examples_basics/layer_module_ex/activation_module_ex/sigmoid.rst @@ -1,3 +1,3 @@ .. code-block:: python - x = Sigmoid(x) \ No newline at end of file + x = Sigmoid(x) diff --git a/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst b/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst index 1126dd3a..d3a14be8 100644 --- a/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst +++ b/docs/examples_basics/layer_module_ex/activation_module_ex/softmax.rst @@ -1,3 +1,3 @@ .. code-block:: python - x = Softmax(x) \ No newline at end of file + x = Softmax(x) diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst index 227b1ec5..324d6e0a 100644 --- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst +++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/add.rst @@ -2,4 +2,4 @@ add = Add(relation1, relation2) # or - add = relation1 + relation2 \ No newline at end of file + add = relation1 + relation2 diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst index d3a20424..ebd84aac 100644 --- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst +++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/div.rst @@ -2,4 +2,4 @@ div = Div(relation1, relation2) # or - div = relation1 / relation2 \ No newline at end of file + div = relation1 / relation2 diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst index 547b9164..d5b556bf 100644 --- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst +++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/mul.rst @@ -2,4 +2,4 @@ mul = Mul(relation1, relation2) # or - mul = relation1 * relation2 \ No newline at end of file + mul = relation1 * relation2 diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst index 68ce98a9..2326e9d8 100644 --- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst +++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/neg.rst @@ -1,3 +1,3 @@ .. code-block :: python - x = Neg(x) \ No newline at end of file + x = Neg(x) diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst index b7ae9085..c342a3e4 100644 --- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst +++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/pow.rst @@ -2,4 +2,4 @@ pow = Pow(relation, exp) # or - pow = relation1 ** relation2 \ No newline at end of file + pow = relation1 ** relation2 diff --git a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst index e10dcfad..ff47ab7c 100644 --- a/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst +++ b/docs/examples_basics/layer_module_ex/arithmetic_module_ex/sub.rst @@ -2,4 +2,4 @@ sub = Sub(relation1, relation2) # or - sub = relation1 - relation2 \ No newline at end of file + sub = relation1 - relation2 diff --git a/docs/examples_basics/layer_module_ex/fir.rst b/docs/examples_basics/layer_module_ex/fir.rst index aab246cc..e140e08c 100644 --- a/docs/examples_basics/layer_module_ex/fir.rst +++ b/docs/examples_basics/layer_module_ex/fir.rst @@ -16,8 +16,8 @@ Passing a parameter: Parameters initialization: .. code-block:: python - + x = Input('x') F = Input('F') fir_x = Fir(W_init='init_negexp')(x.tw(0.2)) - fir_F = Fir(W_init='init_constant', W_init_params={'value':1})(F.last()) \ No newline at end of file + fir_F = Fir(W_init='init_constant', W_init_params={'value':1})(F.last()) diff --git a/docs/examples_basics/layer_module_ex/part_module/part.rst b/docs/examples_basics/layer_module_ex/part_module/part.rst index fb05be16..80b30327 100644 --- a/docs/examples_basics/layer_module_ex/part_module/part.rst +++ b/docs/examples_basics/layer_module_ex/part_module/part.rst @@ -1,4 +1,4 @@ .. code-block:: python - + x = Input('x', dimensions=3).last() - relation = Part(x, 0, 1) \ No newline at end of file + relation = Part(x, 0, 1) diff --git a/docs/examples_basics/layer_module_ex/part_module/sample_part.rst b/docs/examples_basics/layer_module_ex/part_module/sample_part.rst index 496eeeae..3ce85bc8 100644 --- a/docs/examples_basics/layer_module_ex/part_module/sample_part.rst +++ b/docs/examples_basics/layer_module_ex/part_module/sample_part.rst @@ -1,4 +1,4 @@ .. code-block:: python - + x = Input('x').sw(3) - relation = SamplePart(x, 0, 1) \ No newline at end of file + relation = SamplePart(x, 0, 1) diff --git a/docs/examples_basics/layer_module_ex/part_module/sample_select.rst b/docs/examples_basics/layer_module_ex/part_module/sample_select.rst index a531ba1b..e9e40a50 100644 --- a/docs/examples_basics/layer_module_ex/part_module/sample_select.rst +++ b/docs/examples_basics/layer_module_ex/part_module/sample_select.rst @@ -1,4 +1,4 @@ .. code-block:: python x = Input('x').sw(3) - relation = SampleSelect(x, 1) \ No newline at end of file + relation = SampleSelect(x, 1) diff --git a/docs/examples_basics/layer_module_ex/part_module/select.rst b/docs/examples_basics/layer_module_ex/part_module/select.rst index 13cf44f4..8bcc809c 100644 --- a/docs/examples_basics/layer_module_ex/part_module/select.rst +++ b/docs/examples_basics/layer_module_ex/part_module/select.rst @@ -1,4 +1,4 @@ .. code-block:: python x = Input('x', dimensions=3).last() - relation = Select(x, 1) \ No newline at end of file + relation = Select(x, 1) diff --git a/docs/examples_basics/layer_module_ex/part_module/time_part.rst b/docs/examples_basics/layer_module_ex/part_module/time_part.rst index 87a3297b..08d66d61 100644 --- a/docs/examples_basics/layer_module_ex/part_module/time_part.rst +++ b/docs/examples_basics/layer_module_ex/part_module/time_part.rst @@ -1,4 +1,4 @@ .. code-block:: python x = Input('x').sw(10) - time_part = TimePart(x, i=0, j=5) \ No newline at end of file + time_part = TimePart(x, i=0, j=5) diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst index ba879cb6..304216ff 100644 --- a/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst +++ b/docs/examples_basics/layer_module_ex/trig_module_ex/cos.rst @@ -1,3 +1,3 @@ .. code-block:: python - cos = Cos(relation) \ No newline at end of file + cos = Cos(relation) diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst index 1f1852d4..975a677c 100644 --- a/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst +++ b/docs/examples_basics/layer_module_ex/trig_module_ex/cosh.rst @@ -1,3 +1,3 @@ .. code-block:: python - cosh = Cosh(relation) \ No newline at end of file + cosh = Cosh(relation) diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst index c3445322..76c4e6fb 100644 --- a/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst +++ b/docs/examples_basics/layer_module_ex/trig_module_ex/sech.rst @@ -1,3 +1,3 @@ .. code-block:: python - sech = Sech(relation) \ No newline at end of file + sech = Sech(relation) diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst index 1ee6cb4e..1f64a417 100644 --- a/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst +++ b/docs/examples_basics/layer_module_ex/trig_module_ex/sin.rst @@ -1,3 +1,3 @@ .. code-block:: python - sin = Sin(relation) \ No newline at end of file + sin = Sin(relation) diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst index 420ff4c1..c816c5ac 100644 --- a/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst +++ b/docs/examples_basics/layer_module_ex/trig_module_ex/tan.rst @@ -1,3 +1,3 @@ .. code-block:: python - tan = Tan(relation) \ No newline at end of file + tan = Tan(relation) diff --git a/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst b/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst index bc3285c6..4191bf64 100644 --- a/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst +++ b/docs/examples_basics/layer_module_ex/trig_module_ex/tanh.rst @@ -1,3 +1,3 @@ .. code-block:: python - tanh = Tanh(relation) \ No newline at end of file + tanh = Tanh(relation) diff --git a/docs/examples_basics/parameter_module_ex/sample_time.rst b/docs/examples_basics/parameter_module_ex/sample_time.rst index 994f1290..171b6e00 100644 --- a/docs/examples_basics/parameter_module_ex/sample_time.rst +++ b/docs/examples_basics/parameter_module_ex/sample_time.rst @@ -1,3 +1,3 @@ .. code-block:: python - dt = SampleTime() \ No newline at end of file + dt = SampleTime() diff --git a/docs/examples_basics/trainer_module_ex/addMinimize.rst b/docs/examples_basics/trainer_module_ex/addMinimize.rst index a61c02a6..941f46b8 100644 --- a/docs/examples_basics/trainer_module_ex/addMinimize.rst +++ b/docs/examples_basics/trainer_module_ex/addMinimize.rst @@ -1,3 +1,3 @@ .. code-block:: python - model.addMinimize('minimize_op', streamA, streamB, loss_function='mse') \ No newline at end of file + model.addMinimize('minimize_op', streamA, streamB, loss_function='mse') diff --git a/docs/examples_basics/trainer_module_ex/removeMinimize.rst b/docs/examples_basics/trainer_module_ex/removeMinimize.rst index 7999e263..3f66f480 100644 --- a/docs/examples_basics/trainer_module_ex/removeMinimize.rst +++ b/docs/examples_basics/trainer_module_ex/removeMinimize.rst @@ -1,3 +1,3 @@ .. code-block:: python - model.removeMinimize(['minimize_op1', 'minimize_op2']) \ No newline at end of file + model.removeMinimize(['minimize_op1', 'minimize_op2']) diff --git a/docs/examples_basics/trainer_module_ex/trainModel.rst b/docs/examples_basics/trainer_module_ex/trainModel.rst index a835dea3..57d55385 100644 --- a/docs/examples_basics/trainer_module_ex/trainModel.rst +++ b/docs/examples_basics/trainer_module_ex/trainModel.rst @@ -36,4 +36,4 @@ Example - recurrent training: mass_spring_damper.loadData(name='mass_spring_dataset', source=data_folder, format=data_struct, delimiter=';') params = {'num_of_epochs': 100, 'train_batch_size': 128, 'lr': 0.001} - mass_spring_damper.trainModel(splits=[70, 20, 10], prediction_samples=10, training_params=params) \ No newline at end of file + mass_spring_damper.trainModel(splits=[70, 20, 10], prediction_samples=10, training_params=params) diff --git a/docs/index.rst b/docs/index.rst index d0b78a50..1497c358 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,15 +8,15 @@ Welcome to nnodely's documentation! .. image:: https://raw.githubusercontent.com/tonegas/nnodely/main/imgs/logo_white_info.png :target: https://github.com/tonegas/nnodely - :alt: Open + :alt: Open -*nnodely* is a framework designed to facilitate the creation and deployment of **Model-Structured Neural Networks** (**MSNNs**). -Modeling, control, and estimation of physical systems impose constraints that differ fundamentally from typical -deep-learning tasks (e.g., images or text). In engineering applications, models often need to respect -known physical laws or constraints, operate in real time, remain interpretable, and generalize reliably -even when only limited experimental data are available. MS-NNs combine the learning capabilities of neural -networks with structural priors grounded in physics, control and estimation theory, enabling: +*nnodely* is a framework designed to facilitate the creation and deployment of **Model-Structured Neural Networks** (**MSNNs**). +Modeling, control, and estimation of physical systems impose constraints that differ fundamentally from typical +deep-learning tasks (e.g., images or text). In engineering applications, models often need to respect +known physical laws or constraints, operate in real time, remain interpretable, and generalize reliably +even when only limited experimental data are available. MS-NNs combine the learning capabilities of neural +networks with structural priors grounded in physics, control and estimation theory, enabling: - **Data Efficiency**: By embedding structural priors, MS-NNs can learn effectively from limited data, reducing the need for extensive datasets. @@ -66,7 +66,7 @@ Overview .. sidebar:: Overview - + Overview of the *nnodely* development pipeline. It spans model design (:ref:`PH1 `), dataset construction aligned with the network architecture (:ref:`PH2 `), training (:ref:`PH3 `), domain-specific validation (:ref:`PH4 `), model export (:ref:`PH5 `), and composition of complex models (:ref:`PH6 `). Ellipses indicate the pipeline phases, while rectangles denote the artifacts produced at each phase. @@ -99,4 +99,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/requirements.txt b/docs/requirements.txt index 8e66700e..415be759 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,4 @@ myst_parser nbsphinx pandoc jupyter -jupyterlab \ No newline at end of file +jupyterlab diff --git a/tests/get_samples_data/data.csv b/tests/get_samples_data/data.csv index 74a9c6f3..5ba268fb 100644 --- a/tests/get_samples_data/data.csv +++ b/tests/get_samples_data/data.csv @@ -7,4 +7,4 @@ x,y 6,4 0,5 0,6 -0,7 \ No newline at end of file +0,7 diff --git a/tests/multifile/file1.csv b/tests/multifile/file1.csv index 145603d0..4141b195 100644 --- a/tests/multifile/file1.csv +++ b/tests/multifile/file1.csv @@ -8,4 +8,4 @@ x,y 10,10 10,10 10,10 -10,10 \ No newline at end of file +10,10 diff --git a/tests/multifile/file2.csv b/tests/multifile/file2.csv index ac05b0d0..9ba54d2f 100644 --- a/tests/multifile/file2.csv +++ b/tests/multifile/file2.csv @@ -18,4 +18,4 @@ x,y 20,20 20,20 20,20 -20,20 \ No newline at end of file +20,20 diff --git a/tests/multifile/file3.csv b/tests/multifile/file3.csv index 79180bf8..bc734e0d 100644 --- a/tests/multifile/file3.csv +++ b/tests/multifile/file3.csv @@ -28,4 +28,4 @@ x,y 30,30 30,30 30,30 -30,30 \ No newline at end of file +30,30 diff --git a/tests/multifile2/file1.csv b/tests/multifile2/file1.csv index 7d5eaf2f..a28e024a 100644 --- a/tests/multifile2/file1.csv +++ b/tests/multifile2/file1.csv @@ -8,4 +8,4 @@ x,y 40,40 40,40 40,40 -40,40 \ No newline at end of file +40,40 diff --git a/tests/multifile2/file2.csv b/tests/multifile2/file2.csv index 7713584e..47c5fef4 100644 --- a/tests/multifile2/file2.csv +++ b/tests/multifile2/file2.csv @@ -18,4 +18,4 @@ x,y 50,50 50,50 50,50 -50,50 \ No newline at end of file +50,50 diff --git a/tests/multifile2/file3.csv b/tests/multifile2/file3.csv index 31898748..440c03f5 100644 --- a/tests/multifile2/file3.csv +++ b/tests/multifile2/file3.csv @@ -28,4 +28,4 @@ x,y 60,60 60,60 60,60 -60,60 \ No newline at end of file +60,60 diff --git a/tests/multifile3/file1.csv b/tests/multifile3/file1.csv index bee647ba..56266193 100644 --- a/tests/multifile3/file1.csv +++ b/tests/multifile3/file1.csv @@ -48,4 +48,4 @@ x,y 125,125 126,126 127,127 -128,128 \ No newline at end of file +128,128 diff --git a/tests/test_data/testdata.dta b/tests/test_data/testdata.dta index 535ad715..405cdd02 100644 --- a/tests/test_data/testdata.dta +++ b/tests/test_data/testdata.dta @@ -15,4 +15,3 @@ x1 y1 x2 y2 A1x A1y B1x B1y A2x A2y 0.812 0.818 0.348 0.453 - 0.350 1.375 0.571 1.199 - 0.575 1.375 0.699 3.214 - 0.274 0.742 12.556 0.100 - 0.813 0.816 0.354 0.445 - 0.350 1.375 0.567 1.194 - 0.575 1.375 0.695 3.211 - 0.273 0.733 12.570 0.110 - 0.814 0.814 0.361 0.436 - 0.350 1.375 0.562 1.189 - 0.575 1.375 0.690 3.207 - 0.272 0.723 12.585 0.120 - - diff --git a/tests/val_data/testdata.dta b/tests/val_data/testdata.dta index 3175afdc..24f246c3 100644 --- a/tests/val_data/testdata.dta +++ b/tests/val_data/testdata.dta @@ -13,4 +13,3 @@ x1 y1 x2 y2 A1x A1y B1x B1y A2x A2y 0.809 0.820 0.337 0.466 - 0.350 1.375 0.577 1.208 - 0.575 1.375 0.706 2.220 - 0.274 0.857 12.533 0.080 - 0.810 0.819 0.342 0.460 - 0.350 1.375 0.574 1.204 - 0.575 1.375 0.703 2.217 - 0.274 0.850 12.543 0.090 - 0.812 0.818 0.348 0.453 - 0.350 1.375 0.571 1.199 - 0.575 1.375 0.699 2.214 - 0.274 0.842 12.556 0.100 - - diff --git a/tests/vector_data/vector_2.dta b/tests/vector_data/vector_2.dta index bb9f5885..e5b91343 100644 --- a/tests/vector_data/vector_2.dta +++ b/tests/vector_data/vector_2.dta @@ -14,4 +14,3 @@ x1 x2 x3 x4 y1 y2 y3 NO NO NO NO k1 0.805 0.825 0.322 0.485 0.350 1.375 0.585 1.218 - 0.575 1.375 0.714 1.227 - 0.274 0.977 12.502 0.040 0.806 0.824 0.325 0.481 0.350 1.375 0.584 1.216 - 0.575 1.375 0.712 1.225 - 0.274 0.973 12.508 0.050 0.807 0.823 0.329 0.477 0.350 1.375 0.582 1.214 - 0.575 1.375 0.710 1.224 - 0.274 0.969 12.515 0.060 - diff --git a/tests/vehicle_data/vehicle.csv b/tests/vehicle_data/vehicle.csv index 5625a587..3d80578b 100644 --- a/tests/vehicle_data/vehicle.csv +++ b/tests/vehicle_data/vehicle.csv @@ -98,4 +98,4 @@ 13.473333333333336,12.779200000000001,0.,5,-0.7460835566787409,-0.6602761975976819,-0.5746327099489577,-0.48922828574131927,-0.4061226600199461,-0.3259584416340431,-0.2506976788511679,-0.18109245144700026,-0.11566989947226602,-0.05551166846061051,0.,0.04959411345004128,0.09326429982348827,0.12985450593151882,0.16023045483507303,0.1832476556463689,0.20626485645772163,0.22928205726901751,0.2522992580803134,0.2753164588916661,0.298333659702962,0.09911902783658734 13.474444444444448,13.0928,0.,5,-0.7485232229184362,-0.6627158638373771,-0.5770452306900893,-0.4916408064824509,-0.4081821510060877,-0.32801793262018464,-0.25204581648097246,-0.18244058907686167,-0.11638159820341798,-0.05622336719181931,0.,0.04959411345004128,0.09414036583672214,0.13073057194475268,0.162020926674586,0.1850381274858819,0.2080553282972346,0.2310725291085305,0.25408972991982637,0.2771069307311791,0.30012413154247497,0.11169376341230414 13.496111111111112,13.014400000000002,0.,5,-0.7509649513013414,-0.6651575922202255,-0.5794597906294712,-0.4940553664218328,-0.41024338278987216,-0.3300791644039691,-0.2533950936325482,-0.1837898662284374,-0.11709389850244634,-0.05693566749079082,0.,0.04959411345004128,0.09501717235025353,0.13160737845828407,0.16381291192163872,0.1868301127329346,0.20984731354423047,0.2328645143555832,0.2558817151668791,0.2788989159782318,0.3019161167895277,0.1517397792930878 -13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794 \ No newline at end of file +13.51277777777778,13.0928,0.,5,-0.7534101501202031,-0.667602791039144,-0.581877782390336,-0.49647335818269767,-0.41230754420877247,-0.3321433258228126,-0.2547462885158893,-0.18514106111177853,-0.11780721119600912,-0.05764898018441045,0.,0.04959411345004128,0.09589522507155834,0.13248543117953204,0.16560744412277018,0.18862464493406605,0.21164184574541878,0.23465904655671466,0.2576762473680674,0.28069344817936326,0.30371064899065914,0.19856692399271794 From 3bf9d527a4a74e05a470b2182103f09b148bca81 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:13:41 +0200 Subject: [PATCH 08/26] feat: updated codecov action --- .github/workflows/codecov.yml | 38 ++++++++++++----------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 5a54aa8b..43272b24 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -1,42 +1,30 @@ name: Coverage (Codecov) on: - push: - branches: ["main", "develop"] pull_request: branches: ["main", "develop"] jobs: - test: - name: Run tests and upload coverage + coverage: + name: Compute coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Set up Python 3.10 - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - python-version: "3.10" - - - name: Install pandoc - uses: pandoc/actions/setup@v1 + version: "0.11.6" + enable-cache: true - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install coverage - python -m pip install sphinx myst_parser sphinx-rtd-theme nbsphinx pandoc jupyter jupyterlab - pip install -e . + - name: Install the project + run: uv sync --locked --all-extras --dev - name: Tests with coverage - run: | - coverage run --omit='./results/*','./docs/*','./examples/*' -m unittest discover tests - coverage xml -o coverage.xml + run: uv run pytest --cov=nnodely --cov-branch --cov-report=xml tests - name: Upload results to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - + uses: codecov/codecov-action@v6 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From c70ebfa1aec1defbcfc67ec46fb85a21805650a6 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:56:17 +0200 Subject: [PATCH 09/26] feat: removed pin on `uv` version We'll see how that turns out --- .github/workflows/codecov.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 43272b24..48782979 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,7 +15,6 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 with: - version: "0.11.6" enable-cache: true - name: Install the project From 40b152b800099c6ab31dd03f0cae7ac49d542096 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:56:57 +0200 Subject: [PATCH 10/26] feat: updated run tests action --- .github/workflows/run-tests.yml | 50 +++++++++------------------------ 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index cec03b02..dccfb921 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,51 +1,29 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - name: Run Tests on: - push: - branches: [ "main", "develop" ] pull_request: - branches: [ "main", "develop"] + branches: ["main", "develop"] jobs: - build: - + test: runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: [ "3.10", "3.11", "3.12"] - architecture: ['x64', 'x86'] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install pandoc - uses: pandoc/actions/setup@v1 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 pytest - python -m pip install sphinx myst_parser sphinx-rtd-theme nbsphinx pandoc jupyter jupyterlab - pip install -e . - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest - - - + - uses: actions/checkout@v6 + - name: Install uv and set the Python version + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + - name: Install the project + run: uv sync --locked --all-extras --dev + - name: Run tests + run: uv run pytest tests From f33d9d79b0dc00aa5154a78070125fc5b4b2ffce Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:59:58 +0200 Subject: [PATCH 11/26] feat: added linting via ruff --- .github/workflows/lint.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..57db3ccc --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: Lint + +on: + pull_request: + branches: ["main", "develop"] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install the project + run: uv sync --locked --all-extras --dev + + - name: Lint + run: uv run ruff check From e1801cf7a43d8f9118660fe18ebe6ed8fe1266e0 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:11:19 +0200 Subject: [PATCH 12/26] chore: ruff check --fix --- .../lateral_dynamics_control.ipynb | 1 - .../lateral_dynamics_model.ipynb | 1 - .../lateral_dynamics_model_torch.ipynb | 1 - case-studies/neuralODE/neuralODE_msd.ipynb | 3 --- case-studies/pinn/pinn_Burgers_equation.ipynb | 20 +++++++++---------- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb index b29fff17..d4ce0344 100644 --- a/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb +++ b/case-studies/lateral_dynamics/lateral_dynamics_control.ipynb @@ -46,7 +46,6 @@ ], "source": [ "# Import necessary packages\n", - "import sys, os\n", "import pandas as pd\n", "\n", "# import nnodely modules\n", diff --git a/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb index daa5f142..83e20531 100644 --- a/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb +++ b/case-studies/lateral_dynamics/lateral_dynamics_model.ipynb @@ -47,7 +47,6 @@ ], "source": [ "# Import necessary packages\n", - "import sys, os\n", "import pandas as pd\n", "\n", "# import nnodely modules\n", diff --git a/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb b/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb index bc406c8c..733b2a73 100644 --- a/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb +++ b/case-studies/lateral_dynamics/lateral_dynamics_model_torch.ipynb @@ -49,7 +49,6 @@ ], "source": [ "# Import necessary packages\n", - "import os\n", "import numpy as np\n", "import torch\n", "from torch import nn\n", diff --git a/case-studies/neuralODE/neuralODE_msd.ipynb b/case-studies/neuralODE/neuralODE_msd.ipynb index 0ad1497e..a9ede6aa 100644 --- a/case-studies/neuralODE/neuralODE_msd.ipynb +++ b/case-studies/neuralODE/neuralODE_msd.ipynb @@ -30,12 +30,9 @@ } ], "source": [ - "import os\n", "from nnodely import *\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", - "from nnodely.support import earlystopping\n", - "from nnodely.support.odeint.adjoint import odeint_adjoint\n", "\n", "\n", "def init_random_range(\n", diff --git a/case-studies/pinn/pinn_Burgers_equation.ipynb b/case-studies/pinn/pinn_Burgers_equation.ipynb index a350df58..2cb39068 100644 --- a/case-studies/pinn/pinn_Burgers_equation.ipynb +++ b/case-studies/pinn/pinn_Burgers_equation.ipynb @@ -1883,8 +1883,8 @@ " t = torch.zeros(100, dtype=torch.float32)\n", " u_target = -torch.sin(torch.pi * x)\n", " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", - " plt.plot(x.tolist(), u[\"U\"], label=f\"network\")\n", - " plt.plot(x.tolist(), u_target.tolist(), label=f\"target\")\n", + " plt.plot(x.tolist(), u[\"U\"], label=\"network\")\n", + " plt.plot(x.tolist(), u_target.tolist(), label=\"target\")\n", " plt.grid(True)\n", " plt.legend(loc=\"best\")\n", " plt.xlabel(\"x[t=0]\")\n", @@ -1895,19 +1895,19 @@ " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", " t = torch.ones(100, dtype=torch.float32) * 0.25\n", " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", - " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=0.25\")\n", + " plt.plot(x.tolist(), u[\"U\"], label=\"network t=0.25\")\n", " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", " t = torch.ones(100, dtype=torch.float32) * 0.5\n", " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", - " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=0.5\")\n", + " plt.plot(x.tolist(), u[\"U\"], label=\"network t=0.5\")\n", " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", " t = torch.ones(100, dtype=torch.float32) * 0.75\n", " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", - " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=0.75\")\n", + " plt.plot(x.tolist(), u[\"U\"], label=\"network t=0.75\")\n", " x = torch.linspace(-1, 1, steps=100, dtype=torch.float32)\n", " t = torch.ones(100, dtype=torch.float32)\n", " u = self.modely({\"x\": x.tolist(), \"t\": t.tolist()})\n", - " plt.plot(x.tolist(), u[\"U\"], label=f\"network t=1\")\n", + " plt.plot(x.tolist(), u[\"U\"], label=\"network t=1\")\n", " plt.grid(True)\n", " plt.legend(loc=\"best\")\n", " plt.xlabel(\"x\")\n", @@ -1919,14 +1919,14 @@ " x_1 = torch.ones(100, dtype=torch.float32)\n", " u_1_target = torch.zeros(100, dtype=torch.float32)\n", " u_1 = self.modely({\"x\": x_1.tolist(), \"t\": t_1.tolist()})\n", - " plt.plot(t_1.tolist(), u_1[\"U\"], label=f\"network x=1\")\n", - " plt.plot(t_1.tolist(), u_1_target.tolist(), label=f\"target x=1\")\n", + " plt.plot(t_1.tolist(), u_1[\"U\"], label=\"network x=1\")\n", + " plt.plot(t_1.tolist(), u_1_target.tolist(), label=\"target x=1\")\n", " t_2 = torch.linspace(0, 1, steps=100, dtype=torch.float32)\n", " x_2 = -torch.ones(100, dtype=torch.float32)\n", " u_2_target = torch.zeros(100, dtype=torch.float32)\n", " u_2 = self.modely({\"x\": x_2.tolist(), \"t\": t_2.tolist()})\n", - " plt.plot(t_2.tolist(), u_2[\"U\"], label=f\"network x=-1\")\n", - " plt.plot(t_2.tolist(), u_2_target.tolist(), label=f\"target x=-1\")\n", + " plt.plot(t_2.tolist(), u_2[\"U\"], label=\"network x=-1\")\n", + " plt.plot(t_2.tolist(), u_2_target.tolist(), label=\"target x=-1\")\n", " plt.grid(True)\n", " plt.legend(loc=\"best\")\n", " plt.xlabel(\"x[t]\")\n", From 6f23f449b98b4159fd94c3654cc7e49604988beb Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:12:15 +0200 Subject: [PATCH 13/26] feat: added vscode config --- .vscode/extensions.json | 7 +++++++ .vscode/settings.json | 9 +++++++++ 2 files changed, 16 insertions(+) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..25a30545 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d410bbd2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "python.languageServer": "Pylance", + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true + }, + "ruff.enable": true, + "ruff.lint.enable": true +} From baafd1d4e72cac94a3807ff06e2f03b58f64de21 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:49:53 +0200 Subject: [PATCH 14/26] feat: cleanup .gitignore Some things will change, but at least now its manageable --- .gitignore | 185 +++-------------------------------------------------- 1 file changed, 9 insertions(+), 176 deletions(-) diff --git a/.gitignore b/.gitignore index 5ca985b7..1eaa0598 100644 --- a/.gitignore +++ b/.gitignore @@ -1,182 +1,15 @@ -# 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 - -# 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/ -docs/out_docs/ - -# 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/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# 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 +__pycache__ .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/ - -# vscode test .vscode +!.vscode/settings.json +!.vscode/extensions.json -# MacOS system files -.DS_Store - -# default results folder results +dist/ +docs/_build +docs/out_docs -# default temp folder -temp - -# Todo for testing -TODO/ - -#trained_models folder -trained_models -trained_models_torch +.DS_Store +.coverage +coverage.xml From 5d11934e3ecdaf824931b166d8600b78c1a0837f Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:56:21 +0200 Subject: [PATCH 15/26] docs: contributing --- CONTRIBUTING.md | 192 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fd834c80 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,192 @@ +# Contributing + +Thanks for your interest in **nnodely**! 🎉 +We're just getting started and welcome contributions of all kinds — bug +reports, documentation fixes, examples, and new features. + +## Setup + +All you need to do is: + +- install [uv](https://docs.astral.sh/uv/) +- clone the repo +- run `uv sync --dev` +- install the hooks `uv run pre-commit install --install-hooks` + +You are now up and running, just make sure to run everything via `uv` (e.g., +`uv run ...`). + +### IDE + +We have config files for the IDEs we use, so `VSCode`, `PyCharm`, and `neovim` +should be ready to go. Anyhow, here are some tips for each, but keep in mind +that the CI and pre-commit hooks are the main source of truth for linting and +formatting. + +- `VSCode`: you can find a `.vscode` folder which should prompt you to install + the necessary plugins and enable them as we expect them to be. Nonetheless + know that it will use `PyLance` and not bare `pyright` so you will see + different diagnostics. It should pick up the `uv` managed environment right + away. +- `PyCharm`: enable `pyright` and `ruff` and you should be good to go. It + should also pick up `uv` automatically. +- `neovim`: you have to add the `pyright` and `ruff` (e.g., using `mason`) and + enable them (e.g., `vim.lsp.enable(...)`). To pick up `uv` just run `uv run +nvim .` in the project root. + +> `neovim` is harder to setup, but worth it! Checkout my +> [config](https://github.com/SebastianoTaddei/nvim-config) for reference on +> the Python setup. + +## Code Style + +This project follows a strict and largely automated Python code style. The goal +is to keep the codebase consistent, readable, and easy to review, while +minimising style-related discussion in PRs. + +In short: **let the tools do the work**. + +--- + +## General Principles + +- Prefer clarity over cleverness. +- Keep functions and classes small and focused. +- Be consistent with existing code. + +--- + +## Python Version + +We target all the [supported Python +versions](https://devguide.python.org/versions/). Tests will catch most of the +version specific behaviour, but please keep it in mind. + +--- + +## Linting and Formatting + +This project uses `ruff` for both linting and formatting. + +- All code **must** pass `ruff` checks. +- Formatting is enforced via `ruff format`. +- Do **not** manually fight the formatter. + +A `pre-commit` hook will take care of this, but you can also run it manually with: + +```bash +uv run ruff check +uv run ruff format +``` + +### Ignoring Rules and Formatting + +Disabling rules or formatting should be rare and justified: + +```python +value = legacy_call() # noqa: PLW0603 # required by external API + +# fmt: off +table = [ + ("short", 1), + ("muchlonger", 2), +] +# fmt: on +``` + +--- + +## Naming Conventions + +> Needs to be understood. + +--- + +## Type Hints + +- Always use type hints unless absolutely unfeasible. +- Make custom types whenever your type gets too big, for example: + + ```python + # This horrible mess + list[dict[str, list[int]]] + + # Should become + CustomType = list[dict[str, list[int]]] + ``` + +- Heavily prefer strong typing (e.g., `Enum` and `dataclass`), for example: + + ```python + # Instead of this + def func(flag: str) -> None: + ... + + # Do this + class Flag(Enum): + ... + + def func(flag: Flag) -> None: + ... + ``` + +Docstrings and Comments +• Use docstrings for public modules, classes, and functions +• Follow the project’s configured docstring style +• Comments should explain why, not what +• Avoid obvious or redundant comments + +--- + +## Pre-commit Hooks + +This project uses pre-commit hooks to enforce good behaviour. You are expected +to install and run them locally: + +```bash +uv run pre-commit install --install-hooks +uv run pre-commit run --all-files +``` + +--- + +## Branching + +### Branching Model + +- `main` is the default branch + - Always stable. + - Always releasable. + - Protected (no direct commits). +- `dev` is the rolling branch where we put all development not quite stable + yet. +- All work happens on **feature branches**, created from `main` + +### Branch Naming + +We follow [this](https://conventional-branch.github.io). + +--- + +## Commit Messages + +We follow [this](https://www.conventionalcommits.org/en/v1.0.0/). + +--- + +## Dev tips + +A collection of tips for the developers of the library. + +### Why `uv` + +It will take too long to explain here. I strongly suggest you to try it out on +a sample project and it will be clear why. + +### GitHub Actions + +Testing GitHub Actions is a pain, but it becomes easier if you test at least +some of their functionality with [act](https://github.com/nektos/act). + +For example, to test the `codecov.xml` action just setup `act` and run: `act +--workflows .github/workflows/codecov.yml". From b3c7ef0993a08e2edc6891732b967c15043d3ff0 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:18:02 +0200 Subject: [PATCH 16/26] docs: updated contributing file --- CONTRIBUTING.md | 59 +++++++++++++++++-------------------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd834c80..dc962b34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,9 +19,7 @@ You are now up and running, just make sure to run everything via `uv` (e.g., ### IDE We have config files for the IDEs we use, so `VSCode`, `PyCharm`, and `neovim` -should be ready to go. Anyhow, here are some tips for each, but keep in mind -that the CI and pre-commit hooks are the main source of truth for linting and -formatting. +should be ready to go. - `VSCode`: you can find a `.vscode` folder which should prompt you to install the necessary plugins and enable them as we expect them to be. Nonetheless @@ -34,10 +32,6 @@ formatting. enable them (e.g., `vim.lsp.enable(...)`). To pick up `uv` just run `uv run nvim .` in the project root. -> `neovim` is harder to setup, but worth it! Checkout my -> [config](https://github.com/SebastianoTaddei/nvim-config) for reference on -> the Python setup. - ## Code Style This project follows a strict and largely automated Python code style. The goal @@ -75,7 +69,7 @@ This project uses `ruff` for both linting and formatting. A `pre-commit` hook will take care of this, but you can also run it manually with: ```bash -uv run ruff check +uv run ruff check --fix uv run ruff format ``` @@ -98,13 +92,17 @@ table = [ ## Naming Conventions -> Needs to be understood. +We use: + +- `snake_case` for functions and variables +- `CamelCase` for classes +- `UPPERCASE` for constants --- ## Type Hints -- Always use type hints unless absolutely unfeasible. +- Required unless impractical. - Make custom types whenever your type gets too big, for example: ```python @@ -112,7 +110,7 @@ table = [ list[dict[str, list[int]]] # Should become - CustomType = list[dict[str, list[int]]] + CustomType: TypeAlias = list[dict[str, list[int]]] ``` - Heavily prefer strong typing (e.g., `Enum` and `dataclass`), for example: @@ -130,23 +128,14 @@ table = [ ... ``` -Docstrings and Comments -• Use docstrings for public modules, classes, and functions -• Follow the project’s configured docstring style -• Comments should explain why, not what -• Avoid obvious or redundant comments - --- -## Pre-commit Hooks +## Docstrings and Comments -This project uses pre-commit hooks to enforce good behaviour. You are expected -to install and run them locally: - -```bash -uv run pre-commit install --install-hooks -uv run pre-commit run --all-files -``` +- Use docstrings for public modules, classes, and functions +- Follow the project's configured docstring style +- Comments should explain why, not what +- Avoid obvious or redundant comments --- @@ -158,9 +147,10 @@ uv run pre-commit run --all-files - Always stable. - Always releasable. - Protected (no direct commits). -- `dev` is the rolling branch where we put all development not quite stable - yet. -- All work happens on **feature branches**, created from `main` +- `develop` is the rolling branch + - Put all development not quite stable yet. + - Protected (no direct commits). +- All work happens on branches created from `develop`. ### Branch Naming @@ -174,19 +164,10 @@ We follow [this](https://www.conventionalcommits.org/en/v1.0.0/). --- -## Dev tips - -A collection of tips for the developers of the library. - -### Why `uv` - -It will take too long to explain here. I strongly suggest you to try it out on -a sample project and it will be clear why. - -### GitHub Actions +## GitHub Actions Testing GitHub Actions is a pain, but it becomes easier if you test at least some of their functionality with [act](https://github.com/nektos/act). For example, to test the `codecov.xml` action just setup `act` and run: `act ---workflows .github/workflows/codecov.yml". +--workflows .github/workflows/codecov.yml`. From e649180d3bcb055b5581a0dfcdbcfc8bbb54890d Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:52:30 +0200 Subject: [PATCH 17/26] feat: avoid running tests if no python file was changed --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53580b84..5ba6b610 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,3 +38,4 @@ repos: entry: uv run pytest language: system pass_filenames: false + types: [python] From 72f154892ffb84ee6793bd1153a8cc73ccc60cf0 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:52:42 +0200 Subject: [PATCH 18/26] feat: added patch testing --- codecov.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 3d07b274..f3dc1ddf 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,4 +3,8 @@ coverage: project: default: target: auto - threshold: 1% # the leniency in hitting the target + threshold: 1% + patch: + default: + target: 90% + threshold: 0% From 24fbb76d5be3367a0ad62cc0f08a97e3e3bca4db Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:53:11 +0200 Subject: [PATCH 19/26] feat: added back ruff check --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ba6b610..02018fe7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,11 +23,11 @@ repos: entry: uv lock language: system pass_filenames: false - # - id: ruff-check - # name: ruff check - # entry: uv run ruff check --fix - # language: system - # types: [python] + - id: ruff-check + name: ruff check + entry: uv run ruff check --fix + language: system + types: [python] - id: ruff-format name: ruff format entry: uv run ruff format From b9323f605f119a712f9430e9f6ea31b31b013b68 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:59:54 +0200 Subject: [PATCH 20/26] feat: updated publish workflow --- .github/workflows/create-release.yml | 130 --------------------------- .github/workflows/publish.yml | 89 ++++++++++++++++++ 2 files changed, 89 insertions(+), 130 deletions(-) delete mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml deleted file mode 100644 index 605f4a43..00000000 --- a/.github/workflows/create-release.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: Publish New nnodely Release - -on: - push: - tags: - - "v*" # Push events to matching v*, i.e., v1.0, v20.15.10 - -jobs: - check-tag-version: - name: Check tag version equal to file version - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Extract tag version - run: | - TAG_NAME="${{ github.ref_name }}" - TAG_VERSION=$(echo $TAG_NAME | sed 's/v//') - echo "Tag version: $TAG_VERSION" - echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_ENV - - name: Extract file version - run: | - FILE_VERSION=$(awk -F"'" '/^__version__/ {print $2}' nnodely/__init__.py) - echo "File version: $FILE_VERSION" - echo "FILE_VERSION=$FILE_VERSION" >> $GITHUB_ENV - - name: Show tag and file versions - run: | - echo "The tag version: ${{ env.TAG_VERSION }}" - echo "The file version: ${{ env.FILE_VERSION }}" - - name: Check if tag and file versions match - if: ${{ env.TAG_VERSION != env.FILE_VERSION }} - run: | - echo "The tag version ${{ env.TAG_VERSION }} does not match file version ${{ env.FILE_VERSION }}" - exit 1 - - build: - name: Build distribution of tagged version - needs: - - check-tag-version - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name }} - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Install pypa/build - run: >- - python3 -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: python3 -m build - - name: Store the distribution packages - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - publish-to-pypi: - name: Publish to PyPI - needs: - - build - - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/nnodely # Replace with your PyPI project name - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Publish nnodely to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - - github-release: - name: >- - Sign the Python with Sigstore and upload them to GitHub Release - needs: - - publish-to-pypi - runs-on: ubuntu-latest - - permissions: - contents: write # IMPORTANT: mandatory for making GitHub Releases - id-token: write # IMPORTANT: mandatory for sigstore - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v3.0.0 - with: - inputs: >- - ./dist/*.tar.gz - ./dist/*.whl - - name: Get the branch version - run: | - TAG_NAME="${{ github.ref_name }}" - TAG_MESSAGE=${{ github.event.workflow_run.head_commit.message }} - echo "Tag message: $TAG_MESSAGE" - echo "TAG_MESSAGE=$TAG_MESSAGE" >> $GITHUB_ENV - - name: Create GitHub release - env: - GITHUB_TOKEN: ${{ github.token }} - tag: ${{ github.ref_name }} - run: | - echo "The branch version: $tag" - gh release create $tag --title "$tag" --repo ${{ github.repository }} --notes "${{ env.TAG_MESSAGE }}" - - name: Upload artifact signatures to GitHub Release - env: - GITHUB_TOKEN: ${{ github.token }} - tag: ${{ github.ref_name }} - # Upload to GitHub Release using the `gh` CLI. - # `dist/` contains the built packages, and the - # sigstore-produced signatures and certificates. - run: >- - gh release upload $tag dist/** --repo ${{ github.repository }} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..bd22ff3b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,89 @@ +name: Publish + +on: + push: + tags: + - "v*.*.*" # Push events to matching v*, i.e., v1.0.0 + +jobs: + publish: + runs-on: ubuntu-latest + environment: + name: pypi + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Validate tag matches project version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PROJ_VERSION=$(uv version --short) + + echo "Tag version: $TAG_VERSION" + echo "Project version: $PROJ_VERSION" + + if [ "$TAG_VERSION" != "$PROJ_VERSION" ]; then + echo "Mismatch: tag=$TAG_VERSION project=$PROJ_VERSION" + exit 1 + fi + + - name: Build + run: uv build + + # Check that basic features work and we didn't miss to include crucial files + - name: Basic test (wheel) + run: uv run --isolated --no-project --with dist/*.whl python -c "import nnodely" + + - name: Basic test (source distribution) + run: uv run --isolated --no-project --with dist/*.tar.gz python -c "import nnodely" + + - name: Test publish + run: uv publish --index testpypi + + - name: Publish + run: uv publish + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: python-package-distributions + path: dist/ + + github-release: + name: Sign the Python with Sigstore and upload them to GitHub Release + runs-on: ubuntu-latest + needs: + - publish + + permissions: + contents: write + id-token: write + + steps: + - uses: actions/download-artifact@v8 + with: + name: python-package-distributions + path: dist/ + + - name: Sign artifacts + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: | + ./dist/*.tar.gz + ./dist/*.whl + + - name: Release + uses: softprops/action-gh-release@v3 + if: github.ref_type == 'tag' + with: + files: | + dist/*.tar.gz + dist/*.whl + generate_release_notes: true From 19079f01210677ed957eec8fbd05f9884c490907 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:34:48 +0200 Subject: [PATCH 21/26] chore: renamed for pypi/testpypi compatibility --- .github/workflows/{publish.yml => create-release.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{publish.yml => create-release.yml} (100%) diff --git a/.github/workflows/publish.yml b/.github/workflows/create-release.yml similarity index 100% rename from .github/workflows/publish.yml rename to .github/workflows/create-release.yml From 856710ecd6d27acedb2d55a3cecf556bb6aa50fb Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:48:31 +0200 Subject: [PATCH 22/26] feat: added publish job name --- .github/workflows/create-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index bd22ff3b..9d039707 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -7,6 +7,7 @@ on: jobs: publish: + name: Build and publish to both TestPyPi and PyPi runs-on: ubuntu-latest environment: name: pypi From c5442ff0c5f7cd0f5cd611ebab5a3c8c2eafa308 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:59:44 +0200 Subject: [PATCH 23/26] feat: added testpypi index --- pyproject.toml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ad34392f..20bc09a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,10 @@ dependencies = [ "Repository" = "https://github.com/tonegas/nnodely" [dependency-groups] -dev = [ - "pre-commit>=4.5.1", - "pytest-cov>=7.1.0", - "ruff>=0.15.10", -] +dev = ["pre-commit>=4.5.1", "pytest-cov>=7.1.0", "ruff>=0.15.10"] + +[[tool.uv.index]] +name = "testpypi" +url = "https://test.pypi.org/simple/" +publish-url = "https://test.pypi.org/legacy/" +explicit = true From bb87dc34ec557fed986cf9cacea6b728bc0b9ff3 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:59:57 +0200 Subject: [PATCH 24/26] chore: improved step names --- .github/workflows/create-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 9d039707..28ed7393 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -45,10 +45,10 @@ jobs: - name: Basic test (source distribution) run: uv run --isolated --no-project --with dist/*.tar.gz python -c "import nnodely" - - name: Test publish + - name: TestPyPi publish run: uv publish --index testpypi - - name: Publish + - name: PyPI publish run: uv publish - name: Upload artifacts From 27c39d99caa83276baad3d5a3e5269f6a2777171 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:23:54 +0200 Subject: [PATCH 25/26] feat: detect prerelase --- .github/workflows/create-release.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 28ed7393..3207b354 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -3,7 +3,7 @@ name: Publish on: push: tags: - - "v*.*.*" # Push events to matching v*, i.e., v1.0.0 + - "v*" jobs: publish: @@ -79,6 +79,16 @@ jobs: inputs: | ./dist/*.tar.gz ./dist/*.whl + - name: Detect prerelease + id: version + run: | + VERSION="${GITHUB_REF_NAME#v}" + + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "PRERELEASE=false" >> "$GITHUB_OUTPUT" + else + echo "PRERELEASE=true" >> "$GITHUB_OUTPUT" + fi - name: Release uses: softprops/action-gh-release@v3 @@ -88,3 +98,4 @@ jobs: dist/*.tar.gz dist/*.whl generate_release_notes: true + prerelease: ${{ steps.version.outputs.PRERELEASE }} From a66a0e84a4cff33dd926f0b5891b7ec029ec6f33 Mon Sep 17 00:00:00 2001 From: Sebastiano Taddei <61469576+SebastianoTaddei@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:24:31 +0200 Subject: [PATCH 26/26] feat: bumped version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 20bc09a1..9a13b455 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "uv_build" [project] name = "nnodely" -version = "1.5.4" +version = "1.5.5.dev1" description = "Model-structured neural network framework for the modeling and control of physical systems" readme = "README.md" requires-python = ">=3.10,<3.14" diff --git a/uv.lock b/uv.lock index f9643e26..20129039 100644 --- a/uv.lock +++ b/uv.lock @@ -803,7 +803,7 @@ wheels = [ [[package]] name = "nnodely" -version = "1.5.4" +version = "1.5.5.dev1" source = { editable = "." } dependencies = [ { name = "graphviz" },