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

1# -*- coding: utf-8 -*- 

2"""Created on Wed Apr 7 08:43:32 2021. 

3 

4@author: Barney 

5 

6Converted to totals on Thur Apr 21 2022 

7""" 

8import logging 

9from typing import Any, Dict 

10 

11from wsimod.core import constants 

12from wsimod.core.core import WSIObj 

13 

14 

15class Node(WSIObj): 

16 """""" 

17 

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}") 

23 

24 NODES_REGISTRY[cls.__name__] = cls 

25 

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. 

29 

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. 

36 

37 Examples: 

38 >>> my_node = nodes.Node(name = 'london_river_junction') 

39 

40 Key assumptions: 

41 - No physical processes represented, can be used as a junction. 

42 

43 Input data and parameter requirements: 

44 - All nodes require a `name` 

45 """ 

46 node_types = list(NODES_REGISTRY.keys()) 

47 

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} 

54 

55 # Set parameters 

56 self.name = name 

57 self.t = None 

58 self.data_input_dict = data_input_dict 

59 

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__() 

74 

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()] 

79 

80 def apply_overrides(self, overrides: Dict[str, Any] = {}) -> None: 

81 """Apply overrides to the node. 

82 

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. 

86 

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 

92 

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") 

100 

101 if len(overrides) > 0: 

102 print(f"No override behaviour defined for: {overrides.keys()}") 

103 

104 def total_in(self): 

105 """Sum flow and pollutant amounts entering a node via in_arcs. 

106 

107 Returns: 

108 in_ (dict): Summed VQIP of in_arcs 

109 

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) 

116 

117 return in_ 

118 

119 def total_out(self): 

120 """Sum flow and pollutant amounts leaving a node via out_arcs. 

121 

122 Returns: 

123 out_ (dict): Summed VQIP of out_arcs 

124 

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) 

131 

132 return out_ 

133 

134 def node_mass_balance(self): 

135 """Wrapper for core.py/WSIObj/mass_balance. Tracks change in mass balance. 

136 

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 

141 

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_ 

147 

148 def pull_set(self, vqip, tag="default"): 

149 """Receives pull set requests from arcs and passes request to query handler. 

150 

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'. 

156 

157 Returns: 

158 (dict): VQIP received from query_handler 

159 

160 Examples: 

161 >>> water_received = my_node.pull_set({'volume' : 10}) 

162 """ 

163 return self.query_handler(self.pull_set_handler, vqip, tag) 

164 

165 def push_set(self, vqip, tag="default"): 

166 """Receives push set requests from arcs and passes request to query handler. 

167 

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'. 

172 

173 Returns: 

174 (dict): VQIP not received from query_handler 

175 

176 Examples: 

177 water_not_pushed = my_node.push_set(wastewater_vqip) 

178 """ 

179 return self.query_handler(self.push_set_handler, vqip, tag) 

180 

181 def pull_check(self, vqip=None, tag="default"): 

182 """Receives pull check requests from arcs and passes request to query handler. 

183 

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'. 

190 

191 Returns: 

192 (dict): VQIP available from query_handler 

193 

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) 

199 

200 def push_check(self, vqip=None, tag="default"): 

201 """Receives push check requests from arcs and passes request to query handler. 

202 

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' 

208 

209 Returns: 

210 (dict): VQIP available to push from query_handler 

211 

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) 

217 

218 def get_direction_arcs(self, direction, of_type=None): 

219 """Identify arcs to/from all attached nodes in a given direction. 

220 

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) 

226 

227 Returns: 

228 f (str): Either 'send_pull_check' or 'send_push_check' depending on 

229 direction 

230 arcs (list): List of arc objects 

231 

232 Raises: 

233 Message if no direction is specified 

234 

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") 

251 

252 else: 

253 if isinstance(of_type, str): 

254 of_type = [of_type] 

255 

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") 

268 

269 return f, arcs 

270 

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. 

273 

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'. 

281 

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) 

290 

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": {}} 

302 

303 # Get arcs 

304 f, arcs = self.get_direction_arcs(direction, of_type) 

305 

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 

316 

317 return connected 

318 

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'). 

322 

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 

327 

328 Returns: 

329 (dict): the VQIP reply from push/pull request 

330 

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) 

344 

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. 

349 

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'. 

357 

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 

378 

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 

384 

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) 

397 

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 

404 

405 if iter_ == constants.MAXITER: 

406 print("Maxiter reached in {0} at {1}".format(self.name, self.t)) 

407 return pulled 

408 

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. 

413 

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'. 

420 

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"] 

451 

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) 

460 

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) 

465 

