Coverage for wsimod\nodes\nodes.py: 31%
188 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-30 14:52 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-30 14:52 +0000
1# -*- coding: utf-8 -*-
2"""Created on Wed Apr 7 08:43:32 2021.
4@author: Barney
6Converted to totals on Thur Apr 21 2022
7"""
8import logging
9from typing import Any, Dict
11from wsimod.core import constants
12from wsimod.core.core import WSIObj
15class Node(WSIObj):
16 """"""
18 def __init_subclass__(cls, **kwargs):
19 """Adds all subclasses to the nodes registry."""
20 super().__init_subclass__(**kwargs)
21 if cls.__name__ in NODES_REGISTRY:
22 logging.warning(f"Overwriting {cls.__name__} in NODES_REGISTRY with {cls}")
24 NODES_REGISTRY[cls.__name__] = cls
26 def __init__(self, name, data_input_dict=None):
27 """Base class for CWSD nodes. Constructs all the necessary attributes for the
28 node object.
30 Args:
31 name (str): Name of node
32 data_input_dict (dict, optional): Dictionary of data inputs relevant for
33 the node. Keys are tuples where first value is the name of the
34 variable to read from the dict and the second value is the time.
35 Defaults to None.
37 Examples:
38 >>> my_node = nodes.Node(name = 'london_river_junction')
40 Key assumptions:
41 - No physical processes represented, can be used as a junction.
43 Input data and parameter requirements:
44 - All nodes require a `name`
45 """
46 node_types = list(NODES_REGISTRY.keys())
48 # Default essential parameters
49 # Dictionary of arcs
50 self.in_arcs = {}
51 self.out_arcs = {}
52 self.in_arcs_type = {x: {} for x in node_types}
53 self.out_arcs_type = {x: {} for x in node_types}
55 # Set parameters
56 self.name = name
57 self.t = None
58 self.data_input_dict = data_input_dict
60 # Initiailise default handlers
61 self.pull_set_handler = {"default": self.pull_distributed}
62 self.push_set_handler = {
63 "default": lambda x: self.push_distributed(
64 x, of_type=["Node", "River", "Waste", "Reservoir"]
65 )
66 }
67 self.pull_check_handler = {"default": self.pull_check_basic}
68 self.push_check_handler = {
69 "default": lambda x: self.push_check_basic(
70 x, of_type=["Node", "River", "Waste", "Reservoir"]
71 )
72 }
73 super().__init__()
75 # Mass balance checking
76 self.mass_balance_in = [self.total_in]
77 self.mass_balance_out = [self.total_out]
78 self.mass_balance_ds = [lambda: self.empty_vqip()]
80 def apply_overrides(self, overrides: Dict[str, Any] = {}) -> None:
81 """Apply overrides to the node.
83 The Node does not have any overwriteable parameters. So if any
84 overrides are passed up to the node, this means that there are unused
85 parameters from the Node subclass, which is flagged.
87 Args:
88 overrides (dict, optional): Dictionary of overrides. Defaults to {}.
89 """
90 # overrides data_input_dict
91 from wsimod.orchestration.model import read_csv
93 content = overrides.pop("data_input_dict", self.data_input_dict)
94 if isinstance(content, str):
95 self.data_input_dict = read_csv(content)
96 elif not content:
97 pass
98 else:
99 raise RuntimeError("Not recognised format for data_input_dict")
101 if len(overrides) > 0:
102 print(f"No override behaviour defined for: {overrides.keys()}")
104 def total_in(self):
105 """Sum flow and pollutant amounts entering a node via in_arcs.
107 Returns:
108 in_ (dict): Summed VQIP of in_arcs
110 Examples:
111 >>> node_inflow = my_node.total_in()
112 """
113 in_ = self.empty_vqip()
114 for arc in self.in_arcs.values():
115 in_ = self.sum_vqip(in_, arc.vqip_out)
117 return in_
119 def total_out(self):
120 """Sum flow and pollutant amounts leaving a node via out_arcs.
122 Returns:
123 out_ (dict): Summed VQIP of out_arcs
125 Examples:
126 >>> node_outflow = my_node.total_out()
127 """
128 out_ = self.empty_vqip()
129 for arc in self.out_arcs.values():
130 out_ = self.sum_vqip(out_, arc.vqip_in)
132 return out_
134 def node_mass_balance(self):
135 """Wrapper for core.py/WSIObj/mass_balance. Tracks change in mass balance.
137 Returns:
138 in_ (dict): A VQIP of the total from mass_balance_in functions
139 ds_ (dict): A VQIP of the total from mass_balance_ds functions
140 out_ (dict): A VQIP of the total from mass_balance_out functions
142 Examples:
143 >>> node_in, node_out, node_ds = my_node.node_mass_balance()
144 """
145 in_, ds_, out_ = self.mass_balance()
146 return in_, ds_, out_
148 def pull_set(self, vqip, tag="default"):
149 """Receives pull set requests from arcs and passes request to query handler.
151 Args:
152 vqip (dict): the VQIP pull request (by default, only the 'volume' key is
153 needed).
154 tag (str, optional): optional message to direct query_handler which pull
155 function to call. Defaults to 'default'.
157 Returns:
158 (dict): VQIP received from query_handler
160 Examples:
161 >>> water_received = my_node.pull_set({'volume' : 10})
162 """
163 return self.query_handler(self.pull_set_handler, vqip, tag)
165 def push_set(self, vqip, tag="default"):
166 """Receives push set requests from arcs and passes request to query handler.
168 Args:
169 vqip (_type_): the VQIP push request
170 tag (str, optional): optional message to direct query_handler which pull
171 function to call. Defaults to 'default'.
173 Returns:
174 (dict): VQIP not received from query_handler
176 Examples:
177 water_not_pushed = my_node.push_set(wastewater_vqip)
178 """
179 return self.query_handler(self.push_set_handler, vqip, tag)
181 def pull_check(self, vqip=None, tag="default"):
182 """Receives pull check requests from arcs and passes request to query handler.
184 Args:
185 vqip (dict, optional): the VQIP pull check (by default, only the
186 'volume' key is used). Defaults to None, which returns all available
187 water to pull.
188 tag (str, optional): optional message to direct query_handler which pull
189 function to call. Defaults to 'default'.
191 Returns:
192 (dict): VQIP available from query_handler
194 Examples:
195 >>> water_available = my_node.pull_check({'volume' : 10})
196 >>> total_water_available = my_node.pull_check()
197 """
198 return self.query_handler(self.pull_check_handler, vqip, tag)
200 def push_check(self, vqip=None, tag="default"):
201 """Receives push check requests from arcs and passes request to query handler.
203 Args:
204 vqip (dict, optional): the VQIP push check. Defaults to None, which
205 returns all available capacity to push
206 tag (str, optional): optional message to direct query_handler which pull
207 function to call. Defaults to 'default'
209 Returns:
210 (dict): VQIP available to push from query_handler
212 Examples:
213 >>> total_available_push_capacity = my_node.push_check()
214 >>> available_push_capacity = my_node.push_check(wastewater_vqip)
215 """
216 return self.query_handler(self.push_check_handler, vqip, tag)
218 def get_direction_arcs(self, direction, of_type=None):
219 """Identify arcs to/from all attached nodes in a given direction.
221 Args:
222 direction (str): can be either 'pull' or 'push' to send checks to
223 receiving or contributing nodes
224 of_type (str or list) : optional, can be specified to send checks only
225 to nodes of a given type (must be a subclass in nodes.py)
227 Returns:
228 f (str): Either 'send_pull_check' or 'send_push_check' depending on
229 direction
230 arcs (list): List of arc objects
232 Raises:
233 Message if no direction is specified
235 Examples:
236 >>> arcs_to_push_to = my_node.get_direction_arcs('push')
237 >>> arcs_to_pull_from = my_node.get_direction_arcs('pull')
238 >>> arcs_from_reservoirs = my_node.get_direction_arcs('pull', of_type =
239 'Reservoir')
240 """
241 if of_type is None:
242 # Return all arcs
243 if direction == "pull":
244 arcs = list(self.in_arcs.values())
245 f = "send_pull_check"
246 elif direction == "push":
247 arcs = list(self.out_arcs.values())
248 f = "send_push_check"
249 else:
250 print("No direction")
252 else:
253 if isinstance(of_type, str):
254 of_type = [of_type]
256 # Assign arcs/function based on parameters
257 arcs = []
258 if direction == "pull":
259 for type_ in of_type:
260 arcs += list(self.in_arcs_type[type_].values())
261 f = "send_pull_check"
262 elif direction == "push":
263 for type_ in of_type:
264 arcs += list(self.out_arcs_type[type_].values())
265 f = "send_push_check"
266 else:
267 print("No direction")
269 return f, arcs
271 def get_connected(self, direction="pull", of_type=None, tag="default"):
272 """Send push/pull checks to all attached arcs in a given direction.
274 Args:
275 direction (str, optional): The type of check to send to all attached
276 nodes. Can be 'push' or 'pull'. The default is 'pull'.
277 of_type (str or list) : optional, can be specified to send checks only
278 to nodes of a given type (must be a subclass in nodes.py)
279 tag (str, optional): optional message to direct query_handler which pull
280 function to call. Defaults to 'default'.
282 Returns:
283 connected (dict) :
284 Dictionary containing keys:
285 'avail': (float) - total available volume for push/pull
286 'priority': (float) - total (availability * preference)
287 of attached arcs
288 'allocation': (dict) - contains all attached arcs in specified
289 direction and respective (availability * preference)
291 Examples:
292 >>> vqip_available_to_pull = my_node.get_direction_arcs()
293 >>> vqip_available_to_push = my_node.get_direction_arcs('push')
294 >>> avail_reservoir_vqip = my_node.get_direction_arcs('pull',
295 of_type = 'Reservoir')
296 >>> avail_sewer_push_to_sewers = my_node.get_direction_arcs('push',
297 of_type = 'Sewer',
298 tag = 'Sewer')
299 """
300 # Initialise connected dict
301 connected = {"avail": 0, "priority": 0, "allocation": {}, "capacity": {}}
303 # Get arcs
304 f, arcs = self.get_direction_arcs(direction, of_type)
306 # Iterate over arcs, updating connected dict
307 for arc in arcs:
308 avail = getattr(arc, f)(tag=tag)["volume"]
309 if avail < constants.FLOAT_ACCURACY:
310 avail = 0 # Improves convergence
311 connected["avail"] += avail
312 preference = arc.preference
313 connected["priority"] += avail * preference
314 connected["allocation"][arc.name] = avail * preference
315 connected["capacity"][arc.name] = avail
317 return connected
319 def query_handler(self, handler, ip, tag):
320 """Sends all push/pull requests/checks using the handler (i.e., ensures the
321 correct function is used that lines up with 'tag').
323 Args:
324 handler (dict): contains all push/pull requests for various tags
325 ip (vqip): the vqip request
326 tag (str): describes what type of push/pull request should be called
328 Returns:
329 (dict): the VQIP reply from push/pull request
331 Raises:
332 Message if no functions are defined for tag and if request/check
333 function fails
334 """
335 try:
336 return handler[tag](ip)
337 except Exception:
338 if tag not in handler.keys():
339 print("No functions defined for " + tag)
340 return handler[tag](ip)
341 else:
342 print("Some other error")
343 return handler[tag](ip)
345 def pull_distributed(self, vqip, of_type=None, tag="default"):
346 """Send pull requests to all (or specified by type) nodes connecting to self.
347 Iterate until request is met or maximum iterations are hit. Streamlines if only
348 one in_arc exists.
350 Args:
351 vqip (dict): Total amount to pull (by default, only the
352 'volume' key is used)
353 of_type (str or list) : optional, can be specified to send checks only
354 to nodes of a given type (must be a subclass in nodes.py)
355 tag (str, optional): optional message to direct query_handler which pull
356 function to call. Defaults to 'default'.
358 Returns:
359 pulled (dict): VQIP of combined pulled water
360 """
361 if len(self.in_arcs) == 1:
362 # If only one in_arc, just pull from that
363 if of_type is None:
364 pulled = next(iter(self.in_arcs.values())).send_pull_request(
365 vqip, tag=tag
366 )
367 elif any(
368 [x in of_type for x, y in self.in_arcs_type.items() if len(y) > 0]
369 ):
370 pulled = next(iter(self.in_arcs.values())).send_pull_request(
371 vqip, tag=tag
372 )
373 else:
374 # No viable out arcs
375 pulled = self.empty_vqip()
376 else:
377 # Pull in proportion from connected by priority
379 # Initialise pulled, deficit, connected, iter_
380 pulled = self.empty_vqip()
381 deficit = vqip["volume"]
382 connected = self.get_connected(direction="pull", of_type=of_type, tag=tag)
383 iter_ = 0
385 # Iterate over sending nodes until deficit met
386 while (
387 (deficit > constants.FLOAT_ACCURACY)
388 & (connected["avail"] > constants.FLOAT_ACCURACY)
389 ) & (iter_ < constants.MAXITER):
390 # Pull from connected
391 for key, allocation in connected["allocation"].items():
392 received = self.in_arcs[key].send_pull_request(
393 {"volume": deficit * allocation / connected["priority"]},
394 tag=tag,
395 )
396 pulled = self.sum_vqip(pulled, received)
398 # Update deficit, connected and iter_
399 deficit = vqip["volume"] - pulled["volume"]
400 connected = self.get_connected(
401 direction="pull", of_type=of_type, tag=tag
402 )
403 iter_ += 1
405 if iter_ == constants.MAXITER:
406 print("Maxiter reached in {0} at {1}".format(self.name, self.t))
407 return pulled
409 def push_distributed(self, vqip, of_type=None, tag="default"):
410 """Send push requests to all (or specified by type) nodes connecting to self.
411 Iterate until request is met or maximum iterations are hit. Streamlines if only
412 one in_arc exists.
414 Args:
415 vqip (dict): Total amount to push
416 of_type (str or list) : optional, can be specified to send checks only
417 to nodes of a given type (must be a subclass in nodes.py)
418 tag (str, optional): optional message to direct query_handler which pull
419 function to call. Defaults to 'default'.
421 Returns:
422 not_pushed_ (dict): VQIP of water that cannot be pushed
423 """
424 if len(self.out_arcs) == 1:
425 # If only one out_arc, just send the water down that
426 if of_type is None:
427 not_pushed_ = next(iter(self.out_arcs.values())).send_push_request(
428 vqip, tag=tag
429 )
430 elif any(
431 [x in of_type for x, y in self.out_arcs_type.items() if len(y) > 0]
432 ):
433 not_pushed_ = next(iter(self.out_arcs.values())).send_push_request(
434 vqip, tag=tag
435 )
436 else:
437 # No viable out arcs
438 not_pushed_ = vqip
439 else:
440 # Push in proportion to connected by priority
441 # Initialise pushed, deficit, connected, iter_
442 not_pushed = vqip["volume"]
443 not_pushed_ = self.copy_vqip(vqip)
444 connected = self.get_connected(direction="push", of_type=of_type, tag=tag)
445 iter_ = 0
446 if not_pushed > connected["avail"]:
447 # If more water than can be pushed, ignore preference and allocate all
448 # available based on capacity
449 connected["priority"] = connected["avail"]
450 connected["allocation"] = connected["capacity"]
452 # Iterate over receiving nodes until sent
453 while (
454 (not_pushed > constants.FLOAT_ACCURACY)
455 & (connected["avail"] > constants.FLOAT_ACCURACY)
456 & (iter_ < constants.MAXITER)
457 ):
458 # Push to connected
459 amount_to_push = min(connected["avail"], not_pushed)
461 for key, allocation in connected["allocation"].items():
462 to_send = amount_to_push * allocation / connected["priority"]
463 to_send = self.v_change_vqip(not_pushed_, to_send)
464 reply = self.out_arcs[key].send_push_request(to_send, tag=tag)
466 sent = self.extract_vqip(to_send, reply)
467 not_pushed_ = self.extract_vqip(not_pushed_, sent)
469 not_pushed = not_pushed_["volume"]
470 connected = self.get_connected(
471 direction="push", of_type=of_type, tag=tag
472 )
473 iter_ += 1
475 if iter_ == constants.MAXITER:
476 print("Maxiter reached in {0} at {1}".format(self.name, self.t))
478 return not_pushed_
480 def check_basic(self, direction, vqip=None, of_type=None, tag="default"):
481 """Generic function that conveys a pull or push check onwards to connected
482 nodes. It is the default behaviour that treats a node like a junction.
484 Args:
485 direction (str): can be either 'pull' or 'push' to send checks to
486 receiving or contributing nodes
487 vqip (dict, optional): The VQIP to check. Defaults to None (if pulling
488 this will return available water to pull, if pushing then available
489 capacity to push).
490 of_type (str or list) : optional, can be specified to send checks only
491 to nodes of a given type (must be a subclass in nodes.py)
492 tag (str, optional): optional message to direct query_handler which pull
493 function to call. Defaults to 'default'.
495 Returns:
496 avail (dict): VQIP responses summed over all requests
497 """
498 f, arcs = self.get_direction_arcs(direction, of_type)
500 # Iterate over arcs, updating total
501 avail = self.empty_vqip()
502 for arc in arcs:
503 avail = self.sum_vqip(avail, getattr(arc, f)(tag=tag))
505 if vqip is not None:
506 avail = self.v_change_vqip(avail, min(avail["volume"], vqip["volume"]))
508 return avail
510 def pull_check_basic(self, vqip=None, of_type=None, tag="default"):
511 """Default node check behaviour that treats a node like a junction. Water
512 available to pull is just the water available to pull from upstream connected
513 nodes.
515 Args:
516 vqip (dict, optional): VQIP from handler of amount to pull check
517 (by default, only the 'volume' key is used). Defaults to None (which
518 returns all availalbe water to pull).
519 of_type (str or list) : optional, can be specified to send checks only
520 to nodes of a given type (must be a subclass in nodes.py)
521 tag (str, optional): optional message to direct query_handler which pull
522 function to call. Defaults to 'default'.
524 Returns:
525 (dict): VQIP check response of upstream nodes
526 """
527 return self.check_basic("pull", vqip, of_type, tag)
529 def push_check_basic(self, vqip=None, of_type=None, tag="default"):
530 """Default node check behaviour that treats a node like a junction. Water
531 available to push is just the water available to push to downstream connected
532 nodes.
534 Args:
535 vqip (dict, optional): VQIP from handler of amount to push check.
536 Defaults to None (which returns all available capacity to push).
537 of_type (str or list) : optional, can be specified to send checks only
538 to nodes of a given type (must be a subclass in nodes.py)
539 tag (str, optional): optional message to direct query_handler which pull
540 function to call. Defaults to 'default'.
542 Returns:
543 (dict): VQIP check response of downstream nodes
544 """
545 return self.check_basic("push", vqip, of_type, tag)
547 def pull_set_deny(self, vqip):
548 """Responds that no water is available to pull from a request.
550 Args:
551 vqip (dict): A VQIP amount of water requested (ignored)
553 Returns:
554 (dict): An empty VQIP indicated no water was pulled
556 Raises:
557 Message when called, since it would usually occur if a model is
558 improperly connected
559 """
560 print("Attempted pull set from deny")
561 return self.empty_vqip()
563 def pull_check_deny(self, vqip=None):
564 """Responds that no water is available to pull from a check.
566 Args:
567 vqip (dict): A VQIP amount of water requested (ignored)
569 Returns:
570 (dict): An empty VQIP indicated no water was pulled
572 Raises:
573 Message when called, since it would usually occur if a model is
574 improperly connected
575 """
576 print("Attempted pull check from deny")
577 return self.empty_vqip()
579 def push_set_deny(self, vqip):
580 """Responds that no water is available to push in a request.
582 Args:
583 vqip (dict): A VQIP amount of water to push
585 Returns:
586 vqip (dict): Returns the request indicating no water was pushed
588 Raises:
589 Message when called, since it would usually occur if a model is
590 improperly connected
591 """
592 print("Attempted push set to deny")
593 return vqip
595 def push_check_deny(self, vqip=None):
596 """Responds that no water is available to push in a check.
598 Args:
599 vqip (dict): A VQIP amount of water to push check (ignored)
601 Returns:
602 (dict): An empty VQIP indicated no capacity for pushes exists
604 Raises:
605 Message when called, since it would usually occur if a model is
606 improperly connected
607 """
608 print("Attempted push check to deny")
609 return self.empty_vqip()
611 def push_check_accept(self, vqip=None):
612 """Push check function that accepts all water.
614 Args:
615 vqip (dict, optional): A VQIP that has been pushed (ignored)
617 Returns:
618 (dict): VQIP or an unbounded capacity, indicating all water can be received
619 """
620 if not vqip:
621 vqip = self.empty_vqip()
622 vqip["volume"] = constants.UNBOUNDED_CAPACITY
623 return vqip
625 def get_data_input(self, var):
626 """Read data from data_input_dict. Keys are tuples with the first entry as the
627 variable to read and second entry the time.
629 Args:
630 var (str): Name of variable
632 Returns:
633 Data read
634 """
635 return self.data_input_dict[(var, self.t)]
637 def end_timestep(self):
638 """Empty function intended to be called at the end of every timestep.
640 Subclasses will overwrite this functions.
641 """
642 pass
644 def reinit(self):
645 """Empty function to be written if reinitialisation capability is added."""
646 pass
649"""
650 This is an attempt to generalise the behaviour of pull/push_distributed
651 It doesn't yet work...
653 def general_distribute(self, vqip, of_type = None, tag = 'default', direction =
654 None):
655 if direction == 'push':
656 arcs = self.out_arcs
657 arcs_type = self.out_arcs_type
658 tracker = self.copy_vqip(vqip)
659 requests = {x.name : lambda y : x.send_push_request(y, tag) for x in arcs.
660 values()}
661 elif direction == 'pull':
662 arcs = self.in_arcs
663 arcs_type = self.in_arcs_type
664 tracker = self.empty_vqip()
665 requests = {x.name : lambda y : x.send_pull_request(y, tag) for x in arcs.
666 values()}
667 else:
668 print('No direction')
670 if len(arcs) == 1:
671 if (of_type == None) | any([x in of_type for x, y in arcs_type.items() if
672 len(y) > 0]):
673 arc = next(iter(arcs.keys()))
674 return requests[arc](vqip)
675 else:
676 #No viable arcs
677 return tracker
679 connected = self.get_connected(direction = direction,
680 of_type = of_type,
681 tag = tag)
683 iter_ = 0
685 target = self.copy_vqip(vqip)
686 #Iterate over sending nodes until deficit met
687 while (((target['volume'] > constants.FLOAT_ACCURACY) &
688 (connected['avail'] > constants.FLOAT_ACCURACY)) &
689 (iter_ < constants.MAXITER)):
691 amount = min(connected['avail'], target['volume']) #Deficit or amount
692 still to push
693 replies = self.empty_vqip()
695 for key, allocation in connected['allocation'].items():
696 to_request = amount * allocation / connected['priority']
697 to_request = self.v_change_vqip(target, to_request)
698 reply = requests[key](to_request)
699 replies = self.sum_vqip(replies, reply)
701 if direction == 'pull':
702 target = self.extract_vqip(target, replies)
703 elif direction == 'push':
704 target = replies
706 connected = self.get_connected(direction = direction,
707 of_type = of_type,
708 tag = tag)
709 iter_ += 1
711 if iter_ == constants.MAXITER:
712 print('Maxiter reached')
713 return target"""
715# def replace(self, newnode):
716# #TODO - doesn't work because the bound methods (e.g., pull_set_handler) get
717# #associated with newnode and can't (as far as I can tell) be moved to self
718#
719# """
720# Replace node with new node, maintaining any existing references to original node
722# Example
723# -------
724# print(my_node.name)
725# my_node.update(Node(name = 'new_node'))
726# print(my_node.name)
727# """
730# #You could use the below code to move the arcs_type references to arcs
731# #that are attached to this node. However, if you were creating a new
732# #subclass of Node then you would need to include this key in all
733# #arcs_type dictionaries in all nodes... which would get complicated
734# #probably safest to leave the arcs_type keys that are associated with
735# #this arc the same (for now...) -therefore - needs the same name
737# # for arc in self.in_arcs.values():
738# # _ = arc.in_port.out_arcs_type[self.__class__.__name__].pop(arc.name)
739# # arc.in_port.out_arcs_type[newnode.__class__.__name__][arc.name] = arc
740# # for arc in self.out_arcs.values():
741# # _ = arc.out_port.in_arcs_type[self.__class__.__name__].pop(arc.name)
742# # arc.out_port.in_arcs_type[newnode.__class__.__name__][arc.name] = arc
745# #Replace class
746# self.__class__ = newnode.__class__
748# #Replace object information (keeping some old information such as in_arcs)
749# for key in ['in_arcs',
750# 'out_arcs',
751# 'in_arcs_type',
752# 'out_arcs_type']:
753# newnode.__dict__[key] = self.__dict__[key]
755# self.__dict__.clear()
756# self.__dict__.update(newnode.__dict__)
759NODES_REGISTRY: dict[str, type[Node]] = {Node.__name__: Node}