nnodely 1.5.5.dev1__tar.gz → 1.5.5.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/PKG-INFO +2 -2
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/pyproject.toml +15 -15
- nnodely-1.5.5.dev2/src/nnodely/layers/localmodel.py +197 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/jsonutils.py +191 -114
- nnodely-1.5.5.dev1/src/nnodely/layers/localmodel.py +0 -130
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/LICENSE +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/README.md +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/loss.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/model.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/modeldef.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/optimizer.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/relation.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/emptyexporter.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/export.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/reporter.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/standardexporter.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/activation.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/arithmetic.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/equationlearner.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/fir.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/fuzzify.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/input.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/interpolation.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/linear.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/neuralODE.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/output.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/parameter.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/parametricfunction.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/part.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/rungekutta.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/timeoperation.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/trigonometric.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/nnodely.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/composer.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/exporter.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/loader.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/network.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/trainer.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/validator.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/earlystopping.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/fixstepsolver.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/initializer.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/logger.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/mathutils.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/adjoint.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/dopri5.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/fixed_grid.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/my_odeint.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/rk_solvers.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/solvers.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/utils.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/utils.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/__init__.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/functionplot.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/resultsplot.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/trainingplot.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/emptyvisualizer.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/mplnotebookvisualizer.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/mplvisualizer.py +0 -0
- {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/textvisualizer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nnodely
|
|
3
|
-
Version: 1.5.5.
|
|
3
|
+
Version: 1.5.5.dev2
|
|
4
4
|
Summary: Model-structured neural network framework for the modeling and control of physical systems
|
|
5
5
|
Author: Gastone Pietro Rosati Papini
|
|
6
6
|
Author-email: Gastone Pietro Rosati Papini <tonegas@gmail.com>
|
|
@@ -18,7 +18,7 @@ Requires-Dist: reportlab
|
|
|
18
18
|
Requires-Dist: matplotlib
|
|
19
19
|
Requires-Dist: onnxruntime
|
|
20
20
|
Requires-Dist: graphviz
|
|
21
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.11, <3.14
|
|
22
22
|
Project-URL: Homepage, https://github.com/tonegas/nnodely
|
|
23
23
|
Project-URL: Repository, https://github.com/tonegas/nnodely
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
@@ -4,30 +4,30 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nnodely"
|
|
7
|
-
version = "1.5.5.
|
|
7
|
+
version = "1.5.5.dev2"
|
|
8
8
|
description = "Model-structured neural network framework for the modeling and control of physical systems"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11,<3.14"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [
|
|
14
|
-
|
|
14
|
+
{ name = "Gastone Pietro Rosati Papini", email = "tonegas@gmail.com" },
|
|
15
15
|
]
|
|
16
16
|
classifiers = [
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
"numpy == 1.26.4; platform_machine == 'x86_64' and python_version == '3.10'",
|
|
22
|
+
"torch == 2.2.2; platform_machine == 'x86_64' and python_version == '3.10'",
|
|
23
|
+
"torch == 2.6.0; platform_machine != 'x86_64' or python_version != '3.10'",
|
|
24
|
+
"numpy",
|
|
25
|
+
"onnx",
|
|
26
|
+
"pandas",
|
|
27
|
+
"reportlab",
|
|
28
|
+
"matplotlib",
|
|
29
|
+
"onnxruntime",
|
|
30
|
+
"graphviz",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from nnodely.basic.relation import NeuObj, Stream
|
|
6
|
+
from nnodely.layers.arithmetic import add_relation_name
|
|
7
|
+
from nnodely.layers.part import Select
|
|
8
|
+
from nnodely.support.jsonutils import merge
|
|
9
|
+
from nnodely.support.utils import check, enforce_types
|
|
10
|
+
|
|
11
|
+
localmodel_relation_name = "LocalModel"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _signature_len(fn) -> int:
|
|
15
|
+
return len(inspect.signature(fn).parameters)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _apply(fn, x):
|
|
19
|
+
"""Invoke ``fn`` on a Stream or on the unpacked elements of a tuple."""
|
|
20
|
+
return fn(*x) if type(x) is tuple else fn(x)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LocalModel(NeuObj):
|
|
24
|
+
"""
|
|
25
|
+
Represents a Local Model relation in the neural network model.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
input_function : Callable, optional
|
|
30
|
+
A callable function to process the inputs.
|
|
31
|
+
output_function : Callable, optional
|
|
32
|
+
A callable function to process the outputs.
|
|
33
|
+
pass_indexes : bool, optional
|
|
34
|
+
A boolean indicating whether to pass indexes to the functions. Default is False.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
relation_name : str
|
|
39
|
+
The name of the relation.
|
|
40
|
+
pass_indexes : bool
|
|
41
|
+
A boolean indicating whether to pass indexes to the functions.
|
|
42
|
+
input_function : Callable
|
|
43
|
+
The function to process the inputs.
|
|
44
|
+
output_function : Callable
|
|
45
|
+
The function to process the outputs.
|
|
46
|
+
|
|
47
|
+
Examples
|
|
48
|
+
--------
|
|
49
|
+
|
|
50
|
+
.. include:: /examples_basics/layer_module_ex/localmodel.rst
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@enforce_types
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
input_function: Callable | None = None,
|
|
57
|
+
output_function: Callable | None = None,
|
|
58
|
+
*,
|
|
59
|
+
pass_indexes: bool = False,
|
|
60
|
+
):
|
|
61
|
+
self.relation_name = localmodel_relation_name
|
|
62
|
+
self.pass_indexes = pass_indexes
|
|
63
|
+
self.input_function = input_function
|
|
64
|
+
self.output_function = output_function
|
|
65
|
+
super().__init__(localmodel_relation_name + str(NeuObj.count))
|
|
66
|
+
self.json["Functions"][self.name] = {}
|
|
67
|
+
|
|
68
|
+
@enforce_types
|
|
69
|
+
def __call__(self, inputs: Stream | tuple, activations: Stream | tuple = None):
|
|
70
|
+
if type(activations) is not tuple:
|
|
71
|
+
activations = (activations,)
|
|
72
|
+
|
|
73
|
+
in_func = self.input_function
|
|
74
|
+
check(
|
|
75
|
+
in_func is not None or type(inputs) is not tuple,
|
|
76
|
+
TypeError,
|
|
77
|
+
"The input cannot be a tuple without input_function",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# ``input_function`` output is reusable across cells iff the same
|
|
81
|
+
# callable is invoked with the same arguments for every cell:
|
|
82
|
+
# ``pass_indexes`` False and not a zero-arg factory.
|
|
83
|
+
shared_out_in = None
|
|
84
|
+
if (
|
|
85
|
+
in_func is not None
|
|
86
|
+
and not self.pass_indexes
|
|
87
|
+
and _signature_len(in_func) > 0
|
|
88
|
+
):
|
|
89
|
+
shared_out_in = _apply(in_func, inputs)
|
|
90
|
+
|
|
91
|
+
select_cache: dict[tuple[int, int], Stream] = {}
|
|
92
|
+
|
|
93
|
+
def cached_select(act_idx: int, i: int) -> Stream:
|
|
94
|
+
cached = select_cache.get((act_idx, i))
|
|
95
|
+
if cached is None:
|
|
96
|
+
cached = Select(activations[act_idx], i)
|
|
97
|
+
select_cache[(act_idx, i)] = cached
|
|
98
|
+
return cached
|
|
99
|
+
|
|
100
|
+
cells: list[Stream] = []
|
|
101
|
+
self._build_cells(
|
|
102
|
+
activations,
|
|
103
|
+
inputs,
|
|
104
|
+
cells,
|
|
105
|
+
cached_select,
|
|
106
|
+
shared_out_in,
|
|
107
|
+
prefix=None,
|
|
108
|
+
idx_list=[],
|
|
109
|
+
depth=0,
|
|
110
|
+
)
|
|
111
|
+
return self._nary_add(cells)
|
|
112
|
+
|
|
113
|
+
def _build_cells(
|
|
114
|
+
self,
|
|
115
|
+
activations,
|
|
116
|
+
inputs,
|
|
117
|
+
cells,
|
|
118
|
+
cached_select,
|
|
119
|
+
shared_out_in,
|
|
120
|
+
*,
|
|
121
|
+
prefix,
|
|
122
|
+
idx_list,
|
|
123
|
+
depth,
|
|
124
|
+
):
|
|
125
|
+
# ``prefix`` is the cached product of Selects for indices [0..depth);
|
|
126
|
+
# sibling subtrees reuse the same Stream, turning the per-cell K-1
|
|
127
|
+
# chain of activation muls into an incremental tree build.
|
|
128
|
+
if depth == len(activations):
|
|
129
|
+
out_in = (
|
|
130
|
+
shared_out_in
|
|
131
|
+
if shared_out_in is not None
|
|
132
|
+
else self._apply_fn(
|
|
133
|
+
self.input_function,
|
|
134
|
+
inputs,
|
|
135
|
+
idx_list,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
cells.append(
|
|
139
|
+
self._apply_fn(
|
|
140
|
+
self.output_function,
|
|
141
|
+
out_in * prefix,
|
|
142
|
+
idx_list,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
for i in range(activations[depth].dim["dim"]):
|
|
148
|
+
sel = cached_select(depth, i)
|
|
149
|
+
new_prefix = sel if prefix is None else prefix * sel
|
|
150
|
+
self._build_cells(
|
|
151
|
+
activations,
|
|
152
|
+
inputs,
|
|
153
|
+
cells,
|
|
154
|
+
cached_select,
|
|
155
|
+
shared_out_in,
|
|
156
|
+
prefix=new_prefix,
|
|
157
|
+
idx_list=idx_list + [i],
|
|
158
|
+
depth=depth + 1,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _apply_fn(self, fn, x, idx_list):
|
|
162
|
+
# Dispatch on the user function's signature:
|
|
163
|
+
# zero-arg ``fn`` is a factory producing a fresh cell callable;
|
|
164
|
+
# ``pass_indexes`` makes the factory idx-dependent; otherwise ``fn``
|
|
165
|
+
# is already the cell callable (shared params across cells).
|
|
166
|
+
if fn is None:
|
|
167
|
+
return x
|
|
168
|
+
if _signature_len(fn) == 0:
|
|
169
|
+
cell_fn = fn()
|
|
170
|
+
elif self.pass_indexes:
|
|
171
|
+
cell_fn = fn(idx_list)
|
|
172
|
+
else:
|
|
173
|
+
cell_fn = fn
|
|
174
|
+
return _apply(cell_fn, x)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _nary_add(cells: list[Stream]) -> Stream:
|
|
178
|
+
# Equivalent to ``cells[0] + cells[1] + ... + cells[-1]``, but folded
|
|
179
|
+
# into a single ``Add`` relation. ``Add_Layer.forward(*inputs)`` does
|
|
180
|
+
# the same left-to-right fold at runtime, so the float result is
|
|
181
|
+
# bit-exact to the chained binary version while the build avoids
|
|
182
|
+
# ``N-1`` intermediate Streams (each of which would deep-copy a
|
|
183
|
+
# growing JSON).
|
|
184
|
+
if len(cells) == 1:
|
|
185
|
+
return cells[0]
|
|
186
|
+
|
|
187
|
+
combined = cells[0].json
|
|
188
|
+
for c in cells[1:]:
|
|
189
|
+
combined = merge(combined, c.json)
|
|
190
|
+
|
|
191
|
+
name = add_relation_name + str(Stream.count)
|
|
192
|
+
new_stream = Stream(name, combined, cells[0].dim)
|
|
193
|
+
new_stream.json["Relations"][name] = [
|
|
194
|
+
add_relation_name,
|
|
195
|
+
[c.name for c in cells],
|
|
196
|
+
]
|
|
197
|
+
return new_stream
|
|
@@ -1,94 +1,172 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
from pprint import pformat
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
from nnodely.support.utils import check
|
|
6
|
-
|
|
7
5
|
from nnodely.support.logger import logging, nnLogger
|
|
8
6
|
|
|
9
7
|
log = nnLogger(__name__, logging.WARNING)
|
|
10
8
|
|
|
9
|
+
# Sections whose inner *entries* are mutated in place by callers after merge() and
|
|
10
|
+
# therefore must be detached (shallow-copied) in the result:
|
|
11
|
+
# Inputs -> connect / closedLoop / local / ns / ntot
|
|
12
|
+
# Functions -> Fuzzify / LocalModel / dim_out / params_and_consts
|
|
13
|
+
# Parameters -> values / init_values / init_fun / dim
|
|
14
|
+
# Constants -> values
|
|
15
|
+
# All other sections (Relations / Outputs / Models / Minimizers) are append-only at
|
|
16
|
+
# the section level, so their inner values are shared between operands and result.
|
|
17
|
+
_MUTABLE_ENTRY_SECTIONS = frozenset(("Inputs", "Functions", "Parameters", "Constants"))
|
|
18
|
+
|
|
11
19
|
|
|
12
20
|
def get_window(obj):
|
|
21
|
+
"""Return the window key (``'tw'``/``'sw'``) of ``obj.dim``, or ``None``."""
|
|
13
22
|
return "tw" if "tw" in obj.dim else ("sw" if "sw" in obj.dim else None)
|
|
14
23
|
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# del self.json['Relations'][obj1.name]
|
|
22
|
-
# else:
|
|
23
|
-
# Devo aggiungere un operazione che rimuove un operazione di Add,Sub,Mul,Div se può essere unita ad un'altra operazione dello stesso tipo
|
|
24
|
-
#
|
|
25
|
-
def merge(source, destination, main=True):
|
|
26
|
-
if main:
|
|
27
|
-
for key, value in destination["Functions"].items():
|
|
28
|
-
if (
|
|
29
|
-
key in source["Functions"].keys()
|
|
30
|
-
and "n_input" in value.keys()
|
|
31
|
-
and "n_input" in source["Functions"][key].keys()
|
|
32
|
-
):
|
|
33
|
-
check(
|
|
34
|
-
value == {}
|
|
35
|
-
or source["Functions"][key] == {}
|
|
36
|
-
or value["n_input"] == source["Functions"][key]["n_input"],
|
|
37
|
-
TypeError,
|
|
38
|
-
f"The ParamFun {key} is present multiple times, with different number of inputs. "
|
|
39
|
-
f"The ParamFun {key} is called with {value['n_input']} parameters and with {source['Functions'][key]['n_input']} parameters.",
|
|
40
|
-
)
|
|
41
|
-
for key, value in destination["Parameters"].items():
|
|
42
|
-
if key in source["Parameters"].keys():
|
|
43
|
-
if "dim" in value.keys() and "dim" in source["Parameters"][key].keys():
|
|
44
|
-
check(
|
|
45
|
-
value["dim"] == source["Parameters"][key]["dim"],
|
|
46
|
-
TypeError,
|
|
47
|
-
f"The Parameter {key} is present multiple times, with different dimensions. "
|
|
48
|
-
f"The Parameter {key} is called with {value['dim']} dimension and with {source['Parameters'][key]['dim']} dimension.",
|
|
49
|
-
)
|
|
50
|
-
window_dest = (
|
|
51
|
-
"tw" if "tw" in value else ("sw" if "sw" in value else None)
|
|
52
|
-
)
|
|
53
|
-
window_source = (
|
|
54
|
-
"tw"
|
|
55
|
-
if "tw" in source["Parameters"][key]
|
|
56
|
-
else ("sw" if "sw" in source["Parameters"][key] else None)
|
|
57
|
-
)
|
|
58
|
-
if window_dest is not None:
|
|
59
|
-
check(
|
|
60
|
-
window_dest == window_source
|
|
61
|
-
and value[window_dest]
|
|
62
|
-
== source["Parameters"][key][window_source],
|
|
63
|
-
TypeError,
|
|
64
|
-
f"The Parameter {key} is present multiple times, with different window. "
|
|
65
|
-
f"The Parameter {key} is called with {window_dest}={value[window_dest]} dimension and with {window_source}={source['Parameters'][key][window_source]} dimension.",
|
|
66
|
-
)
|
|
25
|
+
def _shallow_copy_entries(d):
|
|
26
|
+
"""Return a fresh dict where dict-typed values are shallow-copied."""
|
|
27
|
+
if not d:
|
|
28
|
+
return {}
|
|
29
|
+
return {k: dict(v) if isinstance(v, dict) else v for k, v in d.items()}
|
|
67
30
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
31
|
+
|
|
32
|
+
def _window_union(a, b):
|
|
33
|
+
"""Return ``[min(a[0], b[0]), max(a[1], b[1])]`` without mutating *a* or *b*."""
|
|
34
|
+
return [a[0] if a[0] <= b[0] else b[0], a[1] if a[1] >= b[1] else b[1]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _overlay_with_windows(dst, src):
|
|
38
|
+
"""Overlay *src* onto a fresh copy of *dst*; union ``tw``/``sw`` lists.
|
|
39
|
+
|
|
40
|
+
Used for the ``Info`` section and for individual ``Inputs`` descriptors,
|
|
41
|
+
which share the same shape (flat dict of scalars/2-element windows).
|
|
42
|
+
"""
|
|
43
|
+
out = dict(dst) if isinstance(dst, dict) else {}
|
|
44
|
+
if not isinstance(src, dict) or not src:
|
|
45
|
+
return out
|
|
46
|
+
for k, v in src.items():
|
|
47
|
+
if (
|
|
48
|
+
(k == "tw" or k == "sw")
|
|
49
|
+
and isinstance(v, list)
|
|
50
|
+
and isinstance(out.get(k), list)
|
|
51
|
+
):
|
|
52
|
+
out[k] = _window_union(out[k], v)
|
|
80
53
|
else:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
54
|
+
out[k] = v
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _merge_section(sec, dst, src):
|
|
59
|
+
"""Merge a single named section of the model JSON.
|
|
60
|
+
|
|
61
|
+
Three regimes:
|
|
62
|
+
* ``Info`` — flat dict: scalar overlay + ``tw``/``sw`` window union.
|
|
63
|
+
* ``_MUTABLE_ENTRY_SECTIONS`` — union of named entries; each surviving
|
|
64
|
+
entry is shallow-copied so callers can mutate the result.
|
|
65
|
+
``Inputs`` additionally union'es per-entry windows.
|
|
66
|
+
* Everything else (``Relations`` / ``Outputs`` / ``Models`` / ``Minimizers``):
|
|
67
|
+
union of named entries; inner values are *shared* with the operands
|
|
68
|
+
(callers only add new keys, never mutate inner values in place).
|
|
69
|
+
"""
|
|
70
|
+
if sec == "Info":
|
|
71
|
+
return _overlay_with_windows(dst, src)
|
|
72
|
+
|
|
73
|
+
if sec in _MUTABLE_ENTRY_SECTIONS:
|
|
74
|
+
out = _shallow_copy_entries(dst)
|
|
75
|
+
if not src:
|
|
76
|
+
return out
|
|
77
|
+
if sec == "Inputs":
|
|
78
|
+
for k, sv in src.items():
|
|
79
|
+
out[k] = _overlay_with_windows(out.get(k), sv)
|
|
80
|
+
else:
|
|
81
|
+
for k, sv in src.items():
|
|
82
|
+
out[k] = dict(sv) if isinstance(sv, dict) else sv
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
# Shared-value sections: dict-union if both are dicts, else source overrides.
|
|
86
|
+
if isinstance(dst, dict) and isinstance(src, dict):
|
|
87
|
+
out = dict(dst)
|
|
88
|
+
out.update(src)
|
|
89
|
+
return out
|
|
90
|
+
if isinstance(src, dict):
|
|
91
|
+
return dict(src)
|
|
92
|
+
if isinstance(dst, dict):
|
|
93
|
+
return dict(dst)
|
|
94
|
+
return src if src is not None else dst
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _iter_overlap(a, b):
|
|
98
|
+
"""Yield ``(key, a[key], b[key])`` for keys present in both dicts, scanning
|
|
99
|
+
the smaller side."""
|
|
100
|
+
if not a or not b:
|
|
101
|
+
return
|
|
102
|
+
small, big = (a, b) if len(a) <= len(b) else (b, a)
|
|
103
|
+
for key, value in small.items():
|
|
104
|
+
other = big.get(key)
|
|
105
|
+
if other is not None:
|
|
106
|
+
yield key, value, other
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _validate_compat(source, destination):
|
|
110
|
+
"""Verify that overlapping Functions/Parameters agree on their declared shape
|
|
111
|
+
(``n_input``, ``dim``, ``tw``/``sw``)."""
|
|
112
|
+
for key, a, b in _iter_overlap(
|
|
113
|
+
source.get("Functions") or {}, destination.get("Functions") or {}
|
|
114
|
+
):
|
|
115
|
+
if a and b and "n_input" in a and "n_input" in b:
|
|
116
|
+
check(
|
|
117
|
+
a["n_input"] == b["n_input"],
|
|
118
|
+
TypeError,
|
|
119
|
+
f"The ParamFun {key} is present multiple times, with different number of inputs. "
|
|
120
|
+
f"The ParamFun {key} is called with {a['n_input']} parameters and with {b['n_input']} parameters.",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for key, a, b in _iter_overlap(
|
|
124
|
+
source.get("Parameters") or {}, destination.get("Parameters") or {}
|
|
125
|
+
):
|
|
126
|
+
if "dim" in a and "dim" in b:
|
|
127
|
+
check(
|
|
128
|
+
a["dim"] == b["dim"],
|
|
129
|
+
TypeError,
|
|
130
|
+
f"The Parameter {key} is present multiple times, with different dimensions. "
|
|
131
|
+
f"The Parameter {key} is called with {a['dim']} dimension and with {b['dim']} dimension.",
|
|
132
|
+
)
|
|
133
|
+
wa = "tw" if "tw" in a else ("sw" if "sw" in a else None)
|
|
134
|
+
if wa is None:
|
|
135
|
+
continue
|
|
136
|
+
wb = "tw" if "tw" in b else ("sw" if "sw" in b else None)
|
|
137
|
+
check(
|
|
138
|
+
wa == wb and a[wa] == b[wb],
|
|
139
|
+
TypeError,
|
|
140
|
+
f"The Parameter {key} is present multiple times, with different window. "
|
|
141
|
+
f"The Parameter {key} is called with {wa}={a[wa]} dimension and with {wb}={b[wb] if wb else None} dimension.",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def merge(source, destination):
|
|
146
|
+
"""
|
|
147
|
+
Combine two model JSONs into a fresh, independent dict.
|
|
148
|
+
|
|
149
|
+
Complexity: ``O(|sections| + |mutable entries| + |new keys in source|)``
|
|
150
|
+
per call. No recursion, no full deep-copy of *destination*. Inner values
|
|
151
|
+
of named sections are shared with the operands wherever safe; entries in
|
|
152
|
+
``Inputs``/``Functions``/``Parameters``/``Constants`` are shallow-copied
|
|
153
|
+
because callers mutate them in place. ``tw``/``sw`` ranges are union'd.
|
|
154
|
+
"""
|
|
155
|
+
if source is destination:
|
|
156
|
+
return {sec: _merge_section(sec, v, None) for sec, v in destination.items()}
|
|
157
|
+
|
|
158
|
+
_validate_compat(source, destination)
|
|
159
|
+
|
|
160
|
+
sections = set(destination) | set(source)
|
|
161
|
+
result = {
|
|
162
|
+
sec: _merge_section(sec, destination.get(sec), source.get(sec))
|
|
163
|
+
for sec in sections
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if log.isEnabledFor(logging.DEBUG):
|
|
167
|
+
log.debug("Merge Source\n" + pformat(source))
|
|
168
|
+
log.debug("Merge Destination\n" + pformat(destination))
|
|
169
|
+
log.debug("Merge Result\n" + pformat(result))
|
|
92
170
|
return result
|
|
93
171
|
|
|
94
172
|
|
|
@@ -163,52 +241,54 @@ def binary_cheks(self, obj1, obj2, name):
|
|
|
163
241
|
|
|
164
242
|
|
|
165
243
|
def subjson_from_relation(json, relation):
|
|
166
|
-
|
|
167
|
-
#
|
|
244
|
+
# Read-only DAG walk with iterative DFS + visited memo (relations form a DAG with
|
|
245
|
+
# heavy reuse, e.g. RK4 expansions: recursive untracked walks blow up exponentially).
|
|
168
246
|
inputs = set()
|
|
169
247
|
relations = set()
|
|
170
248
|
constants = set()
|
|
171
249
|
parameters = set()
|
|
172
250
|
functions = set()
|
|
173
251
|
|
|
174
|
-
|
|
175
|
-
|
|
252
|
+
j_inputs = json["Inputs"]
|
|
253
|
+
j_constants = json["Constants"]
|
|
254
|
+
j_parameters = json["Parameters"]
|
|
255
|
+
j_functions = json["Functions"]
|
|
256
|
+
j_relations = json["Relations"]
|
|
257
|
+
|
|
258
|
+
visited = set()
|
|
259
|
+
stack = [relation]
|
|
260
|
+
while stack:
|
|
261
|
+
rel = stack.pop()
|
|
262
|
+
if rel in visited:
|
|
263
|
+
continue
|
|
264
|
+
visited.add(rel)
|
|
265
|
+
if rel in j_inputs:
|
|
176
266
|
inputs.add(rel)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
"closed_loop" in json["Inputs"][rel]
|
|
185
|
-
and json["Inputs"][rel]["local"] == 1
|
|
186
|
-
):
|
|
187
|
-
search(json["Inputs"][rel]["closed_loop"])
|
|
188
|
-
# if 'init' in json['Inputs'][rel]:
|
|
189
|
-
# search(json['Inputs'][rel]['init'])
|
|
190
|
-
elif rel in json["Constants"]: # Found a constant or parameter
|
|
267
|
+
entry = j_inputs[rel]
|
|
268
|
+
if "connect" in entry and entry.get("local") == 1:
|
|
269
|
+
stack.append(entry["connect"])
|
|
270
|
+
if "closed_loop" in entry and entry.get("local") == 1:
|
|
271
|
+
stack.append(entry["closed_loop"])
|
|
272
|
+
elif rel in j_constants:
|
|
191
273
|
constants.add(rel)
|
|
192
|
-
elif rel in
|
|
274
|
+
elif rel in j_parameters:
|
|
193
275
|
parameters.add(rel)
|
|
194
|
-
elif rel in
|
|
276
|
+
elif rel in j_functions:
|
|
195
277
|
functions.add(rel)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
278
|
+
f_entry = j_functions[rel]
|
|
279
|
+
pcs = (
|
|
280
|
+
f_entry.get("params_and_consts") if isinstance(f_entry, dict) else None
|
|
281
|
+
)
|
|
282
|
+
if pcs:
|
|
283
|
+
stack.extend(pcs)
|
|
284
|
+
elif rel in j_relations:
|
|
200
285
|
relations.add(rel)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
search(sub_rel)
|
|
208
|
-
if json["Relations"][rel][0] in ("ParamFun"):
|
|
209
|
-
search(sub_rel)
|
|
210
|
-
|
|
211
|
-
search(relation)
|
|
286
|
+
r_entry = j_relations[rel]
|
|
287
|
+
stack.extend(r_entry[1])
|
|
288
|
+
kind = r_entry[0]
|
|
289
|
+
if kind in ("Fir", "Linear", "Fuzzify", "ParamFun") and len(r_entry) > 2:
|
|
290
|
+
stack.extend(r_entry[2:])
|
|
291
|
+
|
|
212
292
|
from nnodely.basic.relation import MAIN_JSON
|
|
213
293
|
|
|
214
294
|
sub_json = copy.deepcopy(MAIN_JSON)
|
|
@@ -233,7 +313,6 @@ def subjson_from_relation(json, relation):
|
|
|
233
313
|
|
|
234
314
|
|
|
235
315
|
def subjson_from_output(json, outputs: str | list):
|
|
236
|
-
json = copy.deepcopy(json)
|
|
237
316
|
from nnodely.basic.relation import MAIN_JSON
|
|
238
317
|
|
|
239
318
|
sub_json = copy.deepcopy(MAIN_JSON)
|
|
@@ -248,7 +327,6 @@ def subjson_from_output(json, outputs: str | list):
|
|
|
248
327
|
def subjson_from_model(json, models: str | list):
|
|
249
328
|
from nnodely.basic.relation import MAIN_JSON
|
|
250
329
|
|
|
251
|
-
json = copy.deepcopy(json)
|
|
252
330
|
sub_json = copy.deepcopy(MAIN_JSON)
|
|
253
331
|
models_names = (
|
|
254
332
|
set([json["Models"]])
|
|
@@ -301,7 +379,6 @@ def subjson_from_model(json, models: str | list):
|
|
|
301
379
|
def subjson_from_minimize(json, minimizers: str | list):
|
|
302
380
|
from nnodely.basic.relation import MAIN_JSON
|
|
303
381
|
|
|
304
|
-
json = copy.deepcopy(json)
|
|
305
382
|
sub_json = copy.deepcopy(MAIN_JSON)
|
|
306
383
|
|
|
307
384
|
if "Minimizers" in json:
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
|
|
5
|
-
from nnodely.basic.relation import NeuObj, Stream
|
|
6
|
-
from nnodely.layers.part import Select
|
|
7
|
-
from nnodely.support.utils import check, enforce_types
|
|
8
|
-
|
|
9
|
-
localmodel_relation_name = "LocalModel"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class LocalModel(NeuObj):
|
|
13
|
-
"""
|
|
14
|
-
Represents a Local Model relation in the neural network model.
|
|
15
|
-
|
|
16
|
-
Parameters
|
|
17
|
-
----------
|
|
18
|
-
input_function : Callable, optional
|
|
19
|
-
A callable function to process the inputs.
|
|
20
|
-
output_function : Callable, optional
|
|
21
|
-
A callable function to process the outputs.
|
|
22
|
-
pass_indexes : bool, optional
|
|
23
|
-
A boolean indicating whether to pass indexes to the functions. Default is False.
|
|
24
|
-
|
|
25
|
-
Attributes
|
|
26
|
-
----------
|
|
27
|
-
relation_name : str
|
|
28
|
-
The name of the relation.
|
|
29
|
-
pass_indexes : bool
|
|
30
|
-
A boolean indicating whether to pass indexes to the functions.
|
|
31
|
-
input_function : Callable
|
|
32
|
-
The function to process the inputs.
|
|
33
|
-
output_function : Callable
|
|
34
|
-
The function to process the outputs.
|
|
35
|
-
|
|
36
|
-
Examples
|
|
37
|
-
--------
|
|
38
|
-
|
|
39
|
-
.. include:: /examples_basics/layer_module_ex/localmodel.rst
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
@enforce_types
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
input_function: Callable | None = None,
|
|
46
|
-
output_function: Callable | None = None,
|
|
47
|
-
*,
|
|
48
|
-
pass_indexes: bool = False,
|
|
49
|
-
):
|
|
50
|
-
|
|
51
|
-
self.relation_name = localmodel_relation_name
|
|
52
|
-
self.pass_indexes = pass_indexes
|
|
53
|
-
super().__init__(localmodel_relation_name + str(NeuObj.count))
|
|
54
|
-
self.json["Functions"][self.name] = {}
|
|
55
|
-
if input_function is not None:
|
|
56
|
-
check(
|
|
57
|
-
callable(input_function),
|
|
58
|
-
TypeError,
|
|
59
|
-
"The input_function must be callable",
|
|
60
|
-
)
|
|
61
|
-
self.input_function = input_function
|
|
62
|
-
if output_function is not None:
|
|
63
|
-
check(
|
|
64
|
-
callable(output_function),
|
|
65
|
-
TypeError,
|
|
66
|
-
"The output_function must be callable",
|
|
67
|
-
)
|
|
68
|
-
self.output_function = output_function
|
|
69
|
-
|
|
70
|
-
@enforce_types
|
|
71
|
-
def __call__(self, inputs: Stream | tuple, activations: Stream | tuple = None):
|
|
72
|
-
out_sum = []
|
|
73
|
-
if type(activations) is not tuple:
|
|
74
|
-
activations = (activations,)
|
|
75
|
-
self.___activations_matrix(activations, inputs, out_sum)
|
|
76
|
-
|
|
77
|
-
out = out_sum[0]
|
|
78
|
-
for ind in range(1, len(out_sum)):
|
|
79
|
-
out = out + out_sum[ind]
|
|
80
|
-
return out
|
|
81
|
-
|
|
82
|
-
# Definisci una funzione ricorsiva per annidare i cicli for
|
|
83
|
-
def ___activations_matrix(self, activations, inputs, out, idx=0, idx_list=[]):
|
|
84
|
-
if idx != len(activations):
|
|
85
|
-
for i in range(activations[idx].dim["dim"]):
|
|
86
|
-
self.___activations_matrix(
|
|
87
|
-
activations, inputs, out, idx + 1, idx_list + [i]
|
|
88
|
-
)
|
|
89
|
-
else:
|
|
90
|
-
if self.input_function is not None:
|
|
91
|
-
if len(inspect.signature(self.input_function).parameters) == 0:
|
|
92
|
-
if type(inputs) is tuple:
|
|
93
|
-
out_in = self.input_function()(*inputs)
|
|
94
|
-
else:
|
|
95
|
-
out_in = self.input_function()(inputs)
|
|
96
|
-
else:
|
|
97
|
-
if self.pass_indexes:
|
|
98
|
-
if type(inputs) is tuple:
|
|
99
|
-
out_in = self.input_function(idx_list)(*inputs)
|
|
100
|
-
else:
|
|
101
|
-
out_in = self.input_function(idx_list)(inputs)
|
|
102
|
-
else:
|
|
103
|
-
if type(inputs) is tuple:
|
|
104
|
-
out_in = self.input_function(*inputs)
|
|
105
|
-
else:
|
|
106
|
-
out_in = self.input_function(inputs)
|
|
107
|
-
else:
|
|
108
|
-
check(
|
|
109
|
-
type(inputs) is not tuple,
|
|
110
|
-
TypeError,
|
|
111
|
-
"The input cannot be a tuple without input_function",
|
|
112
|
-
)
|
|
113
|
-
out_in = inputs
|
|
114
|
-
|
|
115
|
-
act = Select(activations[0], idx_list[0])
|
|
116
|
-
for ind, i in enumerate(idx_list[1:]):
|
|
117
|
-
act = act * Select(activations[ind + 1], i)
|
|
118
|
-
|
|
119
|
-
prod = out_in * act
|
|
120
|
-
|
|
121
|
-
if self.output_function is not None:
|
|
122
|
-
if len(inspect.signature(self.output_function).parameters) == 0:
|
|
123
|
-
out.append(self.output_function()(prod))
|
|
124
|
-
else:
|
|
125
|
-
if self.pass_indexes:
|
|
126
|
-
out.append(self.output_function(idx_list)(prod))
|
|
127
|
-
else:
|
|
128
|
-
out.append(self.output_function(prod))
|
|
129
|
-
else:
|
|
130
|
-
out.append(prod)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|