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.
Files changed (68) hide show
  1. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/PKG-INFO +2 -2
  2. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/pyproject.toml +15 -15
  3. nnodely-1.5.5.dev2/src/nnodely/layers/localmodel.py +197 -0
  4. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/jsonutils.py +191 -114
  5. nnodely-1.5.5.dev1/src/nnodely/layers/localmodel.py +0 -130
  6. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/LICENSE +0 -0
  7. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/README.md +0 -0
  8. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/__init__.py +0 -0
  9. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/__init__.py +0 -0
  10. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/loss.py +0 -0
  11. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/model.py +0 -0
  12. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/modeldef.py +0 -0
  13. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/optimizer.py +0 -0
  14. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/basic/relation.py +0 -0
  15. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/__init__.py +0 -0
  16. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/emptyexporter.py +0 -0
  17. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/export.py +0 -0
  18. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/reporter.py +0 -0
  19. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/exporter/standardexporter.py +0 -0
  20. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/__init__.py +0 -0
  21. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/activation.py +0 -0
  22. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/arithmetic.py +0 -0
  23. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/equationlearner.py +0 -0
  24. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/fir.py +0 -0
  25. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/fuzzify.py +0 -0
  26. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/input.py +0 -0
  27. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/interpolation.py +0 -0
  28. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/linear.py +0 -0
  29. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/neuralODE.py +0 -0
  30. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/output.py +0 -0
  31. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/parameter.py +0 -0
  32. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/parametricfunction.py +0 -0
  33. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/part.py +0 -0
  34. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/rungekutta.py +0 -0
  35. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/timeoperation.py +0 -0
  36. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/layers/trigonometric.py +0 -0
  37. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/nnodely.py +0 -0
  38. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/__init__.py +0 -0
  39. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/composer.py +0 -0
  40. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/exporter.py +0 -0
  41. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/loader.py +0 -0
  42. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/network.py +0 -0
  43. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/trainer.py +0 -0
  44. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/operators/validator.py +0 -0
  45. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/__init__.py +0 -0
  46. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/earlystopping.py +0 -0
  47. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/fixstepsolver.py +0 -0
  48. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/initializer.py +0 -0
  49. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/logger.py +0 -0
  50. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/mathutils.py +0 -0
  51. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/__init__.py +0 -0
  52. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/adjoint.py +0 -0
  53. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/dopri5.py +0 -0
  54. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/fixed_grid.py +0 -0
  55. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/my_odeint.py +0 -0
  56. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/rk_solvers.py +0 -0
  57. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/solvers.py +0 -0
  58. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/odeint/utils.py +0 -0
  59. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/support/utils.py +0 -0
  60. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/__init__.py +0 -0
  61. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/functionplot.py +0 -0
  62. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/fuzzyplot.py +0 -0
  63. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/resultsplot.py +0 -0
  64. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/dynamicmpl/trainingplot.py +0 -0
  65. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/emptyvisualizer.py +0 -0
  66. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/mplnotebookvisualizer.py +0 -0
  67. {nnodely-1.5.5.dev1 → nnodely-1.5.5.dev2}/src/nnodely/visualizer/mplvisualizer.py +0 -0
  68. {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.dev1
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.10, <3.14
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.dev1"
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,<3.14"
10
+ requires-python = ">=3.11,<3.14"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [
14
- { name = "Gastone Pietro Rosati Papini", email = "tonegas@gmail.com" },
14
+ { name = "Gastone Pietro Rosati Papini", email = "tonegas@gmail.com" },
15
15
  ]
16
16
  classifiers = [
17
- "Programming Language :: Python :: 3",
18
- "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
19
  ]
20
20
  dependencies = [
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",
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
- # Codice per comprimere le relazioni
17
- # print(self.json['Relations'])
18
- # used_rel = {string for values in self.json['Relations'].values() for string in values[1]}
19
- # 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:
20
- # self.json['Relations'][self.name] = [add_relation_name, self.json['Relations'][obj1.name][1]+[obj2.name]]
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
- log.debug("Merge Source")
69
- log.debug("\n" + pformat(source))
70
- log.debug("Merge Destination")
71
- log.debug("\n" + pformat(destination))
72
- result = copy.deepcopy(destination)
73
- else:
74
- result = destination
75
- for key, value in source.items():
76
- if isinstance(value, dict):
77
- # get node or create one
78
- node = result.setdefault(key, {})
79
- merge(value, node, False)
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
- if key in result and type(result[key]) is list:
82
- if key == "tw" or key == "sw":
83
- if result[key][0] > value[0]:
84
- result[key][0] = value[0]
85
- if result[key][1] < value[1]:
86
- result[key][1] = value[1]
87
- else:
88
- result[key] = value
89
- if main == True:
90
- log.debug("Merge Result")
91
- log.debug("\n" + pformat(result))
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
- json = copy.deepcopy(json)
167
- # Get all the inputs needed to compute a specific relation from the json graph
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
- def search(rel):
175
- if rel in json["Inputs"]: # Found an input
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
- if rel in json["Inputs"]:
178
- if (
179
- "connect" in json["Inputs"][rel]
180
- and json["Inputs"][rel]["local"] == 1
181
- ):
182
- search(json["Inputs"][rel]["connect"])
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 json["Parameters"]:
274
+ elif rel in j_parameters:
193
275
  parameters.add(rel)
194
- elif rel in json["Functions"]:
276
+ elif rel in j_functions:
195
277
  functions.add(rel)
196
- if "params_and_consts" in json["Functions"][rel]:
197
- for sub_rel in json["Functions"][rel]["params_and_consts"]:
198
- search(sub_rel)
199
- elif rel in json["Relations"]: # Another relation
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
- for sub_rel in json["Relations"][rel][1]:
202
- search(sub_rel)
203
- for sub_rel in json["Relations"][rel][2:]:
204
- if json["Relations"][rel][0] in ("Fir", "Linear"):
205
- search(sub_rel)
206
- if json["Relations"][rel][0] in ("Fuzzify"):
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