466 sent = self.extract_vqip(to_send, reply) 

467 not_pushed_ = self.extract_vqip(not_pushed_, sent) 

468 

469 not_pushed = not_pushed_["volume"] 

470 connected = self.get_connected( 

471 direction="push", of_type=of_type, tag=tag 

472 ) 

473 iter_ += 1 

474 

475 if iter_ == constants.MAXITER: 

476 print("Maxiter reached in {0} at {1}".format(self.name, self.t)) 

477 

478 return not_pushed_ 

479 

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. 

483 

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'. 

494 

495 Returns: 

496 avail (dict): VQIP responses summed over all requests 

497 """ 

498 f, arcs = self.get_direction_arcs(direction, of_type) 

499 

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)) 

504 

505 if vqip is not None: 

506 avail = self.v_change_vqip(avail, min(avail["volume"], vqip["volume"])) 

507 

508 return avail 

509 

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. 

514 

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'. 

523 

524 Returns: 

525 (dict): VQIP check response of upstream nodes 

526 """ 

527 return self.check_basic("pull", vqip, of_type, tag) 

528 

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. 

533 

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'. 

541 

542 Returns: 

543 (dict): VQIP check response of downstream nodes 

544 """ 

545 return self.check_basic("push", vqip, of_type, tag) 

546 

547 def pull_set_deny(self, vqip): 

548 """Responds that no water is available to pull from a request. 

549 

550 Args: 

551 vqip (dict): A VQIP amount of water requested (ignored) 

552 

553 Returns: 

554 (dict): An empty VQIP indicated no water was pulled 

555 

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() 

562 

563 def pull_check_deny(self, vqip=None): 

564 """Responds that no water is available to pull from a check. 

565 

566 Args: 

567 vqip (dict): A VQIP amount of water requested (ignored) 

568 

569 Returns: 

570 (dict): An empty VQIP indicated no water was pulled 

571 

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() 

578 

579 def push_set_deny(self, vqip): 

580 """Responds that no water is available to push in a request. 

581 

582 Args: 

583 vqip (dict): A VQIP amount of water to push 

584 

585 Returns: 

586 vqip (dict): Returns the request indicating no water was pushed 

587 

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 

594 

595 def push_check_deny(self, vqip=None): 

596 """Responds that no water is available to push in a check. 

597 

598 Args: 

599 vqip (dict): A VQIP amount of water to push check (ignored) 

600 

601 Returns: 

602 (dict): An empty VQIP indicated no capacity for pushes exists 

603 

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() 

610 

611 def push_check_accept(self, vqip=None): 

612 """Push check function that accepts all water. 

613 

614 Args: 

615 vqip (dict, optional): A VQIP that has been pushed (ignored) 

616 

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 

624 

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. 

628 

629 Args: 

630 var (str): Name of variable 

631 

632 Returns: 

633 Data read 

634 """ 

635 return self.data_input_dict[(var, self.t)] 

636 

637 def end_timestep(self): 

638 """Empty function intended to be called at the end of every timestep. 

639 

640 Subclasses will overwrite this functions. 

641 """ 

642 pass 

643 

644 def reinit(self): 

645 """Empty function to be written if reinitialisation capability is added.""" 

646 pass 

647 

648 

649""" 

650 This is an attempt to generalise the behaviour of pull/push_distributed 

651 It doesn't yet work... 

652 

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') 

669 

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 

678 

679 connected = self.get_connected(direction = direction, 

680 of_type = of_type, 

681 tag = tag) 

682 

683 iter_ = 0 

684 

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)): 

690 

691 amount = min(connected['avail'], target['volume']) #Deficit or amount 

692 still to push 

693 replies = self.empty_vqip() 

694 

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) 

700 

701 if direction == 'pull': 

702 target = self.extract_vqip(target, replies) 

703 elif direction == 'push': 

704 target = replies 

705 

706 connected = self.get_connected(direction = direction, 

707 of_type = of_type, 

708 tag = tag) 

709 iter_ += 1 

710 

711 if iter_ == constants.MAXITER: 

712 print('Maxiter reached') 

713 return target""" 

714 

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 

721 

722# Example 

723# ------- 

724# print(my_node.name) 

725# my_node.update(Node(name = 'new_node')) 

726# print(my_node.name) 

727# """ 

728 

729 

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 

736 

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 

743 

744 

745# #Replace class 

746# self.__class__ = newnode.__class__ 

747 

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] 

754 

755# self.__dict__.clear() 

756# self.__dict__.update(newnode.__dict__) 

757 

758 

759NODES_REGISTRY: dict[str, type[Node]] = {Node.__name__: Node}