Compare commits

603 Commits
Author SHA1 Message Date
mcDandy 1a02da1aca a fix for lora node 2026-10-01 21:09:39 +02:00
mcDandy b3b1804783 AI: add option to enable tracking for regular dictionary.
On by default for V{N} in model like nodes in dictionary mode.
2026-09-17 19:12:50 +02:00
mcDandy d3d76e6eeb AI: Fix dictionary mode and add functions to read changed keys. 2026-09-17 18:50:09 +02:00
mcDandy a3298018b9 AI: dict mode optimalisations 2026-09-17 18:06:44 +02:00
mcDandy fc3f9351ca AI: fix result not changing when overwriting input value in dictionary mode 2026-09-17 17:29:53 +02:00
mcDandy 5081f25cf9 Add option to run on GPU to video 2026-09-17 17:28:09 +02:00
mcDandy fe77b220fa AI: make sure nested tensors behave as they should when indexing 2026-09-12 23:24:46 +02:00
mcDandy 82cdd4edf7 make sure functions are correctly populated 2026-09-11 22:17:45 +02:00
mcDandy cdbba8bf6a AI: readme audit 3 pas 2026-09-11 16:24:53 +02:00
mcDandy 8cd9baa94d AI: readme audit 2 pas 2026-09-11 15:54:46 +02:00
mcDandy fc3a628b95 AI: readme audit 1 pas 2026-09-11 15:34:30 +02:00
mcDandy d64b23d34b AI: code audit 2026-09-11 13:36:22 +02:00
mcDandy 0437c456a3 AI: general fixes for model adjecent nodes 2026-09-11 13:00:53 +02:00
mcDandy 0a7df88c9a fix compile 2026-09-11 12:43:16 +02:00
mcDandy 70d18dfc43 Try build and try fix (+AI) 2026-09-11 10:59:53 +02:00
mcDandy 6be16be3fd AI: add dict mode to model like nodes. TODO build grammer and check 2026-09-11 10:39:02 +02:00
mcDandy 13cce62633 cleanup of menu 2026-09-10 17:57:39 +02:00
mcDandy af2f84468b forgot to push grammer 2026-09-10 17:13:32 +02:00
mcDandy 18f4e7d4f1 add rm_key 2026-09-10 17:09:22 +02:00
mcDandy 3f717ce2c4 fix missing and correct in readme 2026-09-10 17:08:07 +02:00
mcDandy 0e4e2c06af AI: add dictionaries 2026-09-09 21:57:22 +02:00
mcDandy 59e40f8c6f Update VideoMathNode.py
Fix it not working at all.
2026-09-09 12:34:13 +02:00
mcDandy 8711f2efcc fix probably old standing CFG not found error 2026-09-07 23:08:50 +02:00
mcDandy f249c445e7 ai: slicing 2026-09-07 23:04:54 +02:00
mcDandy 20397b69c2 AI: quick lora math node.
TODO: make human walk through
2026-08-28 21:53:43 +02:00
mcDandy 305ab2db29 fix V{n}_d not working 2026-08-28 21:36:54 +02:00
mcDandy 078553f45f ai: fix delta patches 2026-08-28 21:01:19 +02:00
mcDandy 9f2c766050 sort - add desc+dim 2026-08-28 14:55:09 +02:00
mcDandy b7db90fc84 add workflow 2026-08-28 14:00:50 +02:00
mcDandy 0b5dc28154 AI: optimasitations 2026-08-27 18:54:10 +02:00
mcDandy 2693d11275 AI: make more things lazy 2026-08-27 18:25:03 +02:00
mcDandy 75b9857672 AI: make ModelMathNode way faster + add gpu acceleration to some others 2026-08-27 17:57:12 +02:00
mcDandy 0982fac3aa AI: download riff model to correct folder 2026-08-27 15:53:36 +02:00
mcDandy d3e24d1896 some cleanup 2026-08-26 17:57:22 +02:00
mcDandy 1aa08b814b Should have fixed a long time ago. 2026-08-26 16:17:39 +02:00
mcDandy c15bfae5b0 add missing to readme 2026-08-26 16:14:00 +02:00
mcDandy 8818475fbb remove transpose from SVD 2026-08-26 15:37:23 +02:00
mcDandy a05f4d7b14 AI: add support for negative indexes for lists 2026-08-26 15:01:30 +02:00
mcDandy 7d210b53f4 fix diagonal and better errors 2026-08-26 14:41:14 +02:00
mcDandy 4413d22e5e fix svd + add full_matrix I forgot to 2026-08-26 13:39:59 +02:00
mcDandy 736275ef28 add cord func. Same as Dnumber but for any tensor 2026-08-26 13:29:22 +02:00
mcDandy b783d6f770 AI: add functions
am just lazy to add them myself.
2026-08-25 22:23:35 +02:00
mcDandy ce2455672f AI: add V{N}_d as diff 2026-08-25 20:14:34 +02:00
mcDandy a0f7ad3a7b AI: model math did not see things like added lora 2026-08-25 19:42:45 +02:00
mcDandy 3ec307880c Add missing functions to readme. 2026-08-25 19:04:44 +02:00
mcDandy 11f0c1f3c1 add full matrix option and fix string math 2026-08-21 18:32:17 +02:00
mcDandy d36cb04f91 Build grammer 2026-08-08 23:57:46 +02:00
mcDandy e91b29defb Add nested tensor creation + svd and way to reverse SVD 2026-08-08 23:54:52 +02:00
mcDandy c207f97e7c AI: fix nested tensor phase3
Fix math actually mathing
2026-08-08 21:19:16 +02:00
mcDandy c5d81146df AI: fix NestedTensor phase2
revert splitting of input so no-op is easy / possible
2026-08-08 21:02:11 +02:00
mcDandy 70d6b75853 AI: fix NestedTensor phase1 2026-08-08 20:41:51 +02:00
mcDandy 885896aab6 New version 2026-07-02 14:58:00 +02:00
mcDandy cb136e6818 add V0... back hopefully. 2026-06-21 22:53:40 +02:00
mcDandy 3884a8cb1b only legacy variables work as of now. 2026-06-21 22:33:13 +02:00
mcDandy be0ba5284f Hopefully fix 2026-06-21 21:44:24 +02:00
mcDandy 7956f23d96 HOW DID I MISS STRING RETURNING AUDIO????? 2026-06-21 21:29:58 +02:00
mcDandy d9af505f39 build grammer + add in node descriptions 2026-06-20 16:19:53 +02:00
mcDandy 3462257c23 Fix of a fix 2026-06-20 15:14:34 +02:00
mcDandy 243e449ae0 fix tensor() 2026-06-20 14:39:04 +02:00
mcDandy 06f5859f60 Fix snorm. 2026-06-15 22:27:32 +02:00
mcDandy 928577a9bc Merge branch 'main' of https://github.com/mcDandy/more_math 2026-06-15 22:22:36 +02:00
mcDandy 97ff2fac7f Revert "snorm with dimension is tnorm 😅 moving it to tnorm"
This reverts commit ccff96aad4.
2026-06-15 22:22:19 +02:00
mcDandy 006e8cb5d0 Update README.md - update tnorm 2026-06-15 22:00:06 +02:00
mcDandy ccff96aad4 snorm with dimension is tnorm 😅 moving it to tnorm 2026-06-15 21:57:19 +02:00
mcDandy 2ebe252045 snorm allows dimension/s 2026-06-15 21:49:26 +02:00
mcDandy 5ac9fbb62c First part of in comfyUI documentation 2026-06-15 19:44:46 +02:00
mcDandy 756943f0f2 messy fixes 2026-06-11 23:40:41 +02:00
mcDandy 34db35ad07 Add in node documentation (Partially AI) 2026-06-11 23:27:37 +02:00
mcDandy 25d0a3ca8a Fix the worst of software italic 2026-06-11 12:12:03 +02:00
mcDandy 228f2f414b Update autocomplete.js 2026-06-11 00:54:52 +02:00
mcDandy 96e0a52c25 English 2026-06-10 17:30:54 +02:00
mcDandy 31dde39e63 AI: fix offset so it is not hiding what is being written 2026-06-10 16:25:37 +02:00
mcDandy 76e14d6f2a AI: attempt to add autocomplete 2026-06-10 15:58:46 +02:00
mcDandy 6987878ef0 AI: more fixes of italic on arial 2026-06-10 15:38:01 +02:00
mcDandy a961ee2443 AI: attempt fix arial italics being bold when weight=400 2026-06-10 14:45:20 +02:00
mcDandy dd1fd21cce AI: fix text 2026-06-10 14:29:02 +02:00
mcDandy 8308a22068 AI: attempt fix 2 2026-06-10 14:23:07 +02:00
mcDandy b3f64d1664 AI: fix attempt 1 2026-06-10 14:17:28 +02:00
mcDandy 898238d833 AI: text 2026-06-09 23:14:13 +02:00
mcDandy 79adf0f82c AI: add opacity being able to be a tensor 2026-06-09 21:55:11 +02:00
mcDandy c9cfd25f96 Fix broken file. 2026-06-09 21:54:23 +02:00
mcDandy d89fa084af Throw error on length mismatch 2026-06-08 21:38:26 +02:00
mcDandy d1a05e0d7a Change to usual 2026-06-08 21:35:40 +02:00
mcDandy 5854a35c95 change to string 2026-06-08 21:22:32 +02:00
mcDandy 2fa527bca3 AI: add option to change strength of allready applied lora (Load lora (bypass) only) 2026-06-04 00:38:06 +02:00
mcDandy 9e11087466 Different language 2026-06-04 00:36:34 +02:00
mcDandy dfeb6a73e4 For later 2026-06-03 13:39:59 +02:00
mcDandy f26fd2e6de AI Auto-generate functions for frontend and future use 2026-06-03 12:02:07 +02:00
mcDandy d3af128043 It expects a list 2026-06-03 11:49:44 +02:00
mcDandy 7fda04154f batch lora first version
name Will change when I figure better one.
2026-05-31 22:12:36 +02:00
mcDandy 9b773390a2 AI: frontend ready for V2 2026-05-28 15:00:53 +02:00
mcDandy c2af417e61 AI: smoother size change 2026-05-26 16:34:42 +02:00
mcDandy 42a27443b2 AI: shrink grow 2026-05-26 15:44:23 +02:00
mcDandy f6265de9c5 AI: Fix newline 2026-05-26 15:32:54 +02:00
mcDandy 9b95e5b2fa AI: Fix V1 also 2026-05-26 15:28:44 +02:00
mcDandy 2621d78607 AI Nodes V2 work 2026-05-26 15:20:27 +02:00
mcDandy e2d558b889 t 2026-05-26 14:10:29 +02:00
mcDandy 4bfde14aa8 AI: set gutter size correctly 2026-05-26 13:49:27 +02:00
mcDandy e0e9ac790a attempt to support nodes V2 begin 2026-05-26 13:45:22 +02:00
mcDandy ad1132acee Update README to clarify expression requirements
Clarify that branches and loops can precede the final expression.
2026-05-19 17:18:55 +02:00
mcDandy 5493722f39 Update README with workflow examples and input details
Added a note about workflow examples and clarified input requirements.
2026-05-19 17:16:43 +02:00
mcDandy aca74337be Create stack_guider.json 2026-05-19 14:58:50 +02:00
mcDandy 7f2301f813 Guider lazy loading 2026-05-18 00:04:58 +02:00
mcDandy c8315a6b20 add ridge and domain_warp noise 2026-05-17 23:56:51 +02:00
mcDandy 96a2eca686 further optimalisations 2026-05-17 11:01:04 +02:00
mcDandy c7d18550ca forgot to change when moving file around 2026-05-17 00:28:02 +02:00
mcDandy 0c8c98e655 AI: optimalise heavy noise functions
They were also called 4x more then they should be in some cases
2026-05-17 00:24:44 +02:00
mcDandy b2f84ab8b5 add lambda 2026-05-17 00:22:59 +02:00
mcDandy 5fa5506db4 just a antlr4 runtime needed. Old versions now supported. 2026-05-14 16:02:59 +02:00
mcDandy ef5240e3d8 Merge branch 'main' of https://github.com/mcDandy/more_math 2026-05-14 14:18:45 +02:00
mcDandy 11be4840ce Try fix compatibility with old antlr 2026-05-14 14:18:41 +02:00
mcDandy bc97746c3c Forgot to push a commit. Making a new version. 2026-05-14 11:55:15 +02:00
mcDandy d4cbefc986 Merge branch 'main' of https://github.com/mcDandy/more_math 2026-05-14 08:56:31 +02:00
mcDandy 0ed4e75ca9 Bump version to 1.0.0 and update project description
Updated version and description to include STRING and GUIDER types.
2026-05-13 13:15:12 +02:00
mcDandy 016bf23a17 Refine README variables and remove grammar notes
Updated common variables section and removed parser notes.
2026-05-13 13:14:54 +02:00
mcDandy 81d5b3f6a7 normalise cielab and oklab to the same scale 2026-05-11 23:20:34 +02:00
mcDandy dfaae09506 Forgot to save 2026-05-11 23:06:03 +02:00
mcDandy f6dd7f2985 no clamp. User should clamp values. Also CIELAB L€0...1 2026-05-11 22:59:02 +02:00
mcDandy 1f3a85dbdc correct name 2026-05-11 22:42:27 +02:00
mcDandy 0334e24c0c Remove java classes 2026-05-11 22:27:03 +02:00
mcDandy 4fc7e245f6 fix an build grammer 2026-05-11 22:26:17 +02:00
mcDandy 1571b10043 AI: add RGB to CIELAB and OKLAB and inverses
Should have released 1.0 like a month ago...
2026-05-11 22:16:02 +02:00
mcDandy 61d64cc994 Update README.md 2026-05-04 14:30:21 +02:00
mcDandy 3723bf0ded AI: Interpolate 2026-05-01 23:59:41 +02:00
mcDandy a619d66179 add description 2026-04-28 16:31:32 +02:00
mcDandy 82e71a2845 add unbatcher 2026-04-28 16:25:19 +02:00
mcDandy c7390e6750 move tests to tests 2026-04-28 16:13:20 +02:00
mcDandy fd557bdd1e Create StackUnBatcher.py 2026-04-27 15:09:20 +02:00
mcDandy 4b7dbc141d Color type does not work for this 2026-04-26 22:25:05 +02:00
mcDandy 99a2fae2ae Cancel action 2026-04-23 12:53:27 +02:00
mcDandy 44f9a2c26b AI: checkpoint 5 2026-04-23 11:57:56 +02:00
mcDandy 6fe92bc7c6 AI: checkpoint 4 2026-04-23 11:25:43 +02:00
mcDandy 75f2f81585 AI: Checkpoint 3 2026-04-23 11:05:23 +02:00
mcDandy fec747cf94 AI: Checkpoint 2 2026-04-23 10:50:48 +02:00
mcDandy c36ec00dd3 AI: savepoint 1 2026-04-23 10:49:53 +02:00
mcDandy 573ac457a3 AI: revert to AI using previous AI version 2026-04-23 10:47:41 +02:00
mcDandy 0b116dfed9 AI: rewrite of ScriptInputNode 2026-04-22 13:46:49 +02:00
mcDandy 1643fb3fb2 Fixes on unusable node 2026-04-22 10:43:12 +02:00
mcDandy dad80da1dd test to test if problem in node or in comfyUI 2026-04-22 00:03:03 +02:00
mcDandy 0e42e60aa8 String math node was shadowing AudioMathNode. oops 2026-04-21 22:40:33 +02:00
mcDandy 4b140e6512 Try fix color math
ComfyUI itself probably has bug since input does not behave as if connected.
2026-04-21 22:35:16 +02:00
mcDandy b68e247675 ai: add missing activatin shape 2026-04-21 21:51:30 +02:00
mcDandy 7c5746c0bb Update SelectiveGuiderMathNode.py 2026-04-21 16:29:17 +02:00
mcDandy 0a1e1bc1f6 AI: Add debug toggle - This node is hard to use 2026-04-21 16:16:46 +02:00
mcDandy cd1e380901 forgot to build grammer 2026-04-20 20:10:22 +02:00
mcDandy da497db110 AI: add Color math node 2026-04-20 19:16:50 +02:00
mcDandy e8a222489d AI: clarifications 2026-04-20 15:41:31 +02:00
mcDandy 609b851359 small human change 2026-04-20 15:33:49 +02:00
mcDandy 544bc3f01a AI: better descriptions pass1 2026-04-20 15:20:57 +02:00
mcDandy 6ae1fb1c50 AI: code fixes 2026-04-20 15:02:36 +02:00
mcDandy 947a97d3b7 AI: reorganise README 2026-04-20 14:54:17 +02:00
mcDandy 36fcbd48c8 add flow magnitude and angle, histogram 2026-04-20 14:24:37 +02:00
mcDandy b8bf4a8ea7 grammer build 2026-04-14 23:20:46 +02:00
mcDandy e2a9eed38f add preparations for overlay with opacity 2026-04-14 18:51:49 +02:00
mcDandy 85fd06594c forgot self 2026-04-14 13:35:29 +02:00
mcDandy 17bb87ebf4 add compound assignments 2026-04-14 13:32:57 +02:00
mcDandy 0e5ebde862 Fix error in int_to_rgb 2026-04-14 13:31:56 +02:00
mcDandy ad9f747c5b Throw clearererror 2026-04-14 13:31:04 +02:00
Daniel Martinek 58287392fb fix logspace not using base 2026-04-05 13:00:50 +02:00
Daniel Martinek f0721e09c9 README closer to rality 2026-04-05 00:33:35 +02:00
Daniel Martinek da5862c2f9 AI: attempt to add Flux hooks 2026-04-04 23:37:00 +02:00
Daniel Martinek 028b487d8a AI: Attempt to fix bug when using something else then SD1.5 + add model begin and model end option 2026-04-04 22:47:04 +02:00
Daniel Martinek 7c2788d777 AI: Add hook target + potential fix 2026-04-04 22:39:14 +02:00
Daniel Martinek ad833649b9 add back stack 2026-04-04 21:47:45 +02:00
Daniel Martinek 0accd11dc5 AI: try shallow copying + rename of variable 2026-04-03 12:30:40 +02:00
Daniel Martinek fc2d09304c change default for debugging 2026-04-03 11:41:20 +02:00
Daniel Martinek 1a47e37e5a add more variables (probably useless) 2026-04-03 11:35:24 +02:00
Daniel Martinek cd28d6a9fe * instead of and 2026-04-02 12:57:25 +02:00
Daniel Martinek c613ed01cb AI: add is_ node for time embedding 2026-04-02 12:46:54 +02:00
Daniel Martinek 8c3a57abf0 AI: add the edges (time embedding) 2026-04-02 12:30:40 +02:00
Daniel Martinek 12fdc5e25c AI add middle blocks 2026-04-02 11:04:08 +02:00
Daniel Martinek 81514ee96f Forgot to change tooltip 2026-04-02 10:16:02 +02:00
Daniel Martinek eed01cb41d AI: to table + description 2026-04-01 23:05:34 +02:00
Daniel Martinek 9499a537d8 AI: fixes 2026-04-01 23:03:10 +02:00
Daniel Martinek 80a240efee AI: add unet hooks 2026-04-01 22:57:09 +02:00
Daniel Martinek 5726132d70 AI: fixes 2026-04-01 22:46:23 +02:00
Daniel Martinek 2da943a58e AI: remove not working and confusing options 2026-04-01 22:07:39 +02:00
Daniel Martinek 87a9a9b54c AI: work ver 2026-04-01 21:55:43 +02:00
Daniel Martinek cff12d66e0 AI sgmn runs 2026-04-01 21:13:30 +02:00
Daniel Martinek e6fc81d1a3 AI: forgot to save 2026-04-01 15:56:14 +02:00
Daniel Martinek adc6c36aec AI: selective guider node commit 3 2026-04-01 15:55:05 +02:00
Daniel Martinek f81cff61df AI: multiple changes 2026-04-01 14:52:42 +02:00
Daniel Martinek d98b65bcc2 AI: Add node to run between model layers 2026-04-01 14:08:06 +02:00
Daniel Martinek a05ffef169 add remember stack to guider 2026-04-01 11:36:53 +02:00
Daniel Martinek 7df370d3a4 build grammer 2026-04-01 11:29:23 +02:00
Daniel Martinek 4cfaf36851 erf,erfinv 2026-04-01 11:26:32 +02:00
Daniel Martinek 2d5d4188a3 FORMAT 2026-04-01 11:23:05 +02:00
Daniel Martinek 10be52d481 roll linspace, logspace - no grammer build 2026-04-01 10:59:00 +02:00
Daniel Martinek 49ce366a98 fix icon 2026-03-31 23:32:43 +02:00
Daniel Martinek 60c026ecc2 add icon 2026-03-31 23:29:03 +02:00
Daniel Martinek 9595e09f1a AI: bit of a rewrite 2026-03-31 23:23:32 +02:00
Daniel Martinek 0881ed9dc8 přidat funkce radme 2026-03-31 11:18:52 +02:00
Daniel Martinek f970a12053 build grammer 2026-03-30 23:41:14 +02:00
Daniel Martinek c5f58e4ece add int to rgb and rgb to int 2026-03-30 22:07:49 +02:00
Daniel Martinek 1ca1c2af54 missing space 2026-03-26 14:23:46 +01:00
Daniel Martinek 5aa5d3c12f fix #2 2026-03-26 14:23:33 +01:00
Daniel Martinek 5c63ad12e1 better error when something is completly broken 2026-03-26 14:14:33 +01:00
Daniel Martinek ca9de242af nasic rgb2int 2026-03-26 13:44:30 +01:00
Daniel Martinek c4d7e60600 int2rgb 2026-03-26 13:21:33 +01:00
Daniel Martinek 354ac53664 ruff 2026-03-26 12:28:36 +01:00
Daniel Martinek db94472a27 Add string node 2026-03-26 11:17:11 +01:00
Daniel Martinek 085cba73ec remove unneeded calls 2026-03-26 10:38:46 +01:00
Daniel Martinek e43dc78e33 ruff check 2026-03-26 10:34:21 +01:00
Daniel Martinek 943cb53ccb add ability to remember stack during batching for other nodes 2026-03-26 10:17:08 +01:00
Daniel Martinek 10deecb7b0 fix node being unusable 2026-03-26 09:14:35 +01:00
Daniel Martinek b91513dfe5 add optional stack remember on batch to AUdioMathNode 2026-03-23 21:45:09 +01:00
Daniel Martinek 1393a83f0f Did I mess up this bad? 2026-03-23 21:44:07 +01:00
Daniel Martinek d407e5af40 add stach batch toggle. 2026-03-21 23:00:54 +01:00
Daniel Martinek a4dd0a2e00 formatting FloatMathNode 2026-03-21 22:52:57 +01:00
Daniel Martinek d29a90b146 add int and float conversion 2026-03-18 18:55:45 +01:00
Daniel Martinek d58bf02893 Do not clamp in rgb to hsv and inverse 2026-03-18 18:22:27 +01:00
Daniel Martinek 161861f06f Update UnifiedMathVisitor.py 2026-03-15 16:19:48 +01:00
Daniel Martinek d5b8e3ccdb add concat 2026-03-15 15:46:26 +01:00
Daniel Martinek 75a14df5d6 AI script input node attempt fix not filling 2026-03-14 16:41:00 +01:00
Daniel Martinek 9d6a2d3c1c Revert "check why no stack is between float and guider" - problem in workflow
This reverts commit cfdf30d0ee.
2026-03-14 12:17:15 +01:00
Daniel Martinek cfdf30d0ee check why no stack is between float and guider 2026-03-14 11:58:14 +01:00
Daniel Martinek c17479029f try fix rolling back stack when using batching in guider 2026-03-13 23:21:52 +01:00
Daniel Martinek 8197f914da try rename to fix problem 2026-03-13 22:21:51 +01:00
Daniel Martinek 7fa46ca3b0 remove debug prints 2026-03-13 21:52:24 +01:00
Daniel Martinek d3ff9e834c fix GuiderMath? 2026-03-13 21:48:05 +01:00
Daniel Martinek bd090bb40f debug 1 2026-03-13 21:43:20 +01:00
Daniel Martinek 67c6571b38 check if something wrong 2026-03-13 21:36:15 +01:00
Daniel Martinek 570d297ac9 fix outside stack not usable in GuiderMathNode 2026-03-13 21:26:59 +01:00
Daniel Martinek d5b840450f guiderMathNode add previons sample result and whathever seed input is 2026-03-13 16:36:43 +01:00
Daniel Martinek 96df6b6a53 Allways stack when converting color representations 2026-03-13 15:26:14 +01:00
mcDandy 7ce673175a Fix tensor shape indexing for 'T' variable 2026-03-12 21:31:57 +01:00
mcDandy f788a636a3 actually fix 2026-03-12 21:15:48 +01:00
Daniel Martinek 1e1d80f30c prepare for add concat 2026-03-12 20:59:26 +01:00
Daniel Martinek 328a90ff04 AI: hopefully fix conditioning variable not found on flux.2 D
in pooled_input which does not exist there
2026-03-12 20:59:11 +01:00
mcDandy 4f3aeba493 will change later 2026-03-05 09:01:54 +01:00
mcDandy b7f3f9e533 formatting 2026-03-03 22:32:19 +01:00
mcDandy 6a1832394c Readme accurecy fixesd. 2026-03-03 12:08:28 +01:00
mcDandy 809ce6f0b4 entropy has units 2026-03-02 16:15:54 +01:00
mcDandy f0663e3386 count returns number of elements in tensor instead of batch size 2026-03-01 19:06:53 +01:00
mcDandy 052d14b1a4 Keep type 2026-03-01 16:10:51 +01:00
mcDandy 076a3aa5d7 AI: fix - hopefully rgb2hsv 2026-02-28 18:06:53 +01:00
mcDandy d74304d0f2 fix shape 2026-02-28 17:44:37 +01:00
mcDandy 0faaec395a Fix node being able to be used 2026-02-28 17:43:52 +01:00
mcDandy c40f3e32b2 Fix error with tensor creation. 2026-02-28 17:04:31 +01:00
mcDandy 61681ddf5a Add better error message 2026-02-28 16:24:13 +01:00
mcDandy 0c5fed6780 AI: fix cossim and cross 2026-02-28 16:09:58 +01:00
mcDandy bb9ec72418 add support for lists in shape 2026-02-28 15:28:15 +01:00
mcDandy 29999e97dc Add mising to readme 2026-02-28 15:27:40 +01:00
mcDandy f013d3d16c add missing function to highligting 2026-02-27 19:20:56 +01:00
mcDandy 1c6b0b6c63 AI: green again 2026-02-27 19:07:44 +01:00
mcDandy 471b90c533 AI: tests all green 2026-02-27 18:45:18 +01:00
mcDandy 64a2f6358b AI: fix most tests 2026-02-27 18:17:41 +01:00
mcDandy 5398b651ed AI: entropy and corelation 2026-02-27 17:37:58 +01:00
mcDandy 3f97c5c75d fix channel to variables mapping for image and video 2026-02-27 13:14:21 +01:00
mcDandy 9f49c6d054 AI convert size variables to floats 2026-02-27 12:19:25 +01:00
mcDandy 9129653f40 AI: add reshape check 2026-02-26 23:26:12 +01:00
mcDandy bfe0fd2137 fix wierd bug with reshape (AI) 2026-02-26 23:19:40 +01:00
mcDandy 7f4e0e5384 bring back variables, do not need examples 2026-02-26 21:26:12 +01:00
mcDandy 1fb8bf7f05 forgot to build grammer 2026-02-26 21:19:28 +01:00
mcDandy a9ed8cae2f add ctx to bin_op to fix errors 2026-02-26 21:18:50 +01:00
mcDandy 6d0d804a9e add string key to vae, clip and model 2026-02-26 21:18:07 +01:00
mcDandy 525e1643a1 I thought I reverted the deleted functions... + AI RGB2HSV func more usefull 2026-02-26 13:39:41 +01:00
mcDandy ddc4bfd460 I forgot to save fix after AI + AI - new functions 2026-02-26 13:03:11 +01:00
mcDandy e9b9486bf1 Update MathExpr.g4 2026-02-26 12:46:32 +01:00
mcDandy d80c8c1156 AI: add string functions 2026-02-26 12:46:13 +01:00
mcDandy ed030f1542 match background color to default text box 2026-02-26 10:04:15 +01:00
mcDandy 3cbaa6b0f3 ai: better brackets coloring 2026-02-25 18:45:46 +01:00
mcDandy 800bb10835 add better error in bitwise op 2026-02-25 18:44:24 +01:00
mcDandy 1c32d60a56 add string support to [] 2026-02-24 20:13:19 +01:00
mcDandy 512635f2c0 overlay string and list support 2026-02-24 15:34:00 +01:00
mcDandy ca98326f3e add string compare 2026-02-23 15:21:39 +01:00
mcDandy e4e5b21b56 crop works as substr 2026-02-22 16:15:10 +01:00
mcDandy 3d60b109bd string can be added to anything now 2026-02-22 15:53:15 +01:00
mcDandy 83bca12a75 Rename type 2026-02-22 15:35:03 +01:00
mcDandy 18e9221aa2 better error message when dividing by 0 (and others) 2026-02-22 15:34:44 +01:00
mcDandy dc67e74789 String as a type 2026-02-22 11:59:50 +01:00
mcDandy 138a951b2d no longer a class method 2 2026-02-21 21:43:03 +01:00
mcDandy 6e3daba1ed no longer a class method 2026-02-21 21:41:07 +01:00
mcDandy 4e1cd2c872 AI: add support for scrollbar. 2026-02-21 18:47:02 +01:00
mcDandy d3bc62cbb9 Add ability to use custom widget everywhere 2026-02-21 17:27:56 +01:00
mcDandy bc52d51900 move check to helper functions 2026-02-21 16:43:33 +01:00
mcDandy 4bc2b67a85 Change types 2026-02-21 16:30:52 +01:00
mcDandy 3e7560a565 AI: add autoclose brackets 2026-02-21 13:53:26 +01:00
mcDandy a75ed2ff1a AI: rainbow brackets 2026-02-21 13:48:35 +01:00
mcDandy 9f783b2f26 AI: fix size change 2026-02-21 12:35:48 +01:00
mcDandy 9010056bee AI: newline bug squashed. 2026-02-21 11:52:15 +01:00
mcDandy ccf142bd78 AI: add colors iteration 1 2026-02-21 11:31:17 +01:00
mcDandy 8d1ff510b9 AI: add colors - somewhat working. 2026-02-21 00:28:41 +01:00
mcDandy 9db29fbb28 Node rename 2026-02-20 23:53:09 +01:00
mcDandy b2598fef93 Revert "AI attempt adding ACE editor"
This reverts commit 48d5380ec5.
2026-02-20 23:50:55 +01:00
mcDandy 48d5380ec5 AI attempt adding ACE editor 2026-02-20 23:27:04 +01:00
mcDandy 0d22efc8b1 AI: add line numbers to custom node 2026-02-20 21:46:57 +01:00
mcDandy 62475fdd65 actually regenerate parser 2026-02-20 18:04:18 +01:00
mcDandy ebbc837629 add overlay (rewrode code by AI) and fix voroni 2026-02-20 18:03:25 +01:00
mcDandy ada7688deb fix problem after AI 2026-02-20 17:09:46 +01:00
mcDandy 79c403bd15 add missing to readme 2026-02-19 23:59:39 +01:00
mcDandy 53fa16dd70 Update UnifiedMathVisitor.py 2026-02-19 22:34:59 +01:00
mcDandy fa018e6e4f fuíxed list OP tensor when first dim of tensor is the same size as list lenght 2026-02-19 22:33:46 +01:00
mcDandy edb7908328 remove debug prints 2026-02-19 10:08:21 +01:00
mcDandy 1037a88b40 import forgitten pad function 2026-02-19 09:04:02 +01:00
mcDandy e30e253957 fix error 2026-02-19 07:27:42 +01:00
mcDandy 4f20ab8d1d Mostly AI: make more clever check_lazy_staus
For AudioMathNode it will allow using a,b,c,d,w,x,y,z, V..., F... in function definitions and after assignment when the corresponding connections are notconnected
2026-02-18 21:06:45 +01:00
mcDandy 84df61f696 remove unused import 2026-02-18 13:59:39 +01:00
mcDandy 4021d3e617 Fix failed to load 2026-02-18 12:29:14 +01:00
mcDandy 0f76196dda edge cases for clamp if type is wierd combination of list and tensors 2026-02-18 10:26:07 +01:00
mcDandy 891634a912 AI: add plasma, voronoi and perlin noise 2026-02-18 00:49:25 +01:00
mcDandy 412ea5e89b shape returns list now 2026-02-17 00:34:51 +01:00
mcDandy 0a8b87d574 add type to tensor optionally 2026-02-16 14:50:42 +01:00
mcDandy f0e13f430c add back layer and layer count and progress bar for model-like 2026-02-16 14:50:14 +01:00
mcDandy e3676d681f add type optional to tensor 2026-02-15 16:27:08 +01:00
mcDandy 9715cd273f Back to what AI created. I thought I could do better. 2026-02-15 16:10:27 +01:00
mcDandy e1f37d25e3 add shape function 2026-02-15 15:47:52 +01:00
mcDandy 0604551199 fix what AI messed up. 2026-02-15 12:20:04 +01:00
mcDandy 9b6c10b664 AI: fix some tests 2026-02-15 11:47:32 +01:00
mcDandy 7cd8e0362e ruff + AI: fix determnism 2026-02-14 23:04:17 +01:00
mcDandy f57f224fd6 AI: add noise 2026-02-14 22:38:28 +01:00
mcDandy a2aa2601d5 safety if we get tensor there 2026-02-14 11:42:59 +01:00
mcDandy 5dabd12c9a S{N} can be a number. 2026-02-14 11:42:26 +01:00
mcDandy 0817e50109 AI: fix reshape 2026-02-14 11:35:15 +01:00
mcDandy 04906ee2cb fix crashes 2026-02-13 23:54:11 +01:00
mcDandy 4a265c4768 grammer got reverted 2026-02-13 23:45:14 +01:00
mcDandy f05771b9b6 exzmples for this not needed 2026-02-13 23:17:26 +01:00
mcDandy 588a74cfeb add bit count, bit shifts + tests + restore what AI deleted 2026-02-13 23:15:50 +01:00
mcDandy 93209bf261 AI fix 2026-02-13 22:25:00 +01:00
mcDandy d766172383 AI: tests 2026-02-13 18:33:46 +01:00
mcDandy 0d65516cbb AI: rewrite bin_op and add fp16 support for bitwise operations 2026-02-13 16:23:03 +01:00
mcDandy 2a0fa503c9 AI fix and add fp8 support 2026-02-13 15:40:47 +01:00
mcDandy 9a9274f443 fix grammer 2026-02-13 14:06:42 +01:00
mcDandy 8c2eab8626 AI: bitwise - check later manually 2026-02-13 14:02:50 +01:00
mcDandy 2ae4674336 AI: fix mask 2026-02-13 14:02:03 +01:00
mcDandy 3d9e37b602 motion functions and permute fix 2026-02-12 08:51:20 +01:00
mcDandy 0ba8c8cf26 fix runtime errors + stack passing for noise 2026-02-10 17:29:21 +01:00
mcDandy 5f6f648636 stack is now partial execution safe 2026-02-10 17:08:06 +01:00
mcDandy 4bdbdd7bde forgot to add matmul and cross code 2026-02-10 16:25:46 +01:00
mcDandy 7c20d7d23e fix unknown characters 2026-02-10 15:41:34 +01:00
mcDandy 5072b191b7 Add tensor ops and parser support
Expose new tensor operations (softmax, softmin, argmin, argmax, unique, flatten, matmul, cross, and related tensor helpers) in the README and add corresponding grammar rules/tokens. Updated MathExpr.g4 to include new func productions and token definitions, and regenerated parser artifacts (MathExprLexer/Parser/Listener/Visitor/UnifiedMathVisitor and .interp/.tokens files). Also includes minor README formatting/typo fixes.
2026-02-10 15:38:47 +01:00
mcDandy f73a472324 Merge branch 'main' of https://github.com/mcDandy/more_math 2026-02-10 00:11:02 +01:00
mcDandy 4aeb009129 missed functions in readme 2026-02-10 00:10:59 +01:00
mcDandy d4cd834fb8 forgot to change video to output video... 2026-02-09 23:55:48 +01:00
mcDandy de65293966 batching 2026-02-09 23:54:11 +01:00
mcDandy 0010d31995 Update README with compatibility warning clarification
Clarified warning about compatibility with ComfyUI-Impact-Pack and ComfyUI-Ovi.
2026-02-09 19:40:02 +01:00
mcDandy b5f7ca83d0 Update README.md - add latest changes 2026-02-09 19:39:38 +01:00
mcDandy 7c5f644349 Clarify stack support in README
Updated README to clarify stack support and its usage.
2026-02-09 18:58:52 +01:00
mcDandy 4935791ebb rewrite index expression 2026-02-08 17:10:22 +01:00
mcDandy 6122ca4b0c unnecesery casting 2026-02-08 12:24:40 +01:00
mcDandy ebdaefbe14 forgot a zero 2026-02-08 11:16:11 +01:00
mcDandy fb3e41e54f fix 2026-02-08 11:10:25 +01:00
mcDandy b8b98cb758 add optional shape argument 2026-02-07 16:37:39 +01:00
mcDandy 74e5859366 Update README with new variable and function descriptions 2026-02-06 10:52:29 +01:00
mcDandy 29ecec9a9e return scalar instead of 1 element tensor 2026-02-06 10:28:31 +01:00
mcDandy ed2781d125 fix V not being acessable in some cases 2026-02-04 19:36:13 +01:00
mcDandy 0c1d3f52d6 argsort + add do nothing option on tensor size mismatch 2026-02-04 19:09:56 +01:00
mcDandy 8ffc9a394c AI refactor indexing 2026-02-04 14:05:01 +01:00
mcDandy f5e8c3ba8c fixed 2026-02-03 14:11:47 +01:00
mcDandy 38e795eb35 Better errors + batch shuffle (subject to rename) 2026-02-03 13:59:19 +01:00
mcDandy b06e26b82e Update ConditioningMathNode.py 2026-02-03 13:17:59 +01:00
mcDandy 7e637da47f dict ->{} 2026-02-03 13:17:39 +01:00
mcDandy e01a74d6ad add tensor creation and fix ruff 2026-02-03 10:17:43 +01:00
mcDandy 9d0f87e43f Int<->bool conversion 2026-02-02 20:44:20 +01:00
mcDandy d53d725fcc AI fix stack and large refactor + add count 2026-02-02 20:16:22 +01:00
mcDandy a1d1ce8c03 change stack to dict 2026-02-02 18:59:30 +01:00
mcDandy 881a18d392 added stack prefix to stack operations 2026-02-02 15:17:59 +01:00
mcDandy eae6b03f25 Add stack passing 2026-02-02 12:48:13 +01:00
mcDandy ac0c2f43d3 Add None. It is sometimes usefull to create empty variable 2026-02-01 17:26:53 +01:00
mcDandy 34eb4837cd how did this get there 2026-02-01 16:52:26 +01:00
mcDandy e501753407 AI: add F[x] along with cound and fixes 2026-02-01 16:52:09 +01:00
mcDandy 93c4559bfd add optional semicolon 2026-02-01 16:45:32 +01:00
mcDandy c4b270bc96 AI add a variable which contains all input V variables
V0 = V[0]
...
2026-02-01 16:33:33 +01:00
mcDandy 3467cf0663 (AI) set value based on position 2026-02-01 15:21:32 +01:00
mcDandy 4c5ef98571 (AI)get_value + crop 2026-02-01 15:20:01 +01:00
mcDandy 2ee4dfd507 crop, get_value + formating 2026-02-01 15:19:24 +01:00
mcDandy 270a539d91 Addid scientific notation. I forgot to commit. 2026-02-01 15:18:19 +01:00
mcDandy ce0c620a36 did I not puth it? 2026-02-01 15:17:23 +01:00
mcDandy 44e5bcc0e2 (BREAKING) (AI): using [,] to repeat batches was STUPID. Why did I even thought it was a good idea? 2026-02-01 15:15:48 +01:00
mcDandy c537f72d63 fix bug 2026-02-01 00:14:26 +01:00
mcDandy ef4c0c6f78 ability to batch latent + fixes 2026-01-31 12:27:19 +01:00
mcDandy a84a3b16e0 add support for scientific notation 2026-01-30 23:27:20 +01:00
mcDandy e13e3ac013 fixed an issue when (probably) multiplying list and tensor 2026-01-30 21:22:58 +01:00
mcDandy 9e2e4d4db6 make conditioning math batchable 2026-01-30 18:47:51 +01:00
mcDandy db8000d51e add device info to visitor and fixed forgotten ' 2026-01-30 14:22:13 +01:00
mcDandy a84c4f3048 fix stack not behaving correctly on model,vae and clip 2026-01-30 11:38:13 +01:00
mcDandy 03dcb7c424 aded break and continue 2026-01-30 11:35:20 +01:00
mcDandy e2bf6f1a20 Change to exception-less return
It was giving me problems in ModelMath
2026-01-29 17:26:17 +01:00
mcDandy e7d913ccd0 add time measurement and updated readme 2026-01-29 12:54:53 +01:00
mcDandy 13e5b4e800 Add comments 2026-01-29 11:19:02 +01:00
mcDandy 769c6a47a7 fixed input Float correctly deprecated 2026-01-29 11:18:36 +01:00
mcDandy 102902ec68 Forgot to deprecate FloatMath correctly 2026-01-28 19:59:23 +01:00
mcDandy b7d0c793c0 forgot to fix floats not being acknowlagled as existing 2026-01-28 19:15:22 +01:00
mcDandy 0d8cc9c2fe fix longstanding bug of tensor OP list 2026-01-28 19:09:15 +01:00
mcDandy 4ac582c045 fix + node conversion
Fix conditioning not knowing about Float inputs;
Convert VideoMathNode to new format
2026-01-28 17:53:56 +01:00
mcDandy 5a801ecbd2 tile,error,pad everywhere 2026-01-28 16:02:14 +01:00
mcDandy 380021fdf1 Add stack 2026-01-27 19:35:23 +01:00
mcDandy c3b7822ad2 (AI) merge some features from Computer architecture class project 2026-01-27 13:41:15 +01:00
mcDandy 5a8466b4df wrong backtick-like characters. 2026-01-25 23:42:59 +01:00
mcDandy 5574fe6627 Did I forgot commit these? + AI refactor 2026-01-25 22:43:13 +01:00
mcDandy a395bbf3b4 Update ImageMathNode.py 2026-01-25 22:21:15 +01:00
mcDandy f6413f4202 Refactor math nodes and update documentation
Improved error messages in AudioMathNode and refactored ConditioningMathNode to simplify input handling and output structure. Updated README to clarify blur and edge function behaviors. Removed redundant 'category' field from deprecated node schemas to hide them.
2026-01-25 22:20:54 +01:00
mcDandy bf3d12fc73 Update README.md 2026-01-25 22:16:12 +01:00
mcDandy 1a80777aac optional auto-permute for gausian and edge 2026-01-25 22:06:54 +01:00
mcDandy 2794623194 (AI) fix 2026-01-25 14:49:55 +01:00
mcDandy c7237b36fd (AI) blur, edge, botk/topk-index 2026-01-25 14:49:31 +01:00
mcDandy c5b53df261 remap, dist 2026-01-25 14:34:02 +01:00
mcDandy 9e3e21da4c any, all, easings, cumulative sum and product 2026-01-25 11:42:09 +01:00
mcDandy 4a0011514e fix linter errors 2026-01-25 10:58:05 +01:00
mcDandy 5d634ecaf4 Added ternary expression
Little higher then in other languages (tired of needing to bracket when the compExpr is more then variable)

Also IT SHOULD BE TURING COMPLETE NOW
2026-01-25 00:03:56 +01:00
mcDandy 3b53a30591 ruff check --fix 2026-01-24 23:28:40 +01:00
mcDandy 3bd8700201 (AI) fixes 2026-01-24 23:25:01 +01:00
mcDandy 0fcb7bf7bd (AI) implement the rest 2026-01-24 23:15:03 +01:00
mcDandy c61aec2faf add function which replaces NaN, inf and -inf 2026-01-24 14:46:50 +01:00
mcDandy a870ca0689 add ability to declare custom variables 2026-01-24 11:46:57 +01:00
mcDandy a13eb5a965 Add backwards compatibility back 2026-01-22 18:41:00 +01:00
mcDandy 25ee8cc1c5 (AI) fix 2026-01-22 18:25:38 +01:00
mcDandy cc10c1eb8e It does not work for some reason. 2026-01-22 17:47:11 +01:00
mcDandy d3921204ae Fix audio math node and make it visible 2026-01-22 15:21:22 +01:00
mcDandy 947393c6cf Make expression expression 2026-01-22 13:40:28 +01:00
mcDandy 63a0c22da3 It padded even thought mode was error 2026-01-22 13:40:01 +01:00
mcDandy 1294bfe15e Audio is now autogrow 2026-01-22 13:39:32 +01:00
mcDandy 866666060c Sampler node do not expose anything new 2026-01-22 11:38:50 +01:00
mcDandy e05dfe289f Merge branch 'main' of https://github.com/mcDandy/more_math 2026-01-22 11:34:55 +01:00
mcDandy cdd3a4992d Update GuiderMathNode.py 2026-01-22 11:34:52 +01:00
mcDandy 2e87cd861a Update README to simplify GUIDER reference 2026-01-22 10:53:23 +01:00
mcDandy 8783642393 Convertend Noise to use variable inputs 2026-01-21 23:56:35 +01:00
mcDandy c1024002f4 breaking: sgimas and guider - change node id 2026-01-21 11:03:13 +01:00
mcDandy 6631495bbe Change variable from G to V to step around ComfyUI bug 2026-01-20 23:40:40 +01:00
mcDandy 8bf3329e92 ruff fix 2026-01-20 23:34:12 +01:00
mcDandy 1dbd3f076d quick fixes 2026-01-20 23:33:24 +01:00
mcDandy 86ed9640cb fix typo 2026-01-20 22:03:25 +01:00
mcDandy b5c8541a48 (ai) (dirty) add guide math node 2026-01-20 22:02:12 +01:00
mcDandy a601dae138 error as default is better 2026-01-20 18:10:22 +01:00
mcDandy ce8af0532c missed a lot 2026-01-20 18:09:20 +01:00
mcDandy e1e0eb5e0f fix sigmasMath 2026-01-20 17:07:25 +01:00
mcDandy 83d149f950 Decided to go with V for autogrow inputs of output type 2026-01-20 15:37:10 +01:00
mcDandy 01c9a9a3c2 fix typo 2026-01-20 15:33:18 +01:00
mcDandy 15af4a4230 (ai) add tests 2026-01-20 13:20:54 +01:00
mcDandy 27079a0394 ai found some typos when writing tests for me 2026-01-20 13:20:40 +01:00
mcDandy b83e06e83f add random generators 2026-01-20 13:08:14 +01:00
mcDandy b36460e477 fix lerp function 2026-01-19 20:43:35 +01:00
mcDandy b59937d351 add append function 2026-01-19 20:43:19 +01:00
mcDandy 8e0b1c65f5 (ai) add custom functions 2026-01-19 17:22:23 +01:00
mcDandy a4b9abfc61 forgot to add SIGMAS to readme and pyproject 2026-01-19 16:59:30 +01:00
mcDandy 85d10e0b38 (ai) add tests 2026-01-19 15:43:16 +01:00
mcDandy 14f6fff9ea Added a window I missed. 2026-01-19 14:13:05 +01:00
mcDandy ceafc51aa3 (UNTESTED) Add sigmas math node with autogrow
REQUIRES https://github.com/Comfy-Org/ComfyUI_frontend/pull/8026 TO BE MERGED BEFORE IT WILL WORK
2026-01-18 12:43:17 +01:00
mcDandy 84744429e0 clarification 2026-01-17 15:39:23 +01:00
mcDandy 8d5709ba6f added cosine similarity, flip, covariance and sorting 2026-01-17 14:04:27 +01:00
mcDandy c733e4ab93 (AI) fix tests 2026-01-17 10:58:29 +01:00
mcDandy 1c8831f757 forgot to change name of option in readme 2026-01-14 23:18:53 +01:00
mcDandy 101e783e1e (AI) add option to pad or repeat input when size not matches
Nested tensors are broken, tests do not work since my cleanup after the AI.
2026-01-14 22:56:02 +01:00
mcDandy 26f4689210 new version 2026-01-13 23:21:22 +01:00
mcDandy 77f006ad0a bugfixes 2026-01-13 18:09:51 +01:00
mcDandy 6335071950 Add selector 2026-01-13 15:40:42 +01:00
mcDandy 22cc4ea1b4 ruff 2026-01-12 15:02:32 +01:00
mcDandy d04d386a16 (ai) add test for moment 2026-01-12 15:02:24 +01:00
mcDandy 4d3cc9d6ee fix commonLazy missing w,x,y,z and remove MathNodeBase 2026-01-12 14:56:37 +01:00
mcDandy bf5d0c73de make list OP tensor not multiply dimensions when list lenght is equal to tensor 2026-01-11 17:29:16 +01:00
mcDandy 5bfa6997c9 added info to readme 2026-01-11 15:24:04 +01:00
mcDandy c9cd557666 Added moment function
moment(x,0,1) = mean(x)
moment(x,mean(x),2) = var(x)
2026-01-11 15:12:35 +01:00
mcDandy 2cd69ce5fd ruff --fix. Again... 2026-01-10 15:34:06 +01:00
mcDandy cf02031ba2 (AI) add tests + forgot to change tests of functionalyty which changed 2026-01-10 15:32:04 +01:00
mcDandy 81a17c9352 run ruff 2026-01-10 15:25:34 +01:00
mcDandy 1b6b35f5d6 (AI) optimalise quantile and simillar for large count in quantile
passing large tensor into quantlie,quartile or percentile no longer takes minutes + limit testing with image from my phone (3072 x 4080 x 3)
2026-01-10 15:24:23 +01:00
mcDandy 43c3d060b0 add statistical functions 2026-01-10 14:13:44 +01:00
mcDandy 2db7fcc004 rewording of things 2026-01-10 11:38:44 +01:00
mcDandy 4e744a8e82 cleanup 2026-01-10 00:00:34 +01:00
mcDandy 08cb017e57 forgot a line 2026-01-09 23:29:41 +01:00
mcDandy fcc24fca04 (AI) refactor - Lexer and parser grammer are separated 2026-01-09 23:29:24 +01:00
mcDandy 7797c40c4d Merge branch 'main' of https://github.com/mcDandy/more_math 2026-01-09 23:06:40 +01:00
mcDandy 9e55ed1209 added the permute which does not rearrange dims. 2026-01-09 23:06:35 +01:00
mcDandy ec927d7e66 Update README to include MASK node in features 2026-01-09 22:04:29 +01:00
mcDandy bf164d0c6a Update description to include MASK type 2026-01-09 22:03:44 +01:00
mcDandy d3ed28041a Add mask math node 2026-01-09 22:02:29 +01:00
mcDandy e3cc2873a4 ruff fix 2026-01-09 20:34:57 +01:00
mcDandy 52a0874b0a Does not make sense for tensor 2026-01-09 16:23:47 +01:00
mcDandy 77f6979b7b ruff --fix on commit 2026-01-09 13:50:02 +01:00
mcDandy c519cb1dc9 Update MathExpr.g4 2026-01-09 13:28:56 +01:00
mcDandy f4edef96cc add topk, botk and convolution inverse function 2026-01-09 13:28:46 +01:00
mcDandy 1935b80212 Merge branch 'main' of https://github.com/mcDandy/more_math 2026-01-08 16:29:50 +01:00
mcDandy e6b5af1dae forgot v. AudioMathNode works again 2026-01-08 16:16:22 +01:00
mcDandy 48cf40ce16 Fix HTML entity encoding in README.md 2026-01-08 15:34:01 +01:00
mcDandy fc7636fb3f Fix formatting of layer and layer_count descriptions 2026-01-08 15:31:15 +01:00
mcDandy df7109cf67 forgot * 2026-01-08 15:25:41 +01:00
mcDandy a8e5651067 test: attempt to setup comfyui fully 2026-01-08 15:21:55 +01:00
mcDandy b970d0db47 (AI) attempt fixing tests 2026-01-08 15:15:54 +01:00
mcDandy 6df71eea84 (AI) try fix tests again 2026-01-08 15:02:41 +01:00
mcDandy d9cae971d7 (AI) fix tests 2026-01-08 14:47:47 +01:00
mcDandy 6e275722f2 missed a few 2026-01-08 14:28:41 +01:00
mcDandy f76b77c584 fix ruff + ignore errors in auto-generated files 2026-01-08 14:22:53 +01:00
mcDandy f273d74ae6 run ruff 2026-01-08 14:14:44 +01:00
mcDandy 01cbdf2e26 naming consistency 2026-01-08 13:20:03 +01:00
mcDandy f4c5af00b4 Update README - add long forgotten variables 2026-01-06 18:27:56 +01:00
mcDandy bc331154bb Update README with tensor and list operation details
Clarified usage of batched and non-batched tensors in README.
2026-01-05 16:52:39 +01:00
mcDandy 5d2afd3de6 Merge branch 'main' of https://github.com/mcDandy/more_math 2026-01-05 14:19:11 +01:00
mcDandy 9a59708688 ensure output is tensor when tensor is expacted 2026-01-05 14:19:07 +01:00
mcDandy 251f9e74cd Fix formatting and clarify list operations in README 2026-01-04 22:59:01 +01:00
mcDandy 7347b605e2 Fix typo in README.md for print_shape function 2026-01-04 22:55:55 +01:00
mcDandy 69efc6f0c7 Enhance README with list operations details
Clarified list usage in vector math and added examples.
2026-01-04 22:55:25 +01:00
mcDandy 3c948c9ddd add range and using listst to duplicate batches and do operation with different value on each
Works only with operators. Does not work with functions
2026-01-04 21:10:37 +01:00
mcDandy c978412177 better automatic shuffling dimensions for conv (will prob remove it entirly) 2026-01-04 18:54:23 +01:00
mcDandy bbdc7c5ec7 remove unneeded comments 2026-01-04 18:51:41 +01:00
mcDandy 9a34eb0ca2 |list| should behave like |tensor| 2026-01-04 18:50:43 +01:00
mcDandy 16fc9a0258 add reshape function + readme touches 2026-01-04 18:49:43 +01:00
mcDandy b19f582ea5 tests 2026-01-03 00:44:12 +01:00
mcDandy acc2fbfc79 Unite tensor and float visitors. 2026-01-03 00:43:38 +01:00
mcDandy a9322ab055 remove debug prints 2025-12-30 15:32:52 +01:00
mcDandy 47f4d1309d fix map() 2025-12-29 21:34:12 +01:00
mcDandy 744931c6e3 Added a node to convert audio to spectrogram and spectrigram to audio 2025-12-29 20:04:58 +01:00
mcDandy 1ca839e0d9 Update README - setup instructions
Added steps to activate environment and install antlr.
2025-12-29 19:31:30 +01:00
mcDandy 9ecd60554e Update README with new features and clarifications 2025-12-29 16:43:11 +01:00
mcDandy c930fbe6eb cleanup 2025-12-29 16:23:08 +01:00
mcDandy 458901bb2a Fix map function + add common variables
should have added them from beginning instead of giving custom names to them. Will not be deleting that.
2025-12-27 17:58:13 +01:00
mcDandy e44fd922d4 added map,conv and permute
map - maps points based on equations
conv - allows running convolution filter
permute - switches dimension position
2025-12-26 18:42:47 +01:00
mcDandy ba752c4b55 code cleanup (AI) 2025-12-26 18:21:24 +01:00
mcDandy be29ad7f67 massive refactor (AI) 2025-12-25 19:43:09 +01:00
mcDandy 7fad03daf1 torch.full apparently not needed 2025-12-25 16:49:49 +01:00
mcDandy 6756e43749 cleanup swap function 2025-12-25 16:21:14 +01:00
mcDandy d8c2e90b43 push version containing lazy evaluation of nodes 2025-12-25 16:13:07 +01:00
mcDandy 8573bcf25a remove debug info 2025-12-23 20:58:20 +01:00
mcDandy 10074824d2 Fix for lazy evaluation. Forgot to return value. 2025-12-23 20:13:48 +01:00
mcDandy 1843ee3cc3 attempt to fix lazy inputs 2025-12-23 09:51:13 +01:00
mcDandy 3a55fbe33a Made inputs lazy when possible 2025-12-22 00:02:56 +01:00
mcDandy 7ec9b9c92e split ModelMathNode to 3 files - each node in separate file 2025-12-21 23:32:37 +01:00
mcDandy 7f0a05e035 ran README through linter + incompatibility 2025-12-21 15:06:15 +01:00
mcDandy 8e2c3328ff Forgot to add newly supported types 2025-12-19 23:42:14 +01:00
mcDandy 1331396a65 Merge branch 'main' of https://github.com/mcDandy/more_math 2025-12-19 23:36:50 +01:00
mcDandy f2eff3d494 New node version 2025-12-19 23:36:33 +01:00
mcDandy 6cd7f4375b Update README.md - consistency 2025-12-19 23:10:19 +01:00
mcDandy 1526bed627 step function consistency 2025-12-19 23:10:01 +01:00
mcDandy 429b093d5d remove excesive comments 2025-12-19 23:05:24 +01:00
mcDandy 0876875c59 Make smoothstep consistent 2025-12-19 23:05:06 +01:00
mcDandy 967188b681 Added || shorthand for abs and snorm 2025-12-19 22:53:08 +01:00
mcDandy ab518d30cc fix error 2025-12-19 21:58:34 +01:00
mcDandy 77d283cb6a AI: tests for model math 2025-12-19 21:56:02 +01:00
mcDandy ae7d6592c1 AI extend tests 2025-12-17 00:10:46 +01:00
mcDandy 5b04fac197 Added VAE, CLIP and MODEL math nodes 2025-12-16 21:21:43 +01:00
mcDandy 3d0e9adf61 spaces 2025-12-16 20:21:07 +01:00
mcDandy 21b87535b1 Make sure computation do not cross device boundaries 2025-12-16 20:20:33 +01:00
mcDandy d46d85b237 Cleaner functions 2025-12-16 20:19:48 +01:00
mcDandy ebcb022cb4 Fixed conditioning and noise math node 2025-12-11 22:59:05 +01:00
mcDandy f04c2320ed Moved more_math out of src, apparently it is done like that. 2025-12-11 21:35:57 +01:00
mcDandy ef2650c48c Merge branch 'main' of https://github.com/mcDandy/more_math 2025-12-11 19:08:12 +01:00
mcDandy 10d3e5a02d try fix tests 2025-12-11 19:08:02 +01:00
mcDandy 589445b32d Add requirements installation step to build pipeline 2025-12-11 19:01:22 +01:00
mcDandy ac027e2cac Add torch to requirements - tests require it 2025-12-11 18:58:11 +01:00
mcDandy fe771feda3 MORE LINTER FIXES 2025-12-11 18:55:30 +01:00
mcDandy 561a567c15 Update test_more_math.py 2025-12-11 18:51:15 +01:00
mcDandy d82e74c3bb Merge branch 'main' of https://github.com/mcDandy/more_math 2025-12-11 18:48:23 +01:00
mcDandy d418e8eec3 fIX LINter eRRors 2025-12-11 18:48:10 +01:00
mcDandy ea8d26e534 Fixed action to working state 2025-12-11 18:38:19 +01:00
mcDandy ccb533536a Add push trigger to CI build workflow
Accidental revert
2025-12-11 18:35:04 +01:00
mcDandy 11b5ffcc3a made linter happy by removing all trailing spaces and unused imports 2025-12-11 18:33:27 +01:00
mcDandy 4abd03eb94 remove tooltip for testing- used copilot and I am lazy to remove lines which do nothing
Added steps to backup, modify, and restore pyproject.toml during CI.
2025-12-11 17:52:56 +01:00
mcDandy fce5f2748d Try fix runner not runing 2025-12-11 17:37:17 +01:00
mcDandy 00dc764a7c Add push trigger to CI build workflow 2025-12-10 14:35:33 +01:00
mcDandy bffdac5c47 Force těsta to run 2025-12-10 14:32:49 +01:00
mcDandy 38d8d4c5b4 Delete .github/workflows/test.yml (there was allready test file) 2025-12-10 12:23:31 +01:00
mcDandy 6595a3e53b Remove 'master' branch from pull request triggers - using main 2025-12-10 12:21:37 +01:00
mcDandy 46294b9571 Add GitHub Actions workflow for Python app testing 2025-12-10 11:08:11 +01:00
mcDandy c30fdc2286 Attempt fix for conditioning if pooled_output exists 2025-12-09 12:30:19 +01:00
mcDandy 729aaf488a Merge branch 'main' of https://github.com/mcDandy/more_math 2025-12-08 21:38:59 +01:00
mcDandy 45e2619033 Consistency - tensor allways first 2025-12-08 21:38:56 +01:00
mcDandy f10b53c9b6 Bit of a cleanup 2025-12-08 21:38:13 +01:00
mcDandy dfbf0f6f2c fix problems with conditioning with flux2 2025-12-08 21:20:57 +01:00
mcDandy d419585fcd Split tests so each one tests one thing 2025-12-05 22:45:58 +01:00
mcDandy e673ba4349 Add tests 2025-12-05 20:41:58 +01:00
mcDandy fa8c415eb9 Added more functions 2025-12-05 20:20:35 +01:00
mcDandy b1c96340da Update README.md 2025-12-05 17:22:15 +01:00
mcDandy 3f47fc5916 fix latent math and noise math nodes. It should now work. 2025-12-05 17:13:26 +01:00
mcDandy e4faaded4b added fft/ifft compatibilty for other types 2025-12-05 16:42:43 +01:00
mcDandy bf77b39f4b fix to a fix when pooled_output is None 2025-11-30 16:02:42 +01:00
mcDandy 670df99a5f fix error on flux2 2025-11-27 20:46:07 +01:00
mcDandy ec2714aa78 cleanup 2025-11-27 20:45:53 +01:00
mcDandy a297c002c3 Copilot: Hopefully fixed 5D tensors. 2025-11-24 15:03:03 +01:00
mcDandy f6205ba530 change variables formatting 2025-11-24 11:32:40 +01:00
mcDandy 1415f1a18e Copilot: add support for 5D tensor 2025-11-24 10:44:05 +01:00
mcDandy 8ed56e8f44 FFT and sFFT support long name variable 2025-11-21 09:40:11 +01:00
mcDandy cf0af6f241 remove spaces 2025-11-21 09:39:22 +01:00
mcDandy 4434f9432e remove enters 2025-10-25 17:00:15 +02:00
mcDandy 439a1947cd checked for \u000a. No such character exists at that position. 2025-10-25 16:57:44 +02:00
mcDandy ec7a74ca72 V 0.2 2025-10-25 16:45:09 +02:00
mcDandy 8c9e054602 forgot a letter 2025-10-16 13:48:04 +02:00
mcDandy 6fb30e197d fixeed long video variables 2025-10-16 13:46:44 +02:00
mcDandy c272899e64 clarification 2025-10-16 13:45:30 +02:00
mcDandy 747c08e457 Update README.md - add more human readable variable variations 2025-10-15 12:45:01 +02:00
mcDandy cad71dd6e6 add more human readable variable variations 2025-10-15 12:44:46 +02:00
mcDandy 7d5893b935 Update README.md 2025-10-14 23:10:39 +02:00
mcDandy 7d3ceb5009 Added Video math node 2025-10-14 23:01:34 +02:00
mcDandy 750797d06d remove forgotten debug prints 2025-10-05 21:57:45 +02:00
mcDandy 853bc12ef1 Removed debug logging 2025-10-05 21:46:20 +02:00
mcDandy f3d34b3146 add print function 2025-10-05 18:22:15 +02:00
mcDandy ad9fff5298 ignore project file 2025-09-14 21:22:46 +02:00
mcDandy 9581e9ac0c ignore project file 2025-09-14 21:21:31 +02:00
mcDandy 249fa66d94 Merge branch 'main' of https://github.com/mcDandy/more_math 2025-09-05 20:26:17 +02:00
mcDandy 45d513541b Add description 2025-09-05 20:26:14 +02:00
mcDandy 7016f19ae9 Update pyproject.toml
change node spec version from v1 to v3, added audio math node
2025-09-05 20:14:12 +02:00
mcDandy cf62329b8c forgot noise 2025-09-05 20:07:37 +02:00
mcDandy 5f11e40a4b fix names 2025-09-05 20:02:09 +02:00
mcDandy 93cca1f399 fix noise math node 2025-09-05 15:51:25 +02:00
mcDandy 40a2a19564 remove self, making nodes non-functional 2025-09-05 14:17:08 +02:00
mcDandy 367d960309 Convert nodes to V3 2025-09-04 21:57:48 +02:00
mcDandy c24feebac1 Fixed another bug with fft/ifft 2025-09-03 19:06:57 +02:00
mcDandy 74ef4d9743 Merge branch 'main' of https://github.com/mcDandy/more_math 2025-09-03 13:41:09 +02:00
mcDandy e609fb6ca7 mixup of sample and sample count in fft 2025-09-03 13:41:02 +02:00
mcDandy 2799c6257e Forgot to reshape S in fft so it can be used 2025-09-03 13:38:20 +02:00
mcDandy f04f76f1ea Update README.md 2025-08-31 10:31:35 +02:00
mcDandy 95287f3c89 Update README.md 2025-08-30 17:25:57 +02:00
mcDandy bb579b21ad fixed 2025-08-30 17:24:22 +02:00
mcDandy 441987b5f5 partial fix 2025-08-30 15:27:07 +02:00
mcDandy 85fc329364 broken 2025-08-30 13:14:34 +02:00
143 changed files with 95256 additions and 6415 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
* Python version:
* Operating System:
### Description
### tooltip
Describe what you were trying to get done.
Tell us what happened, what went wrong, and what you expected to happen.
+54 -3
View File
@@ -1,13 +1,15 @@
name: "{{ cookiecutter.project_slug }} CI build"
# GitHub CI build pipeline
name: CI build
on:
pull_request:
push:
branches:
- master
- main
jobs:
build:
runs-on: "{% raw %} ${{ matrix.os }} {% endraw %}"
runs-on: ubuntu-latest
env:
PYTHONIOENCODING: "utf8"
strategy:
@@ -17,17 +19,66 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Backup pyproject.toml
run: cp pyproject.toml pyproject.toml.ci.bak
- name: Strip tooltip entries from pyproject.toml for CI
run: |
# Remove `tooltip = ...` from [project] and drop [project.tooltip] section if present.
python - <<'PY'
from pathlib import Path
p = Path('pyproject.toml')
text = p.read_text()
out_lines = []
cur_section = None
skip_section = False
for line in text.splitlines():
s = line.strip()
if s.startswith('[') and s.endswith(']'):
cur_section = s.strip('[]')
# skip any subsection that starts with project.tooltip (or exact match)
if cur_section == 'project.tooltip' or cur_section.startswith('project.tooltip'):
skip_section = True
continue
else:
skip_section = False
if skip_section:
continue
# drop top-level tooltip = ... when inside [project]
if cur_section == 'project' and s and s.split('=')[0].strip() == 'tooltip':
continue
out_lines.append(line)
p.write_text('\n'.join(out_lines) + '\n')
PY
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "{% raw %} ${{ matrix.python-version }} {% endraw %}"
python-version: 3.12
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[dev]
pip install -r requirements.txt
- name: Clone ComfyUI dependency
run: |
git clone --depth 1 https://github.com/comfyanonymous/ComfyUI.git ../ComfyUI-repo
cp -r ../ComfyUI-repo/* ../
python -m pip install -r ../ComfyUI-repo/requirements.txt
- name: Run Linting
run: |
ruff check .
- name: Run Tests
env:
PYTHONPATH: .
run: |
pytest tests/
- name: Restore original pyproject.toml
if: always()
run: mv pyproject.toml.ci.bak pyproject.toml
+4
View File
@@ -116,3 +116,7 @@ node.zip
/.vs
*.editorconfig
*.yaml
!.pre-commit-config.yaml
/more_math.pyproj
/more_math.slnx
/more_math/Parser/.antlr
+9
View File
@@ -0,0 +1,9 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
# Run the linter
- id: ruff
args: [--fix]
# Run the formatter
- id: ruff-format
+212
View File
@@ -0,0 +1,212 @@
# Function Consistency Check: README.md vs MathExpr.g4
## Executive Summary
✅ **All documented functions have grammar rules defined**
✅ **All grammar functions have documentation**
⚠️ **Minor discrepancies in aliases and function naming**
## Functions in README but Need Verification in Grammar
### ✅ Verified - All Present
#### Basic Math (All Present)
- ✅ abs (AbsFunc)
- ✅ sqrt (SqrtFunc)
- ✅ ln (LnFunc)
- ✅ log (LogFunc)
- ✅ exp (ExpFunc)
- ✅ pow (PowFunc as POWE)
- ✅ floor (FloorFunc)
- ✅ ceil (CeilFunc)
- ✅ round (RoundFunc)
- ✅ fract (FractFunc)
- ✅ sign (SignFunc)
- ✅ gamma (GammaFunc)
- ✅ dist/distance (DistFunc)
- ✅ clamp (ClampFunc)
- ✅ step (StepFunc)
#### Trigonometric (All Present)
- ✅ sin, cos, tan, asin, acos, atan, atan2 (all present)
#### Hyperbolic (All Present)
- ✅ sinh, cosh, tanh, asinh, acosh, atanh (all present)
#### Machine Learning / Activation (All Present)
- ✅ relu (ReluFunc)
- ✅ gelu (GeluFunc)
- ✅ softplus (SoftplusFunc)
- ✅ sigm (SigmoidFunc as SIGM)
- ✅ softmax (SoftmaxFunc)
- ✅ softmin (SoftminFunc)
#### Interpolation (All Present)
- ✅ smoothstep (SmoothstepFunc)
- ✅ smootherstep (SmootherstepFunc)
- ✅ cubic_ease/cubic (CubicEaseFunc)
- ✅ sine_ease/sine (SineEaseFunc)
- ✅ elastic_ease/elastic (ElasticEaseFunc)
- ✅ lerp (LerpFunc)
#### Aggregates & Tensor Operations (All Present)
- ✅ tmin (TMinFunc)
- ✅ tmax (TMaxFunc)
- ✅ smin (SMinFunc)
- ✅ smax (SMaxFunc)
- ✅ sum (SumFunc)
- ✅ mean (MeanFunc)
- ✅ std (StdFunc)
- ✅ var (VarFunc)
- ✅ median (MedianFunc)
- ✅ mode (ModeFunc)
- ✅ quartile (QuartileFunc)
- ✅ percentile (PercentileFunc)
- ✅ quantile (QuantileFunc)
- ✅ dot (DotFunc)
- ✅ moment (MomentFunc)
- ✅ topk (TopkFunc)
- ✅ botk (BotkFunc)
- ✅ topk_ind/topk_indices (TopkIndFunc)
- ✅ botk_ind/botk_indices (BotkIndFunc)
- ✅ sort (SortFunc)
- ✅ argsort (ArgsortFunc)
- ✅ argmin (ArgminFunc)
- ✅ argmax (ArgmaxFunc)
- ✅ unique (UniqueFunc)
- ✅ tnorm (TNormFunc)
- ✅ snorm (SNormFunc)
- ✅ swap (SwapFunc)
- ✅ cossim (CossimFunc)
- ✅ flip (FlipFunc)
- ✅ cov (CovFunc)
- ✅ append (AppendFunc)
- ✅ any (AnyFunc)
- ✅ all (AllFunc)
- ✅ cumsum (CumsumFunc)
- ✅ cumprod (CumprodFunc)
- ✅ tensor (EmptyTensorFunc)
- ✅ flatten (FlattenFunc)
#### Advanced Tensor Operations (All Present)
- ✅ map (MapFunc)
- ✅ ezconvolution/ezconv (EzConvFunc)
- ✅ convolution/conv (ConvFunc)
- ✅ get_value (GetValueFunc)
- ✅ crop (CropFunc)
- ✅ permute/perm (PermuteFunc)
- ✅ reshape/rshp (ReshapeFunc)
- ✅ blur/gaussian (GaussianFunc)
- ✅ edge (EdgeFunc)
- ✅ batch_shuffle/shuffle/select (BatchShuffleFunc)
- ✅ matmul (MatmulFunc)
- ✅ cross (CrossFunc)
#### Optical Flow (All Present)
- ✅ rife (RifeFunc)
- ✅ motion_mask (MotionMaskFunc)
- ✅ flow_to_image (FlowToImageFunc)
- ✅ flow_apply (FlowApplyFunc)
#### FFT (All Present)
- ✅ fft (SFFT)
- ✅ ifft (SIFFT)
- ✅ angle (ANGL)
#### Utility (All Present)
- ✅ print (PrintFunc as PRNT)
- ✅ print_shape/pshp (PrintShapeFunc)
- ✅ pinv (PinvFunc)
- ✅ range (RangeFunc)
- ✅ nan_to_num/nvl (NvlFunc)
- ✅ remap (RemapFunc)
- ✅ timestamp (TimestampFunc as TIMESTAMP)
- ✅ count/length/cnt (CountFunc)
#### Random Distributions (All Present)
- ✅ random_normal/randn/noise (NoiseFunc)
- ✅ random_uniform/rand (RandFunc)
- ✅ random_exponential/rande (ExponentialFunc)
- ✅ random_cauchy/randc (CauchyFunc)
- ✅ random_log_normal/randln (LogNormalFunc)
- ✅ random_bernoulli/randb (BernoulliFunc)
- ✅ random_poisson/randp (PoissonFunc)
- ✅ random_gamma/randg (GammaDistFunc)
- ✅ random_beta/randbeta (BetaDistFunc)
- ✅ random_laplace/randl (LaplaceDistFunc)
- ✅ random_gumbel/randgumbel (GumbelDistFunc)
- ✅ random_weibull/randw (WeibullDistFunc)
- ✅ random_chi2/randchi2 (Chi2DistFunc)
- ✅ random_studentt/randt (StudentTDistFunc)
#### Bitwise Operations (All Present)
- ✅ band/bitwise_and (BitAndFunc)
- ✅ bor/bitwise_or (BitOrFunc)
- ✅ xor/bitwise_xor (BitXorFunc)
- ✅ bnot/bitwise_not (BitNotFunc)
- ✅ bitcount/popcount/popcnt (BitCountFunc)
- ✅ << (LSHIFT)
- ✅ >> (RSHIFT)
#### Stack (All Present)
- ✅ stack_push (PushFunc)
- ✅ stack_pop (PopFunc)
- ✅ stack_get (GetFunc)
- ✅ stack_clear (ClearFunc)
- ✅ stack_has (HasFunc)
## Minor Naming Discrepancies
### README Mentions These Aliases Not Explicitly in Grammar Lexer
1. `now` - README mentions as alias for `timestamp`, but only TIMESTAMP: 'timestamp' is in grammar
- **Fix Needed**: Add `TIMESTAMP: 'timestamp' | 'now';`
2. `flow_view` - README mentions as alias for `flow_to_image`
- **Fix Needed**: Add to `FLOW_TO_IMAGE: 'flow_to_image' | 'flow_view';`
3. `apply_flow` - README mentions as alias for `flow_apply`
- **Fix Needed**: Add to `FLOW_APPLY: 'flow_apply' | 'apply_flow';`
4. Bitwise aliases - Should update for consistency:
- `BAND: 'band' | 'bitwise_and' | 'bitand';` (currently has 'bitand' as alias)
- `BOR: 'bor' | 'bitwise_or' | 'bitor';` (currently has 'bitor' as alias)
- `XOR: 'xor' | 'bitwise_xor' | 'xor';` (should add 'bitwise_xor')
- `BNOT: 'bnot' | 'bitnot' | 'bitwise_not';` (should add 'bitwise_not')
## Summary of Issues
### Critical (Must Fix)
None - all documented functions work
### High Priority (Should Fix)
1. Add missing aliases for consistency:
- `now` for `timestamp`
- `flow_view` for `flow_to_image`
- `apply_flow` for `flow_apply`
- `bitwise_*` names for bitwise operators
### Low Priority (Nice to Have)
- Documentation in README could list all aliases explicitly
## Recommended Grammar Updates
```antlr
// Update these lexer rules:
TIMESTAMP: 'timestamp' | 'now';
FLOW_APPLY: 'flow_apply' | 'apply_flow';
FLOW_TO_IMAGE: 'flow_to_image' | 'flow_view';
BAND: 'band' | 'bitand' | 'bitwise_and';
BOR: 'bor' | 'bitor' | 'bitwise_or';
XOR: 'xor' | 'bitwise_xor';
BNOT: 'bnot' | 'bitnot' | 'bitwise_not';
BITCOUNT: 'bitcount' | 'popcount' | 'popcnt' | 'bitcount';
```
## Conclusion
✅ **Status: EXCELLENT CONSISTENCY**
- 100% of documented functions have grammar rules
- 100% of grammar functions have documentation
- Only minor alias discrepancies that don't affect functionality
- All core functionality is properly defined and documented
**Recommendation**: Regenerate parser after applying the minor grammar updates above to ensure all documented aliases work correctly.
+543 -30
View File
@@ -1,43 +1,556 @@
# More Math
Adds math nodes for numbers and types which do not need it. I got inspired by was_extras node.
Adds math nodes for numbers and types which do not need it. Inspired by the `was_extras` node.
#WARNING This node is not compatible to ComfyUI-Impact-Pack which forces older antlr version.
There is workflow folder containing examples. It will expand.
The most important thing is that last part of input must be an expression. Node uses it to output result. It can be constant, function call, variable or mixture of these. No loops, branching using if. Branches, loops, ... can be before the final expression.
## Quickstart
1. Install [ComfyUI](https://docs.comfy.org/get_started).
1. Clone this repository into `ComfyUI/custom_nodes`.
1. open command prompt/terminal/bash in your comfy folder
1. activate environment `./venv/Scripts/activate`
1. go to more_math folder `cd ./custom_nodes/more_math/`
1. install requirements `pip install -r requirements.txt`
1. Restart ComfyUI.
You can also get the node from comfy manager under the name of More math.
# Features
## Features
- expression parsing with support for functions and variables:
- functions:
- run expressions on both components of CONDITIONING (not that I know what they do), LATENT, IMAGE, NOISE and FLOAT
- math operators (binary and unary +,- and ^,*,/,%)
- boolean operators (false = 0.0; true = 1.0) (<,<=,>,>=,==,!=)
- sin, cos, tan
- asin, acos, atan (can produce NaN outside range -1...1)
- atan2 (x,y)
- sinh, cosh, tanh
- asinh, acosh, atanh (can produce NaN outside range -1...1)
- abs, sqrt (produces NaN on negative numbers), ln (log with base e), log (base 10), exp, pow
- smin, tmin, smax, tmax, tnorm, snorm (norm and t prefixed are not applicable on float math node)
- s prefixed return one number
- t prefixed work element-wise
- floor, ceil, round, gamma, clamp, sigm (sigmoid)
- angle (audio node)
- variables
- a, b, c, d : inputs of type based on node
- w, x, y, z : each node allows use of 4 additional floats
- W : Width of tensor - does not work on FLOAT and CONDITIONING
- H : Height of tensor - does not work on FLOAT and CONDITIONING
- C : Channel of tensor - used as color chanel in IMAGE or as information channel in others. does not work on FLOAT and CONDITIONING
- B : Batch - does not work on FLOAT and CONDITIONING
- X : X position in tensor - does not work on FLOAT and CONDITIONING
- Y : Y position in tensor - does not work on FLOAT and CONDITIONING
- I : Tensor used to initalise sampler before noise injection - usually zeroed out and generated by Empty Latent Image node - Exclusive for NOISE
- use constant e and pi
- Functions and variables in math expressions.
- Conversion between `INT` and `FLOAT`, `INT` and `BOOLEAN`, and `AUDIO` and `IMAGE`
- image/audio conversion encodes frequency content into RGB channels.
- Node support for `FLOAT`, `STRING`, `CONDITIONING`, `LATENT`, `IMAGE`, `MASK`, `NOISE`, `AUDIO`, `VIDEO`, `MODEL`, `CLIP`, `VAE`, `SIGMAS`, and `GUIDER`.
- Vector math with list literals `[v1, v2, ...]` and mixed operations between lists, scalars, and tensors.
- Custom functions `funcname(variable, variable, ...)->expression;`
- can be used later in expressions and in other custom functions,
- shadowing built-in functions is not supported,
- recursion is allowed, so be careful.
- Custom variables `varname=expression;`
- can be used later in the same expression chain,
- compound assignments are supported: `+=`, `-=`, `*=`, `/=`, `%=`.
- Indexed assignment `a[i, j, ...] = expression;`
- supports multidimensional tensors and nested lists,
- scalar values and 1-element tensors/lists fill the selected slice,
- leading singleton dimensions are squeezed when needed to match target rank.
- python-like slicing
- `a[start:end]` or `a[start:end:step]` for 1D slices,
- `a[start1:end1, start2:end2, ...]` for N-D slices (step can also be specified),
- negative indices count from the end of the dimension.
- json-like dictionary literals `{key1: value1, key2: value2, ...}` for structured data. Keys can be strings or numbers or variables (variable will be evaluated to its value), values can be any expression. Indexing using [].
- Control flow:
- `if/else`
- `while`
- `for`
- blocks `{ ... }`
- `return`
- `break`
- `continue`
- Stack support shared across evaluations through stack connections.
- useful for `GuiderMath` state between steps.
- Comments:
- line comments `# ...`
- block comments `/* ... */`
- When a tensor is expected, lists can often be used instead and will be promoted automatically.
### Control Flow Statements
- **If/Else**: `if (condition) statement [else statement]`
- **While Loops**: `while (condition) statement`
- **Blocks**: `{ statement1; statement2; ... }`
- New variables defined in blocks are isolated and don't leak to outer scope
- Modifications to existing variables persist to outer scope
- **Return Statements**: `return [expression];`
- Early return from functions or top-level expressions
- **For Loops**: `for (variable in expression) statement`
- Iterates over list elements
- Iterates over tensor elements along dimension `0`
- Scalars are treated as one-item iterables
- **Break/Continue**: `break;`, `continue;`
- Control loop execution (works in `while` and `for` loops)
## Operators
- Math: `+`, `-`, `*`, `/`, `%`, `^`, `|x|` (abs / norm-style magnitude)
- Assignment: `=`, `+=`, `-=`, `*=`, `/=`, `%=`
- also works on indexed targets, for example `a[i] += 1;` or `a[i, j] = expression;`.
- Boolean: `<`, `<=`, `>`, `>=`, `==`, `!=`
(`false = 0.0`, `true = 1.0`)
- Ternary: `condition ? a : b`
- Constants: `pi`/`PI`, `e`/`E` (case-insensitive).
- `none` / `None` / `null` / `NULL`: empty/no-value literal.
- Bitwise shifts: `<<`, `>>`
- Indexing: `x[i]` or `x[i, j, ...]`
- works on tensors, nested lists, and strings,
- supports scalar, tensor, and list indices.
- Lists: `[v1, v2, ...]`
- vector math is supported,
- lists can broadcast across tensor operations in many cases,
- lists can be used for color triples, coordinates, kernel sizes, and permutations.
- **Length Mismatch Handling**: All math nodes (except Model, Clip, Vae which default to broadcast) include a `length_mismatch` option to handle inputs with different batch sizes, sample counts, or list lengths. The target length is determined by the **maximum length** among all provided inputs (`a`, `b`, `c`, `d`).
- `do nothing`: dones **no validation** on input
- `tile`: Repeats shorter inputs to match the maximum length.
- `error` (Default): Raises a `ValueError` if any input lengths differ.
- `pad`: Shorter inputs are padded with zeros to match the maximum length.
## Functions
### 1) Core Math
#### 1.1 Elementary & Numeric
- `abs(x)` or `|x|`: absolute value; tensor/list inputs are handled element-wise.
- `sqrt(x)`: square root.
- `ln(x)`: natural logarithm.
- `log(x)`: base-10 logarithm.
- `exp(x)`: exponential `e^x`.
- `pow(x, y)`: power `x^y`.
- `floor(x)`: round down.
- `ceil(x)`: round up.
- `round(x)`: round to nearest integer.
- `fract(x)`: fractional part (`x - floor(x)`).
- `sign(x)`: sign (`-1`, `0`, `1`).
- `gamma(x)`: gamma function.
- `clamp(x, min, max)`: clamp to interval.
- `step(x, edge)`: returns `1` when `x >= edge`, otherwise `0`.
- `dist(x1, y1, x2, y2)` / `distance`: Euclidean distance between 2D points.
#### 1.2 Trigonometric
- `sin(x)`: sine in radians.
- `cos(x)`: cosine in radians.
- `tan(x)`: tangent in radians.
- `asin(x)`: inverse sine.
- `acos(x)`: inverse cosine.
- `atan(x)`: inverse tangent.
- `atan2(y, x)`: quadrant-aware inverse tangent.
#### 1.3 Hyperbolic
- `sinh(x)`: hyperbolic sine.
- `cosh(x)`: hyperbolic cosine.
- `tanh(x)`: hyperbolic tangent.
- `asinh(x)`: inverse hyperbolic sine.
- `acosh(x)`: inverse hyperbolic cosine.
- `atanh(x)`: inverse hyperbolic tangent.
#### 1.4 Activation / ML
- `relu(x)`: `max(0, x)`.
- `gelu(x)`: Gaussian Error Linear Unit.
- `softplus(x)`: smooth ReLU-like function.
- `sigm(x)`: sigmoid.
- `softmax(x, [dim])`: softmax normalization.
- `softmin(x, [dim])`: softmin normalization.
- `erf(x)`: error function.
- `erfinv(x)`: inverse error function.
#### 1.5 Interpolation & Remap
- `lerp(a, b, t)`: linear interpolation.
- `smoothstep(x, e0, e1)`: 3rd-order smooth transition.
- `smootherstep(x, e0, e1)`: 5th-order smoother transition.
- `cubic_ease(a, b, t)` / `cubic`: cubic easing interpolation.
- `sine_ease(a, b, t)` / `sine`: sine easing interpolation.
- `elastic_ease(a, b, t)` / `elastic`: elastic easing interpolation.
- `remap(v, i_min, i_max, o_min, o_max)`: map value from one interval to another.
---
### 2) Tensor, Stats & Data Ops
#### 2.1 Element-wise / Tensor Math
- `tmin(x, y)`: element-wise minimum.
- `tmax(x, y)`: element-wise maximum.
- `tnorm(x,[dim])`: L2 normalization along the last dimension. If dimension is provided, it uses this dimension. Dimension list can be provided.
- `snorm(x, [dim])`: scalar/tensor norm magnitude. Frobenius norm along `dim` if specified (`dim` can be a single dimension or a list of dimensions); otherwise over the whole tensor.
- `cossim(a, b)` / `cosine_similarity`: cosine similarity.
- `cov(x, y)`: covariance.
- `corr(x, y)` / `correlation`: Pearson correlation.
- `entropy(x)`: Shannon entropy.
- `flip(x, dims)`: flip selected dimensions; `dims` can be a single dimension, list, or tensor of dimensions, so multiple axes can be flipped at once.
- `swap(tensor, dim, i1, i2)`: swap two indices along dimension `dim`.
#### 2.2 Reductions & Statistical Aggregates
- `sum(x, [dims])`: sum; optional `dims` can be a single dimension or a list of dimensions.
- `mean(x)`: mean.
- `std(x)`: standard deviation.
- `var(x)`: variance.
- `median(x)`: median.
- `mode(x)`: mode.
- `quartile(x, k)` / `quartil`: quartile (`k` in `0..4`).
- `percentile(x, p)` / `prcnt`: percentile (`p` in `0..100`).
- `quantile(x, q)`: quantile (`q` in `0..1`).
- `moment(x, a, k)`: k-th moment around center `a`.
- `any(x)`: `1.0` if any element is non-zero.
- `all(x)`: `1.0` if all elements are non-zero.
- `count(x)` / `length(x)` / `cnt(x)`: number of elements or length.
- `cumsum(x, [dim])`: cumulative sum; `dim` defaults to `0`.
- `cumprod(x)`: cumulative product along dimension `0`.
- `smin(x, ...[, dims])`: scalar minimum across inputs; optionally reduce the first tensor/list input across `dims`.
- `smax(x, ...[, dims])`: scalar maximum across inputs; optionally reduce the first tensor/list input across `dims`.
#### 2.3 Sorting / Selection / Indices
- `sort(x, [descending], [dim])`: tensors are sorted along the specified dimension (`dim`); if `dim` is not specified, it defaults to the last dimension (`dim=-1`); lists use Python sorting.
- `argsort(x, [descending], [dim])`: returns indices along `dim` (defaults to last dimension); `descending` defaults to `false`.
- `argmin(x, [as_position])`: global minimum index for tensors; when `as_position` is truthy, returns coordinates as a list.
- `argmax(x, [as_position])`: global maximum index for tensors; when `as_position` is truthy, returns coordinates as a list.
- `topk(x, k)`: tensors keep the same shape and all non-top-k values are zeroed; lists return the sorted top-k slice.
- `botk(x, k)`: same as `topk`, but for smallest values.
- `topk_ind(x, k)` / `topk_indices`: global top-k indices from `flatten()`.
- `botk_ind(x, k)` / `botk_indices`: global bottom-k indices from `flatten()`.
- `unique(x)`: sorted unique values for tensors and lists.
- `where(cond, a, b)`: tensor broadcasting for tensor inputs; recursive truthy selection for lists/scalars.
- `histogram(x, bins, min, max)` / `hist`: histogram counts from `flatten()` in the given interval.
#### 2.4 Shape / Structure / Layout
- `shape(x)`: returns shape as a Python list.
- `flatten(x)`: flattens tensor or nested list.
- `reshape(tensor, shape)` / `rshp`: reshape with element-count validation.
- `squeeze(x, [dim])`: removes singleton dimensions; optional `dim` follows PyTorch semantics.
- `unsqueeze(x, dim)`: inserts a singleton dimension at `dim`.
- `permute(tensor, dims)` / `perm`: reorder dimensions.
- `crop(tensor, position, size)`: extract a region from tensor or string.
- `pad(tensor, padding)`: pad tensor.
- `overlay(base, overlay, offset, [opacity])`: overlay one value/tensor onto another.
- `append(a, b)`: append or concatenate items, lists, and tensors.
- `repeat(x, count, [dims])`: repeat values; tensor `count` can be a scalar or a per-dimension list when `dims` is provided.
- `batch_shuffle(tensor, indices)` / `shuffle` / `select`: reorder along batch dimension.
- `concatenate(..., dim)` / `concat` / `cat`: concatenate tensors or lists.
- `roll(tensor, shifts, [dim])`: circular shift.
- `tensor(shape, [value, [type]])`: create a filled tensor; `type` a tensor to copy its dtype to self when being created.
- `interpolate_linear(tensor, scale)`: linear interpolation-based resizing. Scale can be a single float or a list. When it is a list, it is interpreted as the target output size.
- `interpolate_area(tensor, scale)`: area interpolation-based resizing.
- `interpolate_nearest(tensor, scale)` / `interpolate_nearest_exact`: nearest neighbor interpolation-based resizing.
#### 2.5 Linear Algebra
- `dot(a, b)`: dot product after flattening.
- `matmul(a, b)`: matrix multiplication.
- `cross(a, b)`: cross product; last dimension must be `3`.
- `pinv(x)`: permutation inverse for permutation-like values.
- `singular_value_decomposition(x,[full_matrices])`/`svd`: singular value decomposition.
- `diagonal_matrix(x,shape,[offset],[dim1],[dim2])` / `diag`: create a diagonal matrix from a vector. The diagonal can be offset from the main diagonal using the `offset` parameter. The `dim1` and `dim2` parameters specify which dimensions to use for the diagonal.
#### 2.6 Mapping / Sampling
- `map(tensor, c1, ...)`: coordinate remapping via `grid_sample`.
- supports up to `3` coordinate inputs,
- uses sampling coordinates derived from the provided coordinate tensors or functions,
- intended for spatial remapping / resampling.
- `get_value(tensor, position)`: read value at an N-D position using flat offset math. Superseeded by tensor[position] indexing.
---
### 3) Imaging & Spatial Processing
#### 3.1 Filters & Convolution
- `blur(x, sigma, [reshape])` / `gaussian`: Gaussian blur using separable convolution. If `reshape` is truthy, tries orienting the input such that channel is in the direction of the filter; otherwise it expects the color dimension to be last.
- `edge(x, [kernel_size])`: Sobel-style edge detection.
- `ezconvolution(tensor, ...)` / `ezconv`: convolution with auto layout handling.
- `convolution(tensor, ...)` / `conv`: direct convolution. Expects [batch, channel, spatial...] layout.
#### 3.2 Mask Morphology
- `dilate(x, [kernel_size])`: dilation.
- `erode(x, [kernel_size])`: erosion.
- `morph_open(x, [kernel_size])`: opening = erosion followed by dilation.
- `morph_close(x, [kernel_size])`: closing = dilation followed by erosion.
---
### 4) Optical Flow
- `rife(img1, img2, [tiling_size, iterations, multi_scale])`: compute optical flow using a RAFT-based model.
- `tiling_size` chunks the image into overlapping `tiling_size`x`tiling_size` tiles to conserve memory; defaults to `0` (auto-enables `1024`x`1024` tiling above 2MPx); a value between `0` and `1` is taken as a fraction of the resolution instead.
- `iterations` defaults to `12`.
- `multi_scale` (default `false`) also runs a low-resolution pass of the base image to help catch large movements.
- `motion_mask(flow)`: motion/occlusion mask from flow.
- `flow_to_image(flow)`: visualize flow as RGB.
- `flow_apply(image, flow)`: warp image by flow.
- `flow_mag(flow)` / `flow_magnitude`: flow vector magnitude (or anything else, uses first 2 positions of last dimension).
- `flow_ang(flow)` / `flow_angle`: flow vector angle in radians (`atan2(dy, dx)`). (or anything else, uses first 2 positions of last dimension)
---
### 5) Frequency Domain (FFT)
- `fft(x)`: fast Fourier transform across all dimensions.
- `ifft(x, [shape])`: inverse FFT.
- `angle(x)`: phase angle of complex values.
---
### 6) Random & Noise
#### 6.1 Random Distributions
- All random generators are seeded and deterministic for a given seed.
- All use the current node shape by default (based on shape of input to the node), but an optional trailing `shape` argument can be provided to specify a different output shape.
- All use a `rand<dist>(...)` / `random_<distribution>(...)` naming convention, but the argument list before `[shape]` differs per distribution - see each entry below.
- `noise(seed, [shape])` / `randn` / `random_normal`: normal distribution. Same generator as in `Random Noise` node.
- `rand(seed, [shape])` / `randu` / `random_uniform`: uniform distribution.
- `rande(seed, lambda, [shape])` / `random_exponential`: exponential distribution.
- `randc(seed, median, sigma, [shape])` / `random_cauchy`: Cauchy distribution.
- `randln(seed, mean, std, [shape])` / `random_log_normal`: log-normal distribution.
- `randb(seed, probability, [shape])` / `random_bernoulli`: Bernoulli distribution.
- `randp(seed, lambda, [shape])` / `random_poisson`: Poisson distribution.
- `randg(seed, shape_param, scale, [shape])` / `random_gamma`: gamma distribution.
- `randbeta(seed, alpha, beta, [shape])` / `random_beta`: beta distribution.
- `randl(seed, loc, scale, [shape])` / `random_laplace`: Laplace distribution.
- `randgumbel(seed, loc, scale, [shape])` / `random_gumbel`: Gumbel distribution.
- `randw(seed, scale, concentration, [shape])` / `random_weibull`: Weibull distribution.
- `randchi2(seed, df, [shape])` / `random_chi2`: chi-squared distribution.
- `randt(seed, df, [shape])` / `random_studentt`: Student’s t distribution.
#### 6.2 Procedural Noise
- `perlin(seed, scale, [octaves, [offset, [shape]]])` / `perlin_noise`:
- smooth gradient noise,
- supports arbitrary dimensional grids.
- `voronoi(seed, scale, [jitter], [offset], [shape])` / `cellular` / `worley` / `voronoi_noise` / `cellular_noise`:
- cellular / Voronoi noise,
- `jitter` is clamped to `0..1`.
- `plasma(seed, scale, [octaves, [offset, [shape]]])` / `turbulence` / `plasma_noise`:
- high-frequency turbulence-style noise.
- `ridged(seed, scale, [octaves, [offset, [shape]]])` / `ridged_noise`:
- ridged multifractal noise with sharp peaks and mountain-like structures.
- `domain_warp(seed, scale, warp_scale, warp_strength, [octaves, [warp_octaves, [offset, [shape]]]])` / `domain_warp_noise`:
- domain-warped noise for breaking up regular patterns and adding complex organic detail.
---
### 7) Bitwise
#### 7.1 Shift Operators
- `a << b`: Bitwise left shift.
- `a >> b`: Bitwise right shift.
#### 7.2 Bitwise Functions
- `band(a, b)` / `bitwise_and`: bitwise AND.
- `bor(a, b)` / `bitwise_or`: bitwise OR.
- `bxor(a, b)` / `bitwise_xor`: bitwise XOR.
- `bnot(a)` / `bitwise_not`: bitwise NOT.
- `bitcount(a)` / `popcount` / `popcnt`: number of set bits.
---
### 8) Strings
- `upper(str)`: convert to uppercase.
- `lower(str)`: convert to lowercase.
- `trim(str)`: trim surrounding whitespace.
- `split(str, [delimiter])`: split string.
- `join(list, [separator])`: join list into string.
- `substring(str, start, [length])` / `substr`: substring extraction.
- `find(str, search)`: first match position.
- `replace(str, search, replacement)`: replace occurrences.
- `startswith(str, prefix)`: returns `1.0` if the string starts with `prefix`, otherwise `0.0`.
- `endswith(str, suffix)`: returns `1.0` if the string ends with `suffix`, otherwise `0.0`.
---
### 9) Color
- `rgb_to_hsv(...)`: convert RGB to HSV.
- accepts packed tensor/list input or separate `r, g, b`,
- optional trailing boolean argument enables hue in degrees - the 2nd argument in packed mode (`rgb, [degrees]`), the 4th in separate mode (`r, g, b, [degrees]`).
- `hsv_to_rgb(...)`: convert HSV to RGB.
- accepts packed tensor/list input or separate `h, s, v`,
- optional trailing boolean argument treats hue as degrees - the 2nd argument in packed mode (`hsv, [degrees]`), the 4th in separate mode (`h, s, v, [degrees]`).
- `rgb_to_oklab(...)`: convert RGB to OKLab.
- accepts packed tensor/list input or separate `r, g, b`.
- `oklab_to_rgb(...)`: convert OKLab to RGB.
- accepts packed tensor/list input or separate `L, a, b`.
- `rgb_to_cielab(...)`: convert RGB to CIELAB.
- accepts packed tensor/list input or separate `r, g, b`.
- `cielab_to_rgb(...)`: convert CIELAB to RGB.
- accepts packed tensor/list input or separate `L, a, b`.
- `int_to_rgb(value)`: convert packed integer color to RGB triplet.
- `rgb_to_int(...)`: convert RGB triplet to packed integer color.
---
### 10) Utility & Conversion
- `print(x)`: print value and return it.
- `print_shape(x)` / `pshp`: print shape and return value.
- `range(start, end, step)`: numeric sequence as a list.
- `linspace(start, end, count)`: evenly spaced sequence.
- `logspace(start, end, count, base)`: logarithmically spaced sequence.
- `nan_to_num(x, nan, posinf, neginf)` / `nvl`: replace `NaN` and infinities.
- `timestamp()` / `now`: current Unix timestamp.
- `int(x)`: convert to int32 or nested int values.
- `float(x)`: convert to float or nested float values.
- `as_nested(x)` / `as_nested_tensor`: convert list to a nested tensor. Usefull for video+audio models. Values do not need to be in the same or compatible shape.
___
### Generators
- `text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])` - renders text to an 2D tensor
- `coordinates(shape, dim, [dtype])` / `coords` - generates a tensor whose values are the coordinates of each element along the specified dimension. dtype is copied from tensor at that position. Default is float32.
---
### 11) State / Stack
- `stack_push(id, value)`: push into stack slot.
- `stack_pop(id)`: pop from stack slot.
- `stack_get(id)`: read top value without removing it.
- `stack_clear(id)`: clear stack slot.
- `stack_has(id)`: check whether slot exists and is non-empty.
### 12) Dictionary
- `add_key(dict, key, value)` - add a key-value pair to a dictionary.
- `remove_key(dict, key)` / `rm_key` - remove a key-value pair from a dictionary.
- `keys(dict)` - returns a list of dictionary keys in insertion order.
## Variables
- **Common variables (except FLOAT)**:
- - `D{N}` - position in n-th dimension of tensor, for example `D0`, `D1`, `D2`
- - `S{N}` - size of n-th dimension of tensor, for example `S0`, `S1`, `S2`
- - `V{N}` - value input, for example `V0`, `V1`, `V2`
- - `V` - list of value inputs
- - `F{N}` - float input, for example `F0`, `F1`, `F2`
- - `F` - list of float inputs
- - `Fcnt` or `F_count`: number of float inputs
- - `Vcnt` or `V_count`: number of value inputs
- `depth`: Current recursion depth (0 at top level)
- **common inputs** (legacy):
- `a`, `b`, `c`, `d`
- **Extra floats** (legacy):
- `w`, `x`, `y`, `z`
- **INSIDE IFFT**
- `F` or `frequency_count` – frequency count (freq domain, iFFT only)
- `K` or `frequency` - isotropic frequency (Euclidean norm of indices, iFFT only)
- `Kx`, `Ky`, `K_dimN` - frequency index for specific dimension
- `Fx`, `Fy`, `F_dimN` - frequency count for specific dimension
- **IMAGE and LATENT**:
- `C` or `channel` - channel of image
- `X` - X position in image, origin in top-left
- `Y` - Y position in image, origin in top-left
- `W` or `width` - image width
- `H` or `height` - image height
- `B` or `batch` - batch index
- `T` or `batch_count` - number of batches
- `N` or `channel_count` - channel count
- **LATENT**
- `V{N}_{M}` - In case of nested tensors, takes the M-th tensor from N-th input. For example, `V0_1` is the second tensor from the first input.
- **MODEL, VAE, CLIP**:
- `V{N}_d` - difference against the original input (for example applied lora)
- `K` or `key` - name of current layer. For example, `proj_out.bias`, `diffusion_model.blocks.5.attn.out_proj.weight` or `txtfusion.layerwise_blocks.0.prenorm.scale`.
- `L` or `layer` - index of current layer
- `LC` or `layer_count` - number of layers
- **IMAGE KERNEL**: in `conv`/`convolution` or `ezconv`/`ezconvolution` function kernel input
- `kX`, `kY`, `kZ` (also lowercase `kx`, `ky`, `kz`) - position in kernel, centered at `0.0`; `kZ`/`kz` is only set for 3D kernels
- `kW`, `kernel_width` - kernel width
- `kH`, `kernel_height` - kernel height
- `kD`, `kernel_depth` - kernel depth
- **AUDIO**:
- `B` or `batch` - batch index
- `N` or `channel_count` - channel count
- `C` or `channel` - audio channel
- `S` or `sample` - current sample
- `T` or `sample_count` - audio length in samples
- `R` or `sample_rate` - sample rate
- **VIDEO**
- refer to `IMAGE and LATENT` for the visual part
- `batch` means `frame`
- `batch_count` means `frame_count`
- refer to `AUDIO` for sound part
- **NOISE**
- refer to `IMAGE and LATENT` for most variables
- `I` or `input_latent` - latent used as input to generate noise
- **GUIDER**
- refer to `IMAGE and LATENT`
- `sigma` - current sigma value
- `seed` - seed used for noise generation
- `steps` - total number of sampling steps
- `current_step` - current step index, `0..steps`
- `sample` - tensor input to guider or output from sampling
## SelectiveGuiderMathNode (hooking)
Adds support for hooking into specific layers/blocks of the model during guided diffusion.
Current implementation details:
- `hook_target` supports runtime filtering in node UI.
- `layer_x` is used as direct index match (`idx == layer_x`) for current hook context.
- For guiders with `original_conds`, base conditions are restored before reattaching hooks to avoid hook accumulation across reruns/interrupted runs.
- Active hook paths currently include:
- Attention override (`attn1` / `attn2` / `double_block_attn` / `single_block_attn` / `attn_unknown`)
- DiT block replace (`dit.double_block`, `dit.single_block`)
- UNet block patches (`input_block_patch`, `middle_patch`, `output_block_patch`)
- Timestep embedding start via `emb_patch` (`block_name="time_emb"`, `layer_x=0`)
- Whole-model edges via diffusion-model wrapper (`model_begin`, `model_end`)
### Side behavior (positive / negative)
When guider has `original_conds`, hooks are attached separately for:
- `positive`
- `negative`
Runtime variables expose side information:
- `cond_side`: `"positive" | "negative" | "mixed" | "unknown"`
- `cond_index`: `0 | 1 | -1`
- `is_positive`: `1.0` or `0.0`
- `is_negative`: `1.0` or `0.0`
### Variable reference (`SelectiveGuiderMathNode`)
The following variables are available in `Expression`.
| Variable | Type | Description |
|---|---|---|
| `inp` | tensor | Current tensor received by the active hook. |
| `sample` | tensor | Alias of `inp`. |
| `F0..Fn` | float/tensor | Individual float inputs from the node. |
| `F` | list/tensor | Collection of all float inputs. |
| `D0..Dn` | tensor | Per-dimension index tensors from `generate_dim_variables`. |
| `S0..Sn` | float | Per-dimension sizes from `generate_dim_variables`. |
| `hook_kind` | string | Active hook identifier: `attn1`, `attn2`, `double_block_attn`, `single_block_attn`, `attn_unknown`, `dit_block`, `unet_block`, `model_begin`, `model_end`, or `unknown`. |
| `hook_domain` | string | High-level domain: `attention`, `diffusion`, or `unknown`. |
| `attn_kind` | string | Attention kind: `attn1`, `attn2`, `double_block_attn`, `single_block_attn`, `attn_unknown`, or `none` outside attention hooks. |
| `transformer_index` | float | Attention sub-block index inside a UNet block (`-1` if unavailable). |
| `is_attn1` | float (0/1) | `1` when current hook is `attn1`, else `0`. |
| `is_attn2` | float (0/1) | `1` when current hook is `attn2`, else `0`. |
| `is_attn1_hook` | float (0/1) | `1` when `attn_kind=="attn1"`, else `0`. |
| `is_attn2_hook` | float (0/1) | `1` when `attn_kind=="attn2"`, else `0`. |
| `is_dit` | float (0/1) | `1` when current hook is DiT block hook, else `0`. |
| `is_unet_block` | float (0/1) | `1` when current hook is UNet block hook, else `0`. |
| `is_time_emb` | float (0/1) | `1` when current hook is timestep embedding entry (`block_name=="time_emb"`), else `0`. |
| `block_name` | string | Block/stage name (`input`, `middle`, `output`, `time_emb`, `model`, DiT block type, etc.). |
| `layer_id` | float | Numeric block/layer id used by current hook context. |
| `layer` | float | Alias of `layer_id`. |
| `i` | float | Alias of `layer_id`. |
| `layer_key` | string | Composite identifier for debug/filtering (for example `output.6.attn2.0`, `unet.time_emb.0`, `model.begin`). |
| `total_blocks` | float | Total blocks in stream if available, otherwise `-1`. |
| `has_qkv` | float (0/1) | `1` in attention hooks where `q/k/v` are valid; `0` in diffusion/block hooks. |
| `q` | tensor | Query tensor in attention hooks; fallback placeholder otherwise. |
| `k` | tensor | Key tensor in attention hooks; fallback placeholder otherwise. |
| `v` | tensor | Value tensor in attention hooks; fallback placeholder otherwise. |
| `heads` | float | Number of attention heads (attention hooks only, else `0`). |
| `dim_head` | float | Per-head channel size (`q.shape[-1] / heads`) when available. |
| `activations_shape` | list | Raw shape from transformer context. Empty list if unavailable. |
| `activation_b` | float | Batch dimension from `activations_shape[0]` (or `-1`). |
| `activation_c` | float | Channel dimension from `activations_shape[1]` (or `-1`). |
| `activation_h` | float | Height dimension from `activations_shape[2]` (or `-1`). |
| `activation_w` | float | Width dimension from `activations_shape[3]` (or `-1`). |
| `attn_mode` | string | Legacy compatibility field (default `unknown`). |
| `attention_relation` | string | Inferred semantic relation: `self`, `cross`, or `unknown`. |
| `is_self_attention` | float (0/1) | `1` when the active attention is self-attention. |
| `is_cross_attention` | float (0/1) | `1` when the active attention is cross-attention. |
| `has_context` | float (0/1) | `1` when attention context appears to be present. |
| `query_tokens` | float | Query sequence length. |
| `context_tokens` | float | Context sequence length. |
| `value_tokens` | float | Value sequence length. |
| `activation_rank` | float | Rank of `activations_shape`. |
| `activation_t` | float | Temporal dimension for video-like activations (`-1` if unavailable). |
#### Practical notes
- On SD1.x, repeated hits on the same `layer_id` are normal in attention because one UNet block can contain multiple transformer sub-blocks.
- Use `transformer_index` to target exactly one sub-block.
- For timestep-begin hooking use `layer_x=0` and filter by `block_name=="time_emb"` (or `layer_key=="unet.time_emb.0"`).
- For model-edge hooks filter by `hook_kind=="model_begin"` or `hook_kind=="model_end"`.
- For model-agnostic expressions, prefer guard variables: `has_qkv`, `is_dit`, `is_unet_block`, `is_attn1`, `is_attn2`.
- `attn2` is only a hook label, not guaranteed to mean real cross-attention.
- Use `is_cross_attention` only as an inferred relation from runtime metadata.
- Use `attention_relation` for semantic relation (`self`/`cross`) and `attn_kind` for hook-path classification.
- Selective guider math:
- `hook_target`: `all`, `dit_block`, `unet_block`, `attn1`, `attn2`,
`double_block_attn`, `single_block_attn`, `model_begin`, `model_end`
+8 -14
View File
@@ -1,16 +1,10 @@
"""Top-level package for more_math."""
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
]
__author__ = """Daniel Martinek"""
__email__ = "danda.martinek@gmail.com"
__version__ = "0.0.1"
from .src.more_math.nodes import NODE_CLASS_MAPPINGS
from .src.more_math.nodes import NODE_DISPLAY_NAME_MAPPINGS
import traceback
try:
from .more_math.nodes import comfy_entrypoint as comfy_entrypoint
except ImportError:
print("Something is seriously wrong")
traceback.print_exc()
WEB_DIRECTORY = "./web"
__all__ = ["comfy_entrypoint", "WEB_DIRECTORY"]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+172
View File
@@ -0,0 +1,172 @@
from .helper_functions import (
generate_dim_variables,
parse_expr,
getIndexTensorAlongDim,
as_tensor,
normalize_to_common_shape,
make_zero_like,
get_v_variable,
get_f_variable,
checkLazyNew,
get_tensor_device,
move_to_device,
)
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
import comfy
import torch
from .Stack import MrmthStack
import copy
from .ParseTree import MrmthParseTree
class AudioMathNode(io.ComfyNode):
"""
Enables math expressions on Audio.
Inputs:
V: Autogrow audio inputs (V0, V1, ...)
F: Autogrow float inputs (F0, F1, ...)
Expression: Expression
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_AudioMathNode",
category="More math/math",
display_name="Audio math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Audio.Input("values", optional=True), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on weights",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched audio shapes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing samples as zero."
),
io.Int.Input(id="batching", default=0),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies audio tensors to the compute device for math and moves the result back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Audio.Output(is_output_list=True),
MrmthStack.Output(),
],
)
@classmethod
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile",batching=0, remember_stack=False,use_compute_device=True,stack={}):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, length_mismatch="tile",batching=0, remember_stack=False,use_compute_device=True, stack={}):
# Identify all present audio inputs and their keys
tensor_keys = [k for k, v in V.items() if v is not None and isinstance(v, dict) and "waveform" in v]
if not tensor_keys:
raise ValueError("At least one audio input is required.")
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
original_waveform = V[tensor_keys[0]]["waveform"]
original_device = get_tensor_device(original_waveform)
compute_device = comfy.model_management.get_torch_device() if use_compute_device else original_device
working_V = move_to_device(V, compute_device) if compute_device is not None and compute_device != original_device else V
waveforms = {k: working_V[k]["waveform"] for k in tensor_keys}
sample_rates = {k + "sr": working_V[k].get("sample_rate", 44100) for k in tensor_keys}
# Normalize all waveforms together
normalized_waveforms = normalize_to_common_shape(*waveforms.values(), mode=length_mismatch)
V_norm_waveforms = dict(zip(tensor_keys, normalized_waveforms))
ref_waveform = normalized_waveforms[0]
common_shape = ref_waveform.shape
sample_rate = V[tensor_keys[0]].get("sample_rate", 44100)
if(length_mismatch == "error"):
for name in tensor_keys:
if waveforms[name].shape != common_shape:
raise ValueError(f"Input '{name}' has shape ({waveforms[name].shape[0]}, {waveforms[name].shape[2]}), expected ({common_shape[0]}, {common_shape[2]}) to match input.")
# Setup legacy variables a, b, c, d
a_w = V_norm_waveforms.get("V0", make_zero_like(ref_waveform))
b_w = V_norm_waveforms.get("V1", make_zero_like(a_w))
c_w = V_norm_waveforms.get("V2", make_zero_like(a_w))
d_w = V_norm_waveforms.get("V3", make_zero_like(a_w))
# Ensure legacy are normalized
a_w, b_w, c_w, d_w = normalize_to_common_shape(a_w, b_w, c_w, d_w, mode=length_mismatch)
variables = {
"a": a_w, "b": b_w, "c": c_w, "d": d_w,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"B": getIndexTensorAlongDim(a_w, 0),
"batch": getIndexTensorAlongDim(a_w, 0),
"C": getIndexTensorAlongDim(a_w, 1),
"channel": getIndexTensorAlongDim(a_w, 1),
"N": float(a_w.shape[1]),
"channel_count": float(a_w.shape[1]),
"S": getIndexTensorAlongDim(a_w, 2),
"sample": getIndexTensorAlongDim(a_w, 2),
"T": float(a_w.shape[2]),
"sample_count": float(a_w.shape[2]),
"R": sample_rate,
"sample_rate": sample_rate,
"batch_count": float(a_w.shape[0]),
} | generate_dim_variables(a_w) | V_norm_waveforms | sample_rates
v_stacked, v_cnt = get_v_variable(V_norm_waveforms, length_mismatch=length_mismatch)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
for k, val in F.items():
variables[k] = val if val is not None else 0.0
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, a_w.shape,a_w.device,state_storage=stack)
result = visitor.visit(tree)
result = as_tensor(result, a_w.shape)
result = move_to_device(result, original_device)
if batching and batching > 0:
res = torch.split(result, batching, dim=0)
res_list = []
for result_chunk in res:
res_list.append({"waveform": result_chunk, "sample_rate": sample_rate})
return (res_list, stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([{"waveform": result, "sample_rate": sample_rate}], stack)
+62
View File
@@ -0,0 +1,62 @@
from comfy_api.latest import io
import torch
windows = {"bartlett": torch.bartlett_window, "blackman": torch.blackman_window, "hamming": torch.hamming_window, "hann": torch.hann_window, "kaiser": torch.kaiser_window}
class AudioToSpectrogram(io.ComfyNode):
"""
Converts Audio to an Image spectrogram.
Red = Real, Green = logarithm of value (just so it looks good), Blue = Imaginary.
Each audio channel is stacked vertically.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_AudioToImageSpectrogram",
category="More math/converters",
display_name="Audio -> Spectrogram",
inputs=[
io.Audio.Input(id="audio", tooltip="Input audio"),
io.Int.Input(id="window_length", default=1024, min=16, max=4096, tooltip="Window length in samples"),
io.Int.Input(id="hop_length", default=256, min=1, max=4096, tooltip="Stride of the window (hop length) in samples"),
io.Int.Input(
id="bucket_count", default=513, min=2, max=4096, tooltip="Number of frequency buckets (determines resolution)"
),
io.Combo.Input(id="window_type", default="hann", options=list(windows.keys()), tooltip="Type of window function to apply"),
],
outputs=[
io.Image.Output(id="image"),
io.Int.Output(id="channel_count", display_name="Channel count", tooltip="Number of channels in the output image"),
io.Int.Output(id="sample_rate", display_name="Sample rate", tooltip="Sample rate of the input audio"),
],
)
@classmethod
def execute(cls, audio, window_length, hop_length, bucket_count, window_type):
waveform = audio["waveform"]
sample_rate = audio["sample_rate"]
B, C, S = waveform.shape
n_fft = (bucket_count - 1) * 2
flat_waveform = waveform.reshape(B * C, S)
window = windows[window_type](window_length, device=waveform.device)
stft_out = torch.stft(
flat_waveform,
n_fft=n_fft,
hop_length=hop_length,
win_length=window_length,
window=window,
center=True,
pad_mode="reflect",
normalized=False,
onesided=True,
return_complex=True,
)
F, T = stft_out.shape[1], stft_out.shape[2]
stft_out = stft_out.reshape(B, C, F, T).reshape(B, C * F, T)
image = torch.stack([stft_out.real, torch.log1p(torch.abs(stft_out)), stft_out.imag], dim=-1)
print(image.type())
return (image, C, sample_rate)
+133
View File
@@ -0,0 +1,133 @@
from __future__ import annotations
import copy
import torch
from comfy.patcher_extension import PatcherInjection
from comfy.weight_adapter.bypass import BypassForwardHook
from comfy_api.latest import io
from .helper_functions import parse_expr
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
@staticmethod
def _parse_strengths(Expression, stack):
tree = None
variables = {}
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, [1],torch.device("cpu"),state_storage=stack)
result = visitor.visit(tree)
return (list(result), stack)
class BatchLoraHooksNode(io.ComfyNode):
"""Scale existing bypass LoRA injections on a model/clip pair. Works with models/loaders/Load lora (Bypass) (For Debugging) or more_math batch lora node."""
@staticmethod
def _get_bypass_hooks(injections):
if injections is None:
return []
hooks = []
for injection in injections:
closure = getattr(getattr(injection, "inject", None), "__closure__", None)
if not closure:
continue
for cell in closure:
cell_value = cell.cell_contents
if hasattr(cell_value, "hooks") and hasattr(cell_value, "create_injections"):
manager_hooks = getattr(cell_value, "hooks", None)
if manager_hooks:
hooks.extend(manager_hooks)
break
return hooks
@staticmethod
def _build_bypass_injection(hooks, scale: float):
scaled_hooks = []
for hook in hooks:
adapter = copy.deepcopy(hook.adapter)
scaled_hooks.append(BypassForwardHook(hook.module, adapter, multiplier=hook.multiplier * scale))
def inject_all(model_patcher):
for hook in scaled_hooks:
hook.inject()
def eject_all(model_patcher):
for hook in scaled_hooks:
hook.eject()
return [PatcherInjection(inject=inject_all, eject=eject_all)]
@classmethod
def _scale_bypass_injections(cls, patcher, scale: float):
if patcher is None:
return
injections = getattr(patcher, "get_injections", lambda _key: None)("bypass_lora")
if injections is None:
return
hooks = cls._get_bypass_hooks(injections)
if not hooks:
return
was_injected = getattr(patcher, "is_injected", False)
if was_injected:
patcher.eject_model()
patcher.set_injections("bypass_lora", cls._build_bypass_injection(hooks, scale))
if was_injected:
patcher.inject_model()
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_LoraBatchRescale",
category="More math",
display_name="Scale LoRA (Batch)",
inputs=[
io.Model.Input(id="model", tooltip="The model with bypass LoRA injections"),
io.Clip.Input(id="clip", tooltip="The clip with bypass LoRA injections"),
io.MultiType.Input(
io.String.Input("model_strength", default="[1.0]", multiline=False),
types=[io.String, MrmthParseTree],
tooltip="List of model-side LoRA strengths",
), io.MultiType.Input(
io.String.Input("clip_strength", default="[1.0]", multiline=False),
types=[io.String, MrmthParseTree],
tooltip="List of clip-side LoRA strengths",
),
MrmthStack.Input(id="stack", optional=True)
],
outputs=[
io.Model.Output(id="model", tooltip="The model with scaled bypass LoRA injections", is_output_list=True),
io.Clip.Output(id="clip", tooltip="The clip with scaled bypass LoRA injections", is_output_list=True),
MrmthStack.Output(id="stack", tooltip="The stack"),
],
)
@classmethod
def execute(cls, model, clip, model_strength, clip_strength, stack=None):
stack = copy.deepcopy(stack) if stack is not None else {}
weights0, stack = _parse_strengths(model_strength, {})
weights1, stack = _parse_strengths(clip_strength, {})
scaled_models = []
scaled_clips = []
if len(weights0) != len(weights1):
raise ValueError("model_strength and clip_strength must have the same number of elements. Model: {}, Clip: {}".format(len(weights0), len(weights1)))
for w0,w1 in zip(weights0, weights1):
scaled_model = model.clone()
scaled_clip = clip.clone()
cls._scale_bypass_injections(scaled_model, w0)
clip_patcher = getattr(scaled_clip, "patcher", None)
cls._scale_bypass_injections(clip_patcher, w1)
scaled_models.append(scaled_model)
scaled_clips.append(scaled_clip)
return (scaled_models, scaled_clips, stack)
+101
View File
@@ -0,0 +1,101 @@
from email.mime import base
from comfy_api.latest import io
import folder_paths
from .Stack import MrmthStack
import comfy
import os
from .helper_functions import parse_expr
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
import torch
import copy
from .ParseTree import MrmthParseTree
class BatchLoraNode(io.ComfyNode):
"""
A node that applies a batch of LoRA weights to a model.
"""
@staticmethod
def _parse_strengths(Expression, stack,lora_list,lora_count,base_model):
tree = None
variables = {"lora_names": lora_list, "lora_count": lora_count,"base_model": base_model}
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, [1],torch.device("cpu"),state_storage=stack)
print(stack)
result = visitor.visit(tree)
return (list(result), stack)
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_BatchLoraNode",
category="More math",
display_name="Batch LoRa apply",
inputs=[
io.Model.Input(id="model", tooltip="The model to apply the LoRA weights to"),
io.Clip.Input(id="clip", tooltip="The clip to apply the LoRA weights to"),
io.String.Input(id="folder", tooltip="The sub-folder containing the LoRA weights"),
io.Boolean.Input(id="add_no_lora", tooltip="Whether to add a model execution without a lora"),
io.MultiType.Input(
io.String.Input("model_strength", default="[1.0]", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="strength of lora applied to model",
), io.MultiType.Input(
io.String.Input("clip_strength", default="[1.0]", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="strength of lora applied to clip",
),
MrmthStack.Input(id="stack", tooltip="The stack", optional=True),
],
outputs=[
io.Model.Output(id="model", tooltip="The model with the LoRA weights applied", is_output_list=True),
io.Clip.Output(id="clip", tooltip="The clip with the LoRA weights applied", is_output_list=True),
MrmthStack.Output(id="stack", tooltip="The stack"),
],
)
@classmethod
def execute(cls, model, clip, folder, add_no_lora, model_strength, clip_strength, stack=None):
paths = folder_paths.get_folder_paths("loras")
print(paths)
lora_path = folder_paths.get_filename_list("loras")
stack = copy.deepcopy(stack) if stack is not None else {}
selected_folder = os.path.normpath(folder)
if selected_folder:
lora_path = [
lora
for lora in lora_path
if os.path.normpath(lora) == selected_folder
or os.path.normpath(lora).startswith(selected_folder + os.sep)
]
model_strengths, stack = cls._parse_strengths(model_strength, stack, lora_path, len(lora_path), add_no_lora)
clip_strengths, stack = cls._parse_strengths(clip_strength, stack, lora_path, len(lora_path), add_no_lora)
print(stack)
model_lora = [model] if add_no_lora else []
clip_lora = [clip] if add_no_lora else []
for index, lora in enumerate(lora_path):
model = model.clone()
clip = clip.clone()
full_lora_path = folder_paths.get_full_path("loras", lora)
if full_lora_path is None:
print(f"BatchLoraNode: Skipping missing LoRA: {lora}")
continue
model_strength_value = model_strengths[index] if index < len(model_strengths) else model_strengths[-1]
clip_strength_value = clip_strengths[index] if index < len(clip_strengths) else clip_strengths[-1]
loraw = comfy.utils.load_torch_file(full_lora_path, safe_load=True)
modellr, cliplr = comfy.sd.load_bypass_lora_for_models(
model,
clip,
loraw,
model_strength_value,
clip_strength_value,
)
model_lora.append(modellr)
clip_lora.append(cliplr)
return (model_lora, clip_lora, stack)
+98
View File
@@ -0,0 +1,98 @@
from comfy_api.latest import io
from inspect import cleandoc
from .helper_functions import checkLazyNew
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
import comfy
import comfy.utils
class CLIPMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on CLIP weights using Autogrow inputs.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_CLIPMathNode",
display_name="CLIP Math",
category="More math/math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Clip.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on weights",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched layer counts."
),
io.Boolean.Input(
id="dict_mode",
default=False,
display_name="Dictionary mode",
tooltip="Evaluates CLIP weights as dictionaries. Per-layer and dimension variables are unavailable.",
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies CLIP patch tensors to the compute device for math and moves the patches back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Clip.Output(),
MrmthStack.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile",use_compute_device=True,stack={},dict_mode=False):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, length_mismatch="tile",use_compute_device=True,stack={},dict_mode=False) -> io.NodeOutput:
# Determine reference CLIP
a = V.get("V0")
stack = copy.deepcopy(stack) if stack is not None else {}
if a is None:
for m in V.values():
if m is not None:
a = m
break
if a is None:
raise ValueError("At least one input CLIP is required.")
# Prepare CLIP patchers
patchers_V = {}
for k, v in V.items():
if v is not None:
patchers_V[k] = v.patcher
# Call autogrow patch calculation
from .modelLikeCommon import calculate_patches_autogrow, calculate_patches_dict_mode
ref_patcher = getattr(V.get("V0"), "patcher", None)
layer_count = ref_patcher.model.state_dict().__len__() if ref_patcher is not None and hasattr(ref_patcher, "model") and hasattr(ref_patcher.model, "state_dict") else 0
pbar = comfy.utils.ProgressBar(layer_count)
aliases = {"a": "V0", "b": "V1", "c": "V2", "d": "V3", "w": "F0", "x": "F1", "y": "F2", "z": "F3"}
calculate_patches = calculate_patches_dict_mode if dict_mode else calculate_patches_autogrow
patches = calculate_patches(Expression, V=patchers_V, F=F,pbar=pbar, mapping=aliases,stack=stack,use_compute_device=use_compute_device)
out_clip = a.clone()
if patches:
out_clip.add_patches(patches, 1.0, 1.0)
return (out_clip,stack)
+255
View File
@@ -0,0 +1,255 @@
import torch
from .helper_functions import checkLazyNew, generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, normalize_to_common_shape, make_zero_like, get_v_variable, get_f_variable, get_tensor_device, move_to_device
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
import comfy
import copy
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
class ConditioningMathNode(io.ComfyNode):
"""
Enables math expressions on Audio.
Inputs:
I: Autogrow image inputs (I0, I1, ...)
F: Autogrow float inputs (F0, F1, ...)
Image: Expression
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_ConditioningMathNode",
category="More math/math",
display_name="Conditioning math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Conditioning.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", display_name="Tensor expr.", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on tensor part of conditioning",
),
io.MultiType.Input(
io.String.Input("Expression_pi", display_name="pooled output expr.", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on pooled_input part of conditioning",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
),
io.Int.Input(id="batching", default=0),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies conditioning tensors to the compute device for math and moves the result back afterwards.",
),
MrmthStack.Input(id="stack",optional=True)
],
outputs=[
io.Conditioning.Output(is_output_list=True),
MrmthStack.Output()
],
)
@classmethod
def check_lazy_status(cls, Expression,Expression_pi, V, F, length_mismatch="tile", batching=0,remember_stack=False,use_compute_device=True, stack={}):
d = checkLazyNew(Expression,V,F)
b = checkLazyNew(Expression_pi,V,F)
return d|b
@classmethod
def execute(cls, V, F, Expression, Expression_pi, length_mismatch="tile", batching=0,remember_stack=False,use_compute_device=True, stack={}):
# Identify all present conditioning inputs
tensor_keys = [k for k, v in V.items() if v is not None and isinstance(v, list) and len(v) > 0]
if not tensor_keys:
raise ValueError("At least one input is required.")
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
needed_vars = checkLazyNew(Expression, V, F) | checkLazyNew(Expression_pi, V, F)
needed_tensor_keys = [k for k in tensor_keys if k in needed_vars]
if not needed_tensor_keys:
needed_tensor_keys = tensor_keys
# Extract tensors and pooled outputs
original_tensor = None
tensors = {}
pooled_outputs = {}
for key in needed_tensor_keys:
conditioning = V[key]
tensors[key] = conditioning[0][0]
if original_tensor is None:
original_tensor = conditioning[0][0]
# pooled_output is optional in the dict
pooled_outputs[key] = conditioning[0][1].get("pooled_output")
original_device = get_tensor_device(original_tensor)
compute_device = comfy.model_management.get_torch_device() if use_compute_device else original_device
working_V = move_to_device(V, compute_device) if compute_device is not None and compute_device != original_device else V
# Extract tensors and pooled outputs
tensors = {}
pooled_outputs = {}
for key in needed_tensor_keys:
conditioning = working_V[key]
tensors[key] = conditioning[0][0]
pooled_outputs[key] = conditioning[0][1].get("pooled_output")
# Normalize main tensors
norm_tensors_batch = normalize_to_common_shape(*tensors.values(), mode=length_mismatch)
V_norm_tensors = dict(zip(needed_tensor_keys, norm_tensors_batch))
ref_tensor = norm_tensors_batch[0]
# Normalize pooled outputs (if they exist)
valid_pooled_keys = [k for k, v in pooled_outputs.items() if v is not None]
if valid_pooled_keys:
norm_pooled_batch = normalize_to_common_shape(*[pooled_outputs[k] for k in valid_pooled_keys], mode=length_mismatch)
V_norm_pooled = dict(zip(valid_pooled_keys, norm_pooled_batch))
ref_pooled = norm_pooled_batch[0]
else:
V_norm_pooled = {}
ref_pooled = torch.tensor([])
# Setup legacy variables a, b, c, d (Main Tensor)
a = V_norm_tensors.get("V0", make_zero_like(ref_tensor))
b = V_norm_tensors.get("V1", make_zero_like(a))
c = V_norm_tensors.get("V2", make_zero_like(a))
d = V_norm_tensors.get("V3", make_zero_like(a))
a, b, c, d = normalize_to_common_shape(a, b, c, d, mode=length_mismatch)
# variables for Main Tensor (Expression)
variables = {
"a": a, "b": b, "c": c, "d": d,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"B": getIndexTensorAlongDim(a, 0),
"batch": getIndexTensorAlongDim(a, 0),
"T": a.shape[0],
"batch_count": a.shape[0],
} | generate_dim_variables(a) | V_norm_tensors
v_stacked, v_cnt = get_v_variable(V_norm_tensors, length_mismatch=length_mismatch)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
for k, val in F.items():
variables[k] = val if val is not None else 0.0
# Execute Expression (Main Tensor)
rtensor = None
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, a.shape,a.device, state_storage=stack)
rtensor = visitor.visit(tree)
rtensor = as_tensor(rtensor, a.shape)
# variables for Pooled Output (Expression_pi)
a_p = V_norm_pooled.get("V0", make_zero_like(ref_pooled))
b_p = V_norm_pooled.get("V1", make_zero_like(a_p))
c_p = V_norm_pooled.get("V2", make_zero_like(a_p))
d_p = V_norm_pooled.get("V3", make_zero_like(a_p))
a_p, b_p, c_p, d_p = normalize_to_common_shape(a_p, b_p, c_p, d_p, mode=length_mismatch)
variables_pi = {
"a": a_p, "b": b_p, "c": c_p, "d": d_p,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"B": getIndexTensorAlongDim(a_p, 0) if a_p.numel() > 0 else torch.tensor([]),
"batch": getIndexTensorAlongDim(a_p, 0) if a_p.numel() > 0 else torch.tensor([]),
"T": a_p.shape[0] if a_p.numel() > 0 else 0,
"batch_count": a_p.shape[0] if a_p.numel() > 0 else 0,
} | generate_dim_variables(a_p) | V_norm_pooled
v_stacked, v_cnt = get_v_variable(V_norm_pooled, length_mismatch=length_mismatch)
if v_stacked is not None:
variables_pi["V"] = v_stacked
variables_pi["Vcnt"] = float(v_cnt)
variables_pi["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables_pi["F"] = f_stacked
variables_pi["Fcnt"] = float(f_cnt)
variables_pi["F_count"] = float(f_cnt)
for k, val in F.items():
variables_pi[k] = val if val is not None else 0.0
# Execute Expression_pi
rpooled = None
if valid_pooled_keys:
tree_pi = None
if isinstance(Expression_pi,str):
tree_pi = parse_expr(Expression_pi)
else:
tree_pi = Expression_pi
visitor_pi = UnifiedMathVisitor(variables_pi, a_p.shape,a_p.device, state_storage=stack)
rpooled_raw = visitor_pi.visit(tree_pi)
rpooled = as_tensor(rpooled_raw, a_p.shape)
if rtensor is None:
rtensor = torch.zeros([1])
if rpooled is None:
rpooled = torch.zeros([1])
# batching = size of each chunk -> use torch.split(tensor, batching, dim=0)
if batching and batching > 0:
rt_chunks = torch.split(rtensor, batching, dim=0)
rp_chunks = torch.split(rpooled, batching, dim=0)
res_list = []
for i in range(max(len(rt_chunks), len(rp_chunks))):
result_tensor = rt_chunks[i] if i < len(rt_chunks) else torch.zeros([1])
result_pooled = rp_chunks[i] if i < len(rp_chunks) else torch.zeros([1])
base = copy.deepcopy(V["V0"])
# base[0] is a tuple (tensor, dict), need to reconstruct
old_dict = base[0][1] if len(base[0]) > 1 else {}
new_dict = old_dict.copy()
new_dict["pooled_output"] = result_pooled
base[0] = (result_tensor, new_dict)
res_list.append(base)
else:
# Single output (no batching)
base = copy.deepcopy(V["V0"])
# base[0] is a tuple (tensor, dict), need to reconstruct
old_dict = base[0][1] if len(base[0]) > 1 else {}
new_dict = old_dict.copy()
new_dict["pooled_output"] = rpooled
base[0] = (rtensor, new_dict)
res_list = [base]
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device(res_list, original_device),stack)
+103
View File
@@ -0,0 +1,103 @@
from inspect import cleandoc
import torch
from .helper_functions import parse_expr, get_v_variable, checkLazyNew
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
class FloatMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on Floats.
Inputs:
V: Autogrow float inputs (V0, V1, ...)
FloatFunc: String, describing math expression.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_FloatMathNode",
category="More math/math",
display_name="Float math",
inputs=[
io.Autogrow.Input(
id="V",
template=io.Autogrow.TemplatePrefix(
io.Float.Input("values"), prefix="V", min=1, max=50
),
),
io.MultiType.Input(
io.String.Input("FloatFunc", default="V0", multiline=False),
types=[io.String, MrmthParseTree],
tooltip="Expression to use on inputs",
),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
MrmthStack.Input(
id="stack", tooltip="Access stack between nodes", optional=True
),
],
outputs=[
io.Float.Output(),
MrmthStack.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, FloatFunc, V, remember_stack=False, stack={}):
# remember_stack ani stack nemění lazy logiku
return checkLazyNew(FloatFunc, V, V)
@classmethod
def execute(cls, FloatFunc, V, remember_stack=False, stack={}):
work_stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
variables = {}
# Populate aliases
variables["a"] = V.get("V0", 0.0)
variables["b"] = V.get("V1", 0.0)
variables["c"] = V.get("V2", 0.0)
variables["d"] = V.get("V3", 0.0)
variables["w"] = V.get("V4", 0.0)
variables["x"] = V.get("V5", 0.0)
variables["y"] = V.get("V6", 0.0)
variables["z"] = V.get("V7", 0.0)
# Populate all V inputs
for k, val in V.items():
variables[k] = val if val is not None else 0.0
v_stacked, v_cnt = get_v_variable(variables)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
tree = None
if isinstance(FloatFunc, str):
tree = parse_expr(FloatFunc)
else:
tree = FloatFunc
# scalar execution
visitor = UnifiedMathVisitor(variables, [1], state_storage=work_stack)
result = visitor.visit(tree)
# Result might be float or tensor(scalar)
if torch.is_tensor(result):
result = result.flatten()[0].item()
returned_stack = work_stack if remember_stack else copy.deepcopy(work_stack)
return (float(result), returned_stack)
+322
View File
@@ -0,0 +1,322 @@
import torch
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from .helper_functions import (
checkLazyNew,
generate_dim_variables,
getIndexTensorAlongDim,
parse_expr,
make_zero_like,
as_tensor,
get_v_variable,
get_f_variable,
LazyVariableDict,
)
from comfy_api.latest import io
import comfy.sampler_helpers
import comfy.model_patcher
import comfy.utils
import comfy.hooks
import comfy.samplers
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
class GuiderMathNode(io.ComfyNode):
"""
Enables math expressions on Guiders (sampler inputs) with autogrow support.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_GuiderMathNode",
category="More math/math",
display_name="Guider math",
inputs=[
io.Autogrow.Input(id="V", template=io.Autogrow.TemplatePrefix(io.Guider.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on input guiders. Aliases: a=G0, b=G1, c=G2, d=G3, w=F0, x=F1, y=F2, z=F3. Context: steps, current_step",
),
io.MultiType.Input(
io.String.Input("Expression1", default="sample", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply after generation finishes.",
),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Guider.Output(),
MrmthStack.Output()
],
)
@classmethod
def check_lazy_status(cls, Expression,Expression1, V, F,remember_stack=True,stack={}):
d = checkLazyNew(Expression,V,F)
b = checkLazyNew(Expression1,V,F)
return d|b
@classmethod
def execute(cls, V, F, Expression, Expression1,remember_stack = True, stack=None):
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
guider = MathGuider(V, F, Expression, Expression1, stack);
stack = stack if remember_stack else copy.deepcopy(stack)
return (guider, stack)
class MathGuider:
def __init__(self, V, F, expression,expression1,stack):
self.V = V
self.F = F
self.expression = expression
if isinstance(expression,str):
self.tree = parse_expr(expression)
else:
self.tree = expression
if isinstance(expression1,str):
self.tree1 = parse_expr(expression1)
else:
self.tree1 = expression1
self.inner_model = None # Will be set during sample
self.sigmas = None
self.current_step = 0
self.steps = 0
self.stck = stack
@property
def model_patcher(self):
for v in self.V.values():
if v is not None and hasattr(v, "model_patcher"):
return v.model_patcher
return None
@property
def cfg(self):
"""Delegate cfg to the primary guider."""
for v in self.V.values():
if v is not None and hasattr(v, "cfg"):
return v.cfg
return 1.0 # Default value if no guider has cfg
@cfg.setter
def cfg(self, value):
"""Set cfg on all guiders that support it."""
for v in self.V.values():
if v is not None and hasattr(v, "cfg"):
v.cfg = value
def __call__(self, x, sigma, model_options={}, seed=None):
lazy_g_results = {}
zero_cache = [None]
def get_zero_cache():
if zero_cache[0] is None:
zero_cache[0] = torch.zeros_like(x)
return zero_cache[0]
def make_lazy(guider_obj):
def lazy_eval():
if guider_obj is not None:
return guider_obj(x, sigma, model_options=model_options, seed=seed)
return get_zero_cache()
lazy_eval.is_lazy_var = True
return lazy_eval
for k, guider in self.V.items():
lazy_g_results[k] = make_lazy(guider)
eval_samples, variables = self.setVars(x, sigma, seed, lazy_g_results)
visitor = UnifiedMathVisitor(variables, eval_samples.shape,eval_samples.device,state_storage=self.stck)
result_tensor = visitor.visit(self.tree)
self.current_step = self.current_step + 1;
return as_tensor(result_tensor, eval_samples.shape).to(x.device)
def setVars(self, x, sigma, seed, g_results):
eval_samples = x
ndim = eval_samples.ndim
batch_dim = 0
channel_dim = 1
height_dim = 2
width_dim = 3
time_dim = None
if ndim >= 5:
time_dim = 2
channel_dim = 1
height_dim = 3
width_dim = 4
frame_count = eval_samples.shape[time_dim] if time_dim is not None else 1
def make_lazy_var(value_fn):
value_fn.is_lazy_var = True
return value_fn
variables = LazyVariableDict()
variables.update({
"w": self.F.get("F0", 0.0),
"x": self.F.get("F1", 0.0),
"y": self.F.get("F2", 0.0),
"z": self.F.get("F3", 0.0),
"B": getIndexTensorAlongDim(eval_samples, batch_dim),
"batch": getIndexTensorAlongDim(eval_samples, batch_dim),
"W": float(eval_samples.shape[width_dim]) if width_dim < ndim else 0.0,
"width": float(eval_samples.shape[width_dim]) if width_dim < ndim else 0.0,
"H": float(eval_samples.shape[height_dim]) if height_dim < ndim else 0.0,
"height": float(eval_samples.shape[height_dim]) if height_dim < ndim else 0.0,
"T": float(frame_count),
"batch_count": float(eval_samples.shape[0]),
"N": float(eval_samples.shape[channel_dim]) if channel_dim < ndim else 0.0,
"channel_count": float(eval_samples.shape[channel_dim]) if channel_dim < ndim else 0.0,
"sigma": sigma.item() if isinstance(sigma,torch.Tensor) else sigma,
"seed": seed if seed is not None else 0,
"steps": self.steps,
"current_step": self.current_step,
"sample": x
})
if g_results is not None:
for k, func in g_results.items():
variables[k] = func
variables["a"] = make_lazy_var(lambda: variables["V0"] if "V0" in g_results else make_zero_like(eval_samples))
variables["b"] = make_lazy_var(lambda: variables["V1"] if "V1" in g_results else make_zero_like(eval_samples))
variables["c"] = make_lazy_var(lambda: variables["V2"] if "V2" in g_results else make_zero_like(eval_samples))
variables["d"] = make_lazy_var(lambda: variables["V3"] if "V3" in g_results else make_zero_like(eval_samples))
def lazy_stacked_v():
evaluated_g_results = {k: variables[k] for k in g_results.keys()}
v_stacked, v_cnt = get_v_variable(evaluated_g_results)
return v_stacked
variables["V"] = make_lazy_var(lazy_stacked_v)
def lazy_v_cnt():
evaluated_g_results = {k: variables[k] for k in g_results.keys()}
v_stacked, v_cnt = get_v_variable(evaluated_g_results)
return float(v_cnt)
variables["Vcnt"] = make_lazy_var(lazy_v_cnt)
variables["V_count"] = variables["Vcnt"]
for k, v in self.F.items():
variables[k] = v if v is not None else 0.0
f_stacked, f_cnt = get_f_variable(self.F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
variables.update(generate_dim_variables(eval_samples))
return eval_samples,variables
def sample(self, noise, latent_image, sampler, sigmas, denoise_mask=None, callback=None, disable_pbar=False, seed=None):
self.sigmas = sigmas
self.steps = len(sigmas)
if sigmas.shape[-1] == 0:
return latent_image
active_guiders = [g for g in self.V.values() if g is not None]
if not active_guiders:
return latent_image
primary_guider = active_guiders[0]
cleanup_items = []
try:
for guider in active_guiders:
if hasattr(guider, "original_conds"):
guider.conds = {}
for k in guider.original_conds:
guider.conds[k] = list(map(lambda a: a.copy(), guider.original_conds[k]))
if hasattr(comfy.samplers, "preprocess_conds_hooks") and hasattr(guider, "conds"):
comfy.samplers.preprocess_conds_hooks(guider.conds)
if hasattr(guider, "model_patcher"):
guider._orig_model_options = guider.model_options
guider.model_options = comfy.model_patcher.create_model_options_clone(guider.model_options)
comfy.sampler_helpers.prepare_model_patcher(guider.model_patcher, guider.conds, guider.model_options)
if hasattr(comfy.samplers, "filter_registered_hooks_on_conds"):
comfy.samplers.filter_registered_hooks_on_conds(guider.conds, guider.model_options)
guider.inner_model, guider.conds, guider.loaded_models = comfy.sampler_helpers.prepare_sampling(
guider.model_patcher, noise.shape, guider.conds, guider.model_options
)
cleanup_items.append(guider)
device = primary_guider.model_patcher.load_device
noise = noise.to(device)
latent_image = latent_image.to(device)
sigmas = sigmas.to(device)
for guider in active_guiders:
if hasattr(guider, "model_options"):
comfy.samplers.cast_to_load_options(guider.model_options, device=device, dtype=guider.model_patcher.model_dtype())
patchers = set(g.model_patcher for g in active_guiders if hasattr(g, "model_patcher"))
for p in patchers:
p.pre_run()
try:
if latent_image is not None and torch.count_nonzero(latent_image) > 0:
latent_image = primary_guider.inner_model.process_latent_in(latent_image)
for guider in active_guiders:
if hasattr(guider, "inner_model") and hasattr(guider, "conds"):
guider.conds = comfy.samplers.process_conds(
guider.inner_model, noise, guider.conds, device, latent_image, denoise_mask, seed
)
self.inner_model = primary_guider.inner_model
extra_model_options = comfy.model_patcher.create_model_options_clone(primary_guider.model_options)
extra_model_options.setdefault("transformer_options", {})["sample_sigmas"] = sigmas
extra_args = {"model_options": extra_model_options, "seed": seed}
output = sampler.sample(self, sigmas, extra_args, callback, noise, latent_image, denoise_mask, disable_pbar)
eval_samples, variables = self.setVars(output, 0.0, seed, None)
variables["noise"] = noise
variables["previous_sample"] = latent_image
visitor = UnifiedMathVisitor(variables, eval_samples.shape,state_storage=self.stck)
output = visitor.visit(self.tree1)
if hasattr(primary_guider.inner_model, "process_latent_out"):
output = primary_guider.inner_model.process_latent_out(output.to(torch.float32))
return output
finally:
for p in patchers:
p.cleanup()
finally:
for guider in cleanup_items:
if hasattr(guider, "model_patcher") and hasattr(guider, "loaded_models"):
comfy.sampler_helpers.cleanup_models(guider.conds, guider.loaded_models)
if hasattr(guider, "_orig_model_options"):
comfy.samplers.cast_to_load_options(guider.model_options, device=guider.model_patcher.offload_device)
guider.model_options = guider._orig_model_options
guider.model_patcher.restore_hook_patches()
del guider.inner_model
del guider.loaded_models
del guider.conds
if hasattr(guider, "_orig_model_options"): del guider._orig_model_options
self.inner_model = None
+166
View File
@@ -0,0 +1,166 @@
from .helper_functions import generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, normalize_to_common_shape, make_zero_like, get_v_variable, get_f_variable, checkLazyNew, get_tensor_device, move_to_device
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
import comfy
import torch
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
class ImageMathNode(io.ComfyNode):
"""
Enables math expressions on Images using Autogrow inputs.
Inputs:
V: Autogrow image inputs (V0, V1, ...)
F: Autogrow float inputs (F0, F1, ...)
Image: Expression to apply on input images
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_ImageMathNode",
category="More math/math",
display_name="Image math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Image.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on input images",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
),
io.Int.Input(id="batching", default=0),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies image tensors to the compute device for math and moves the result back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Image.Output(is_output_list=True),
MrmthStack.Output(),
],
)
@classmethod
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile",batching=0,remember_stack=False,use_compute_device=True,stack={}):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, length_mismatch="error",batching=0,remember_stack=False,use_compute_device=True,stack={}):
# I and F are Autogrow.Type which is dict[str, Any]
# Identify all present tensors and their keys
tensor_keys = [k for k, v in V.items() if v is not None]
if not tensor_keys:
raise ValueError("At least one input is required.")
tensors = [V[k] for k in tensor_keys]
original_device = get_tensor_device(tensors[0])
compute_device = comfy.model_management.get_torch_device() if use_compute_device else original_device
working_V = move_to_device(V, compute_device) if compute_device is not None and compute_device != original_device else V
tensors = [working_V[k] for k in tensor_keys]
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
# Normalize all tensors together to find the common target shape
normalized_tensors = normalize_to_common_shape(*tensors, mode=length_mismatch)
V_norm = dict(zip(tensor_keys, normalized_tensors))
# Use first normalized tensor to establish the reference shape
ref_tensor = normalized_tensors[0]
common_shape = ref_tensor.shape
# Setup legacy variables a, b, c, d
ae = V_norm.get("V0", make_zero_like(ref_tensor))
be = V_norm.get("V1", make_zero_like(ae))
ce = V_norm.get("V2", make_zero_like(ae))
de = V_norm.get("V3", make_zero_like(ae))
# Ensure legacy variables are normalized in case they were zero-initialized
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
if(length_mismatch == "error"):
for name, tensor in V.items():
if tensor is not None and tensor.shape[0] != common_shape[0]:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {common_shape[0]} to match largest input.")
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"X": getIndexTensorAlongDim(ae, 2),
"Y": getIndexTensorAlongDim(ae, 1),
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"C": getIndexTensorAlongDim(ae, 3),
"channel": getIndexTensorAlongDim(ae, 3),
"W": float(ae.shape[2]),
"width": float(ae.shape[2]),
"H": float(ae.shape[1]),
"height": float(ae.shape[1]),
"T": float(ae.shape[0]),
"batch_count": float(ae.shape[0]),
"N": float(ae.shape[3]),
"channel_count": float(ae.shape[3]),
} | generate_dim_variables(ae)
# Add all dynamic inputs
variables.update(V_norm)
v_stacked, v_cnt = get_v_variable(V_norm, length_mismatch=length_mismatch)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
for k, val in F.items():
variables[k] = val if val is not None else 0.0
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, ae.shape,ae.device,state_storage=stack)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
result = move_to_device(result, original_device)
if batching and batching > 0:
res = torch.split(result, batching, dim=0)
res_list = []
for result_chunk in res:
res_list.append(result_chunk)
stack = stack if remember_stack else copy.deepcopy(stack)
return (res_list, stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([result], stack)
+270
View File
@@ -0,0 +1,270 @@
from inspect import cleandoc
from comfy_api.latest import io
from .helper_functions import (
generate_dim_variables,
getIndexTensorAlongDim,
parse_expr,
as_tensor,
normalize_to_common_shape,
make_zero_like,
get_v_variable,
get_f_variable,
checkLazyNew,
get_tensor_device,
move_to_device,
)
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
import comfy
import torch
from comfy.nested_tensor import NestedTensor
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
class LatentMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on Latents using Autogrow inputs.
"""
def __init__(self):
pass
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_ag_LatentMathNode",
display_name="Latent math",
category="More math/math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Latent.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on input latents",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched latent batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
),
io.Int.Input(id="batching"),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies latent tensors to the compute device for math and moves the result back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Latent.Output(is_output_list=True),
MrmthStack.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Expression, V, F,batching, length_mismatch="tile",remember_stack=False,use_compute_device=True,stack={}):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression,batching, length_mismatch="tile",remember_stack=False,use_compute_device=True,stack={}) -> io.NodeOutput:
# Determine reference latent
ref_latent = None
for lat in V.values():
if lat is not None:
ref_latent = lat
break
if ref_latent is None:
raise ValueError("At least one input is required.")
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
original_device = get_tensor_device(ref_latent.get("samples"))
compute_device = comfy.model_management.get_torch_device() if use_compute_device else original_device
working_V = move_to_device(V, compute_device) if compute_device is not None and compute_device != original_device else V
# Identify all present tensors and their keys.
# NestedTensor inputs keep their original latent dict under the base key (V0, V1, ...)
# so downstream nodes receive the original NestedTensor, while individual components
# are also exposed as V0_0, V0_1, ... for math expressions.
tensor_keys = []
V_norm_samples = {}
nested_component_keys = {} # maps original key -> list of expanded component variable names
for k, v in working_V.items():
if v is None:
continue
samples = v.get("samples")
if getattr(samples, "is_nested", False):
component_names = []
for idx, t in enumerate(samples.tensors):
comp_key = f"{k}_{idx}"
V_norm_samples[comp_key] = t
component_names.append(comp_key)
nested_component_keys[k] = component_names
# Keep the original NestedTensor under the base key so V0 returns it unchanged
tensor_keys.append(k)
V_norm_samples[k] = samples
else:
tensor_keys.append(k)
V_norm_samples[k] = samples
# Normalize all together; NestedTensor base entries are skipped but other non-tensor values are still supported
at_list = [V_norm_samples[k] for k in tensor_keys if torch.is_tensor(V_norm_samples[k])]
if at_list:
normalized_samples = normalize_to_common_shape(*at_list, mode=length_mismatch)
norm_iter = iter(normalized_samples)
for key in tensor_keys:
if not torch.is_tensor(V_norm_samples[key]):
continue
V_norm_samples[key] = next(norm_iter)
# Add nested component variables to the list of available inputs for variable binding
tensor_keys.extend([c for components in nested_component_keys.values() for c in components])
def _resolve_alias(base):
# Aliases a/b/c/d should point to the first tensor component of a NestedTensor input
components = nested_component_keys.get(base)
if components:
return V_norm_samples[components[0]]
return V_norm_samples.get(base)
first_sample = next(iter(V_norm_samples.values()))
if not torch.is_tensor(first_sample):
# If the first value is a NestedTensor, grab its first component for metadata
first_sample = first_sample.tensors[0]
ae_res = _resolve_alias("V0")
ae = ae_res if ae_res is not None else make_zero_like(first_sample)
be_res = _resolve_alias("V1")
be = be_res if be_res is not None else make_zero_like(ae)
ce_res = _resolve_alias("V2")
ce = ce_res if ce_res is not None else make_zero_like(ae)
de_res = _resolve_alias("V3")
de = de_res if de_res is not None else make_zero_like(ae)
# Ensure legacy are normalized
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
if length_mismatch == "error":
for name in tensor_keys:
sample = V_norm_samples.get(name)
if sample is None:
continue
if not torch.is_tensor(sample):
continue
if sample.shape[0] != ae.shape[0]:
raise ValueError(f"Input '{name}' has shape {sample.shape[0]}, expected {ae.shape[0]} to match input.")
# parse expression once
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
ndim = ae.ndim
batch_dim = 0
channel_dim = -3
height_dim = -2
width_dim = -1
time_dim = None
if ndim >= 5:
time_dim = -4
frame_count = ae.shape[time_dim] if time_dim is not None else ae.shape[batch_dim]
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"X": getIndexTensorAlongDim(ae, width_dim),
"Y": getIndexTensorAlongDim(ae, height_dim),
"B": getIndexTensorAlongDim(ae, batch_dim),
"batch": getIndexTensorAlongDim(ae, batch_dim),
"C": getIndexTensorAlongDim(ae, channel_dim),
"channel": getIndexTensorAlongDim(ae, channel_dim),
"W": float(ae.shape[width_dim]),
"width": float(ae.shape[width_dim]),
"H": float(ae.shape[height_dim]),
"height": float(ae.shape[height_dim]),
"T": float(frame_count),
"batch_count": float(ae.shape[batch_dim]),
"N": float(ae.shape[channel_dim]),
"channel_count": float(ae.shape[channel_dim]),
} | generate_dim_variables(ae)
if time_dim is not None:
F_idx = getIndexTensorAlongDim(ae, time_dim)
variables.update({"frame_idx": F_idx, "frame": F_idx, "frame_count": frame_count})
# Add all dynamic inputs
variables.update(V_norm_samples)
v_stacked, v_cnt = get_v_variable(V_norm_samples, length_mismatch=length_mismatch)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
for k, v in F.items():
variables[k] = v if v is not None else 0.0
visitor = UnifiedMathVisitor(variables, ae.shape,ae.device,state_storage=stack)
raw_result = visitor.visit(tree)
result_t = as_tensor(raw_result, ae.shape)
result_t = move_to_device(result_t, original_device)
result_latent = ref_latent.copy()
# If the visitor produced a NestedTensor (whole-tensor arithmetic on a NestedTensor
# input), propagate that through as a single downstream-compatible latent.
if getattr(result_t, "is_nested", False):
rl = result_latent.copy()
rl["samples"] = result_t
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device([rl], original_device), stack)
# If the visitor produced a list/tuple, emit each element as a separate latent.
if isinstance(result_t, (list, tuple)) and result_t and isinstance(result_t[0], torch.Tensor):
results = []
for comp in result_t:
rl = result_latent.copy()
rl["samples"] = comp
results.append(rl)
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device(results, original_device), stack)
if batching > 0:
res = torch.split(result_t, batching)
results = []
for result_tensor in res:
rl = result_latent.copy()
rl["samples"] = result_tensor
results.append(rl)
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device(results, original_device), stack)
rl = result_latent.copy()
rl["samples"] = result_t
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device([rl], original_device), stack)
+80
View File
@@ -0,0 +1,80 @@
from inspect import cleandoc
from comfy_api.latest import io
import copy
import comfy.utils
from .helper_functions import checkLazyNew
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
from .loraDictCommon import calculate_lora_dict_autogrow, calculate_lora_dict_dict_mode
class LoraMathNode(io.ComfyNode):
"""
This node enables the use of math expressions directly on raw LoRA
tensors (LORA_MODEL: dict of lora_up.weight / lora_down.weight / alpha /
diff tensors) using Autogrow inputs.
Unlike Model/VAE/CLIP Math, these tensors are not attached to a
ModelPatcher, so there is no base-vs-patched distinction - the tensors
already *are* the LoRA's deltas - and therefore no V{n}_d.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_LoraMathNode",
display_name="Lora Math",
category="More math/math",
inputs=[
io.Autogrow.Input(id="V", template=io.Autogrow.TemplatePrefix(io.LoraModel.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String, MrmthParseTree],
tooltip="Expression to apply on LoRA tensors",
),
io.Boolean.Input(
id="dict_mode",
default=False,
display_name="Dictionary mode",
tooltip="Evaluates LoRA tensors as dictionaries. Per-layer and dimension variables are unavailable.",
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies LoRA tensors to the compute device for math and moves the result back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes", optional=True)
],
outputs=[
io.LoraModel.Output(),
MrmthStack.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Expression, V, F, use_compute_device=True, stack={}, dict_mode=False):
return checkLazyNew(Expression, V, F)
@classmethod
def execute(cls, V, F, Expression, use_compute_device=True, stack={}, dict_mode=False) -> io.NodeOutput:
if not any(v is not None for v in V.values()):
raise ValueError("At least one input LoRA is required.")
stack = copy.deepcopy(stack) if stack is not None else {}
ref_lora = V.get("V0")
if ref_lora is None:
for m in V.values():
if m is not None:
ref_lora = m
break
layer_count = len(ref_lora) if ref_lora is not None else 0
pbar = comfy.utils.ProgressBar(layer_count)
aliases = {"a": "V0", "b": "V1", "c": "V2", "d": "V3", "w": "F0", "x": "F1", "y": "F2", "z": "F3"}
calculate_lora_dict = calculate_lora_dict_dict_mode if dict_mode else calculate_lora_dict_autogrow
result = calculate_lora_dict(Expression, V=V, F=F, pbar=pbar, mapping=aliases, stack=stack, use_compute_device=use_compute_device)
return (result, stack)
+147
View File
@@ -0,0 +1,147 @@
from .helper_functions import generate_dim_variables,parse_expr, getIndexTensorAlongDim, as_tensor, normalize_to_common_shape,make_zero_like, get_v_variable, get_f_variable, checkLazyNew
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
import torch
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
class MaskMathNode(io.ComfyNode):
"""
Enables math expressions on Masks using Autogrow inputs.
Inputs:
V: Autogrow mask inputs (V0, V1, ...)
F: Autogrow float inputs (F0, F1, ...)
Mask: Expression to apply on input masks
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_MaskMathNode",
category="More math/math",
display_name="Mask math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Mask.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on input masks",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched mask batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
),
io.Int.Input(id="batching", default=0),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Mask.Output(is_output_list=True),
MrmthStack.Output(),
],
)
@classmethod
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile",batching=0,remember_stack=False,stack={}):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, length_mismatch="tile",batching=0,remember_stack=False,stack={}):
# Identify all present tensors and their keys
tensor_keys = [k for k, v in V.items() if v is not None]
if not tensor_keys:
raise ValueError("At least one input is required.")
tensors = [V[k] for k in tensor_keys]
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
# Normalize all tensors together
normalized_tensors = normalize_to_common_shape(*tensors, mode=length_mismatch)
V_norm = dict(zip(tensor_keys, normalized_tensors))
# Establish reference shape
ref_tensor = normalized_tensors[0]
common_shape = ref_tensor.shape
if(length_mismatch == "error"):
for name, tensor in V.items():
if tensor is not None and tensor.shape[0] != common_shape[0]:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {common_shape[0]} to match largest input.")
# Setup legacy variables a, b, c, d
ae = V_norm.get("V0", make_zero_like(ref_tensor))
be = V_norm.get("V1", make_zero_like(ae))
ce = V_norm.get("V2", make_zero_like(ae))
de = V_norm.get("V3", make_zero_like(ae))
# Ensure legacy are normalized
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"X": getIndexTensorAlongDim(ae, 2),
"Y": getIndexTensorAlongDim(ae, 1),
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"W": float(ae.shape[2]),
"width": float(ae.shape[2]),
"H": float(ae.shape[1]),
"height": float(ae.shape[1]),
"T": float(ae.shape[0]),
"batch_count": float(ae.shape[0]),
} | generate_dim_variables(ae)
v_stacked, v_cnt = get_v_variable(V_norm, length_mismatch=length_mismatch)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
# Add all dynamic inputs
variables.update(V_norm)
for k, val in F.items():
variables[k] = val if val is not None else 0.0
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, ae.shape,ae.device,state_storage=stack)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
if batching and batching > 0:
res = torch.split(result, batching, dim=0)
res_list = []
for result_chunk in res:
res_list.append(result_chunk)
stack = stack if remember_stack else copy.deepcopy(stack)
return (res_list, stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([result], stack)
+92
View File
@@ -0,0 +1,92 @@
from inspect import cleandoc
from comfy_api.latest import io
from .helper_functions import checkLazyNew
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
import comfy.utils
from .modelLikeCommon import calculate_patches_autogrow, calculate_patches_dict_mode
class ModelMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on Model weights (state_dict) using Autogrow inputs.
Functionally acts as a custom model merge.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_ModelMathNode",
display_name="Model Math",
category="More math/math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Model.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on weights",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
),
io.Boolean.Input(
id="dict_mode",
default=False,
display_name="Dictionary mode",
tooltip="Evaluates model weights as dictionaries. Per-layer and dimension variables are unavailable.",
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies model tensors to the compute device for math and moves the patches back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Model.Output(),
MrmthStack.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile",use_compute_device=True,stack={},dict_mode=False):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, length_mismatch="tile",use_compute_device=True,stack={},dict_mode=False) -> io.NodeOutput:
# Determine reference model for cloning
a = V.get("V0")
stack = copy.deepcopy(stack) if stack is not None else {}
if a is None:
# Try finding first valid model
for m in V.values():
if m is not None:
a = m
break
if a is None:
raise ValueError("At least one input model is required.")
layer_count = V.get("V0").model.state_dict().__len__() if hasattr(V.get("V0"), "model") and hasattr(V.get("V0").model, "state_dict") else 0
pbar = comfy.utils.ProgressBar(layer_count)
aliases = {"a": "V0", "b": "V1", "c": "V2", "d": "V3", "w": "F0", "x": "F1", "y": "F2", "z": "F3"}
calculate_patches = calculate_patches_dict_mode if dict_mode else calculate_patches_autogrow
patches = calculate_patches(Expression, V=V, F=F,pbar=pbar, mapping=aliases,stack=stack,use_compute_device=use_compute_device)
out_model = a.clone()
if patches:
out_model.add_patches(patches, 1.0, 1.0)
return (out_model,stack)
+138
View File
@@ -0,0 +1,138 @@
from .helper_functions import generate_dim_variables, as_tensor, parse_expr, getIndexTensorAlongDim, make_zero_like, get_v_variable, get_f_variable, checkLazyNew
from comfy_api.latest import io
import torch
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
class NoiseMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on noise generators.
inputs:
a, b, c, d:
Noise generators.
w, x, y, z:
Floats.
Noise expression:
The expression to apply on those noise generators.
Note that variables X, Y, W, H, C, batch, batch_count, input_latent refer to input_latent.
outputs:
NOISE:
The resulting noise generator.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_NoiseMathNode",
display_name="Noise math",
category="More math/math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Noise.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Noise", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression for noise",
),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Noise.Output(),
MrmthStack.Output(),
],
)
@classmethod
def check_lazy_status(cls, Noise, V, F,remember_stack=False,stack={}):
return checkLazyNew(Noise,V,F)
@classmethod
def execute(cls, Noise, V,F,remember_stack=False,stack={}):
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
executer = NoiseExecutor(V,F, Noise,stack)
stack = stack if remember_stack else copy.deepcopy(stack)
return (executer,stack)
class NoiseExecutor:
def __init__(self, V,F, expr,stack):
self.V = V
self.F = F
if isinstance(expr,str):
self.tree = parse_expr(expr)
else:
self.tree = expr
self.stack = stack
seed = -1
def generate_noise(self, input_latent: torch.Tensor) -> torch.Tensor:
samples = input_latent["samples"]
vals = {v: (self.V[v].generate_noise(input_latent) if self.V[v] is not None else make_zero_like(samples)) for v in self.V}
ndim = samples.ndim
batch_dim = 0
channel_dim = -3
height_dim = -2
width_dim = -1
time_dim = None
if ndim >= 5:
time_dim = -4
frame_count = samples.shape[time_dim] if time_dim is not None else samples.shape[batch_dim]
B = getIndexTensorAlongDim(samples, batch_dim)
W = getIndexTensorAlongDim(samples, width_dim)
H = getIndexTensorAlongDim(samples, height_dim)
C = getIndexTensorAlongDim(samples, channel_dim)
variables = {
"a": vals.get("V0") if "V0" in vals else make_zero_like(samples),
"b": vals.get("V1") if "V1" in vals else make_zero_like(samples),
"c": vals.get("V2") if "V2" in vals else make_zero_like(samples),
"d": vals.get("V3") if "V3" in vals else make_zero_like(samples),
"w": self.F.get("F0", 0.0),
"x": self.F.get("F1", 0.0),
"y": self.F.get("F2", 0.0),
"z": self.F.get("F3", 0.0),
"B": B, "batch": B,
"X": W, "width": float(samples.shape[width_dim]),
"Y": H, "height": float(samples.shape[height_dim]),
"C": C, "channel": C,
"W": float(samples.shape[width_dim]), "H": float(samples.shape[height_dim]), "I": samples,
"T": float(frame_count), "N": float(samples.shape[channel_dim]),
"batch_count": float(samples.shape[batch_dim]), "channel_count": float(samples.shape[channel_dim]),
"input_latent": samples,
} | generate_dim_variables(samples) | vals | self.F
v_stacked, v_cnt = get_v_variable(vals)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(self.F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
if time_dim is not None:
F = getIndexTensorAlongDim(samples, time_dim)
variables.update({"frame": F, "frame_count": frame_count})
visitor = UnifiedMathVisitor(variables, samples.shape,samples.device,state_storage=self.stack)
result = visitor.visit(self.tree)
result = as_tensor(result, samples.shape)
return result
+14
View File
@@ -0,0 +1,14 @@
import antlr4
from comfy_api.latest import io
@io.comfytype(io_type="SYNTAX_TREE")
class MrmthParseTree(io.ComfyTypeIO):
Type = antlr4.ParseTreeVisitor # Python type hint
class Input(io.Input):
def __init__(self, id: str, **kwargs):
super().__init__(id, **kwargs)
class Output(io.Output):
def __init__(self, **kwargs):
super().__init__(**kwargs)
+6
View File
@@ -0,0 +1,6 @@
class LambdaFunction:
__slots__ = ("params", "body", "env")
def __init__(self, params, body, env):
self.params = params
self.body = body
self.env = env.copy() if env is not None else {}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+412
View File
@@ -0,0 +1,412 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
LINSPACE=53
LOGSPACE=54
TOPK=55
BOTK=56
PINV=57
SUM=58
MEAN=59
STD=60
VAR=61
QUARTILE=62
PERCENTILE=63
QUANTILE=64
DOT=65
MOMENT=66
ERF=67
ERFINV=68
ANY=69
ALL=70
EDGE=71
GAUSSIAN=72
MEDIAN=73
MODE=74
CUMSUM=75
CUMPROD=76
TOPK_IND=77
BOTK_IND=78
CUBIC_EASE=79
ELASTIC_EASE=80
SINE_EASE=81
SMOOTHERSTEP=82
DIST=83
REMAP=84
COSSIM=85
COUNT=86
KEYS=87
ENABLE_TRACKING=88
DISABLE_TRACKING=89
RESET_TRACKING=90
GET_CHANGED_KEYS=91
GET_ADDED_KEYS=92
GET_REMOVED_KEYS=93
GET_UNCHANGED_KEYS=94
REPEAT=95
FLATTEN=96
APPEND=97
GET_VALUE=98
FLOW_APPLY=99
BATCH_SHUFFLE=100
MOTION_MASK=101
FLOW_TO_IMAGE=102
FLOW_MAG=103
FLOW_ANG=104
WHERE=105
HISTOGRAM=106
OVERLAY=107
PAD=108
CROSS=109
MATMUL=110
RIFE=111
BNOT=112
BITCOUNT=113
SHAPE=114
BAND=115
XOR=116
BOR=117
TENSOR=118
ADD_KEY=119
REMOVE_KEY=120
PUSH=121
POP=122
CLEAR=123
HAS=124
GET=125
CONCAT=126
INT=127
FLOAT=128
UPPER=129
LOWER=130
TRIM=131
SPLIT=132
JOIN=133
SUBSTRING=134
FIND=135
REPLACE=136
DILATE=137
ERODE=138
MORPH_OPEN=139
MORPH_CLOSE=140
RGB_TO_OKLAB=141
RGB_TO_CIELAB=142
OKLAB_TO_RGB=143
CIELAB_TO_RGB=144
RGB_TO_HSV=145
HSV_TO_RGB=146
INT_TO_RGB=147
RGB_TO_INT=148
INTERPOLATE_LINEAR=149
INTERPOLATE_AREA=150
INTERPOLATE_NEAREST=151
TEXT_IMAGE=152
AS_NESTED=153
SVD=154
DIAG=155
IF=156
ELSE=157
WHILE=158
FOR=159
IN=160
BREAK=161
CONTINUE=162
RETURN=163
TIMESTAMP=164
SORT=165
ARGSORT=166
ARGMIN=167
ARGMAX=168
SOFTMAX=169
SOFTMIN=170
UNIQUE=171
FLIP=172
STARTSWITH=173
ENDSWITH=174
SQUEEZE=175
UNSQUEEZE=176
ROLL=177
COV=178
CORR=179
ENTROPY=180
CROP=181
NONE=182
COORDS=183
NOISE=184
RAND=185
CAUCHY=186
EXPONENTIAL=187
LOGNORMAL=188
BERNOULLI=189
POISSON=190
GAMMADIST=191
BETADIST=192
LAPLACEDIST=193
GUMBELDIST=194
WEIBULLDIST=195
CHI2DIST=196
STUDENTTDIST=197
PERLIN=198
CELLULAR=199
PLASMA=200
RIDGED=201
DOMAIN_WARP=202
PLUS=203
MINUS=204
MULT=205
DIV=206
MOD=207
POW=208
LSHIFT=209
RSHIFT=210
GE=211
GT=212
LE=213
LT=214
EQ=215
EQUEALS=216
PLUS_EQ=217
MINUS_EQ=218
MULT_EQ=219
DIV_EQ=220
MOD_EQ=221
NE=222
PIPE=223
LPAREN=224
RPAREN=225
COMMA=226
SEMICOLON=227
ARROW=228
LBRACKET=229
RBRACKET=230
QUESTION=231
COLON=232
LBRACE=233
RBRACE=234
NUMBER=235
CONSTANT=236
STRING=237
VARIABLE=238
SL_COMMENT=239
ML_COMMENT=240
WS=241
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'linspace'=53
'logspace'=54
'topk'=55
'botk'=56
'pinv'=57
'sum'=58
'mean'=59
'std'=60
'var'=61
'quantile'=64
'dot'=65
'moment'=66
'erf'=67
'erfinv'=68
'any'=69
'all'=70
'edge'=71
'median'=73
'mode'=74
'cumsum'=75
'cumprod'=76
'smootherstep'=82
'remap'=84
'keys'=87
'repeat'=95
'flatten'=96
'append'=97
'get_value'=98
'flow_apply'=99
'motion_mask'=101
'flow_to_image'=102
'where'=105
'overlay'=107
'pad'=108
'cross'=109
'matmul'=110
'rife'=111
'shape'=114
'tensor'=118
'add_key'=119
'stack_push'=121
'stack_pop'=122
'stack_clear'=123
'stack_has'=124
'stack_get'=125
'int'=127
'float'=128
'upper'=129
'lower'=130
'trim'=131
'split'=132
'join'=133
'find'=135
'replace'=136
'dilate'=137
'erode'=138
'morph_open'=139
'morph_close'=140
'rgb_to_oklab'=141
'rgb_to_cielab'=142
'oklab_to_rgb'=143
'cielab_to_rgb'=144
'rgb_to_hsv'=145
'hsv_to_rgb'=146
'int_to_rgb'=147
'rgb_to_int'=148
'interpolate_linear'=149
'interpolate_area'=150
'text_image'=152
'if'=156
'else'=157
'while'=158
'for'=159
'in'=160
'break'=161
'continue'=162
'return'=163
'sort'=165
'argsort'=166
'argmin'=167
'argmax'=168
'softmax'=169
'softmin'=170
'unique'=171
'flip'=172
'startswith'=173
'endswith'=174
'squeeze'=175
'unsqueeze'=176
'roll'=177
'cov'=178
'entropy'=180
'crop'=181
'+'=203
'-'=204
'*'=205
'/'=206
'%'=207
'^'=208
'<<'=209
'>>'=210
'>='=211
'>'=212
'<='=213
'<'=214
'=='=215
'='=216
'+='=217
'-='=218
'*='=219
'/='=220
'%='=221
'!='=222
'|'=223
'('=224
')'=225
','=226
';'=227
'->'=228
'['=229
']'=230
'?'=231
':'=232
'{'=233
'}'=234
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+412
View File
@@ -0,0 +1,412 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
LINSPACE=53
LOGSPACE=54
TOPK=55
BOTK=56
PINV=57
SUM=58
MEAN=59
STD=60
VAR=61
QUARTILE=62
PERCENTILE=63
QUANTILE=64
DOT=65
MOMENT=66
ERF=67
ERFINV=68
ANY=69
ALL=70
EDGE=71
GAUSSIAN=72
MEDIAN=73
MODE=74
CUMSUM=75
CUMPROD=76
TOPK_IND=77
BOTK_IND=78
CUBIC_EASE=79
ELASTIC_EASE=80
SINE_EASE=81
SMOOTHERSTEP=82
DIST=83
REMAP=84
COSSIM=85
COUNT=86
KEYS=87
ENABLE_TRACKING=88
DISABLE_TRACKING=89
RESET_TRACKING=90
GET_CHANGED_KEYS=91
GET_ADDED_KEYS=92
GET_REMOVED_KEYS=93
GET_UNCHANGED_KEYS=94
REPEAT=95
FLATTEN=96
APPEND=97
GET_VALUE=98
FLOW_APPLY=99
BATCH_SHUFFLE=100
MOTION_MASK=101
FLOW_TO_IMAGE=102
FLOW_MAG=103
FLOW_ANG=104
WHERE=105
HISTOGRAM=106
OVERLAY=107
PAD=108
CROSS=109
MATMUL=110
RIFE=111
BNOT=112
BITCOUNT=113
SHAPE=114
BAND=115
XOR=116
BOR=117
TENSOR=118
ADD_KEY=119
REMOVE_KEY=120
PUSH=121
POP=122
CLEAR=123
HAS=124
GET=125
CONCAT=126
INT=127
FLOAT=128
UPPER=129
LOWER=130
TRIM=131
SPLIT=132
JOIN=133
SUBSTRING=134
FIND=135
REPLACE=136
DILATE=137
ERODE=138
MORPH_OPEN=139
MORPH_CLOSE=140
RGB_TO_OKLAB=141
RGB_TO_CIELAB=142
OKLAB_TO_RGB=143
CIELAB_TO_RGB=144
RGB_TO_HSV=145
HSV_TO_RGB=146
INT_TO_RGB=147
RGB_TO_INT=148
INTERPOLATE_LINEAR=149
INTERPOLATE_AREA=150
INTERPOLATE_NEAREST=151
TEXT_IMAGE=152
AS_NESTED=153
SVD=154
DIAG=155
IF=156
ELSE=157
WHILE=158
FOR=159
IN=160
BREAK=161
CONTINUE=162
RETURN=163
TIMESTAMP=164
SORT=165
ARGSORT=166
ARGMIN=167
ARGMAX=168
SOFTMAX=169
SOFTMIN=170
UNIQUE=171
FLIP=172
STARTSWITH=173
ENDSWITH=174
SQUEEZE=175
UNSQUEEZE=176
ROLL=177
COV=178
CORR=179
ENTROPY=180
CROP=181
NONE=182
COORDS=183
NOISE=184
RAND=185
CAUCHY=186
EXPONENTIAL=187
LOGNORMAL=188
BERNOULLI=189
POISSON=190
GAMMADIST=191
BETADIST=192
LAPLACEDIST=193
GUMBELDIST=194
WEIBULLDIST=195
CHI2DIST=196
STUDENTTDIST=197
PERLIN=198
CELLULAR=199
PLASMA=200
RIDGED=201
DOMAIN_WARP=202
PLUS=203
MINUS=204
MULT=205
DIV=206
MOD=207
POW=208
LSHIFT=209
RSHIFT=210
GE=211
GT=212
LE=213
LT=214
EQ=215
EQUEALS=216
PLUS_EQ=217
MINUS_EQ=218
MULT_EQ=219
DIV_EQ=220
MOD_EQ=221
NE=222
PIPE=223
LPAREN=224
RPAREN=225
COMMA=226
SEMICOLON=227
ARROW=228
LBRACKET=229
RBRACKET=230
QUESTION=231
COLON=232
LBRACE=233
RBRACE=234
NUMBER=235
CONSTANT=236
STRING=237
VARIABLE=238
SL_COMMENT=239
ML_COMMENT=240
WS=241
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'linspace'=53
'logspace'=54
'topk'=55
'botk'=56
'pinv'=57
'sum'=58
'mean'=59
'std'=60
'var'=61
'quantile'=64
'dot'=65
'moment'=66
'erf'=67
'erfinv'=68
'any'=69
'all'=70
'edge'=71
'median'=73
'mode'=74
'cumsum'=75
'cumprod'=76
'smootherstep'=82
'remap'=84
'keys'=87
'repeat'=95
'flatten'=96
'append'=97
'get_value'=98
'flow_apply'=99
'motion_mask'=101
'flow_to_image'=102
'where'=105
'overlay'=107
'pad'=108
'cross'=109
'matmul'=110
'rife'=111
'shape'=114
'tensor'=118
'add_key'=119
'stack_push'=121
'stack_pop'=122
'stack_clear'=123
'stack_has'=124
'stack_get'=125
'int'=127
'float'=128
'upper'=129
'lower'=130
'trim'=131
'split'=132
'join'=133
'find'=135
'replace'=136
'dilate'=137
'erode'=138
'morph_open'=139
'morph_close'=140
'rgb_to_oklab'=141
'rgb_to_cielab'=142
'oklab_to_rgb'=143
'cielab_to_rgb'=144
'rgb_to_hsv'=145
'hsv_to_rgb'=146
'int_to_rgb'=147
'rgb_to_int'=148
'interpolate_linear'=149
'interpolate_area'=150
'text_image'=152
'if'=156
'else'=157
'while'=158
'for'=159
'in'=160
'break'=161
'continue'=162
'return'=163
'sort'=165
'argsort'=166
'argmin'=167
'argmax'=168
'softmax'=169
'softmin'=170
'unique'=171
'flip'=172
'startswith'=173
'endswith'=174
'squeeze'=175
'unsqueeze'=176
'roll'=177
'cov'=178
'entropy'=180
'crop'=181
'+'=203
'-'=204
'*'=205
'/'=206
'%'=207
'^'=208
'<<'=209
'>>'=210
'>='=211
'>'=212
'<='=213
'<'=214
'=='=215
'='=216
'+='=217
'-='=218
'*='=219
'/='=220
'%='=221
'!='=222
'|'=223
'('=224
')'=225
','=226
';'=227
'->'=228
'['=229
']'=230
'?'=231
':'=232
'{'=233
'}'=234
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
from .antlr_router import get_antlr_modules
_, _, MathExprVisitor = get_antlr_modules()
class MoreMathValidationVisitor(MathExprVisitor):
def __init__(self, initial_variables, initial_functions):
self.variables = set(initial_variables.keys())
self.functions = set(initial_functions.keys())
self._scope_stack = []
def visitIfStmt(self, ctx):
self.visit(ctx.condition())
self.visit(ctx.thenBody())
if ctx.elseBody():
self.visit(ctx.elseBody())
return None
def visitWhileStmt(self, ctx):
self.visit(ctx.condition())
self.visit(ctx.body())
return None
def visitFunctionDef(self, ctx):
func_name = ctx.VARIABLE().getText()
self.functions.add(func_name)
return None
def visitCallExp(self, ctx):
func_name = ctx.VARIABLE().getText()
if func_name not in self.functions and func_name not in self.variables:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: unknown function or variable: {func_name}")
if ctx.exprList():
for e in ctx.exprList().expr():
self.visit(e)
return "NOT_NULL"
def visitForStmt(self, ctx):
iterator_name = ctx.VARIABLE().getText()
source_expr = ctx.expr()
self.visit(source_expr)
self._scope_stack.append(self.variables.copy())
self.variables.add(iterator_name)
try:
if ctx.block():
self.visit(ctx.block())
elif ctx.expr():
self.visit(ctx.expr())
finally:
self.variables = self._scope_stack.pop()
return None
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
import sys
import antlr4
def get_antlr_modules():
runtime_version = "0.0.0"
try:
if sys.version_info >= (3, 8):
import importlib.metadata
runtime_version = importlib.metadata.version("antlr4-python3-runtime")
else:
import pkg_resources
runtime_version = pkg_resources.get_distribution("antlr4-python3-runtime").version
except Exception:
runtime_version = getattr(antlr4, "__version__", "4.13.2")
if runtime_version.startswith("4.9"):
from .legacy.MathExprLexer import MathExprLexer
from .legacy.MathExprParser import MathExprParser
from .legacy.MathExprVisitor import MathExprVisitor
else:
from .grammer.MathExprLexer import MathExprLexer
from .grammer.MathExprParser import MathExprParser
from .grammer.MathExprVisitor import MathExprVisitor
return MathExprLexer, MathExprParser, MathExprVisitor
+481
View File
@@ -0,0 +1,481 @@
<#
.SYNOPSIS
Build script for the more_math ANTLR grammar and its generated symbol tables.
.DESCRIPTION
1. Compiles more_math/Parser/MathExpr.g4 into more_math/Parser/grammer
(the parser antlr_router.py loads for current antlr4-python3-runtime
versions), using whatever ANTLR toolchain is available on this
machine (an 'antlr4' launcher on PATH, or 'java -jar <jar>').
2. Parses MathExpr.g4 itself (keywords, constants, function tokens and
their doc-comment signatures/descriptions) and regenerates:
- more_math/Parser/inbuilt_symbols.py
- web/inbuilt_symbols.js
so that the Python interpreter and the web autocomplete UI both read
an up-to-date, identical view of the language's built-in symbols.
.PARAMETER SkipAntlr
Skip step 1 (grammar compilation) and only regenerate the symbol tables.
.PARAMETER AntlrJar
Path to an antlr-*-complete.jar to use for step 1. Defaults to
$env:ANTLR_JAR, then tools\antlr.jar next to this script.
.PARAMETER AntlrLegacyJar
Optional path to an ANTLR 4.9.x complete jar to use for regenerating
more_math/Parser/legacy (the antlr4-python3-runtime==4.9.x compatibility
path used by antlr_router.py). Defaults to $env:ANTLR_LEGACY_JAR. When
not given, the 'antlr4' launcher (antlr4-tools) is tried instead, pinned
to -AntlrLegacyVersion, same as the main grammar target.
.PARAMETER AntlrVersion
ANTLR release to use when the 'antlr4' launcher comes from the
antlr4-tools pip package (it otherwise queries Maven Central for the
latest release on every run). Defaults to $env:ANTLR4_TOOLS_ANTLR_VERSION,
then '4.13.2' to match the version more_math/Parser/grammer was last
generated with. Ignored when using a plain jar/java invocation.
.PARAMETER AntlrLegacyVersion
Same as -AntlrVersion, but for the more_math/Parser/legacy (ANTLR 4.9.x)
target. Defaults to $env:ANTLR4_TOOLS_ANTLR_LEGACY_VERSION, then '4.9.3'
to match the version more_math/Parser/legacy was last generated with.
#>
[CmdletBinding()]
param(
[switch]$SkipAntlr,
[string]$AntlrJar = $env:ANTLR_JAR,
[string]$AntlrLegacyJar = $env:ANTLR_LEGACY_JAR,
[string]$AntlrVersion = $(if ($env:ANTLR4_TOOLS_ANTLR_VERSION) { $env:ANTLR4_TOOLS_ANTLR_VERSION } else { '4.13.2' }),
[string]$AntlrLegacyVersion = $(if ($env:ANTLR4_TOOLS_ANTLR_LEGACY_VERSION) { $env:ANTLR4_TOOLS_ANTLR_LEGACY_VERSION } else { '4.9.3' })
)
$ErrorActionPreference = 'Stop'
# This script lives at more_math/Parser/compile.ps1, two levels under the
# node root (more_math/Parser -> more_math -> node root).
$ParserDir = $PSScriptRoot
$NodeRoot = Split-Path (Split-Path $ParserDir -Parent) -Parent
$GrammarFile = Join-Path $ParserDir 'MathExpr.g4'
$GrammarOutDir = Join-Path $ParserDir 'grammer'
$LegacyOutDir = Join-Path $ParserDir 'legacy'
$WebDir = Join-Path $NodeRoot 'web'
$PySymbolsPath = Join-Path $ParserDir 'inbuilt_symbols.py'
$JsSymbolsPath = Join-Path $WebDir 'inbuilt_symbols.js'
if (-not (Test-Path $GrammarFile)) {
throw "Grammar file not found: $GrammarFile"
}
# ---------------------------------------------------------------------------
# Step 1: ANTLR compilation
# ---------------------------------------------------------------------------
function Invoke-AntlrCompile {
# Runs with cwd = $ParserDir and passes ANTLR just the grammar's leaf
# filename, so the "# Generated from MathExpr.g4 by ANTLR ..." header it
# writes into the output files stays a plain relative name instead of
# embedding this machine's local absolute path.
param(
[Parameter(Mandatory)][string]$Grammar,
[Parameter(Mandatory)][string]$OutDir,
[string]$Jar,
[string]$Version = $AntlrVersion
)
$grammarLeaf = Split-Path -Leaf $Grammar
$antlrCmd = Get-Command antlr4 -ErrorAction SilentlyContinue
if ($antlrCmd) {
# antlr4-tools (the pip-installed launcher) otherwise hits Maven
# Central on every invocation to resolve "latest"; pin it so the
# build is reproducible and doesn't need that network call.
$previousVersionEnv = $env:ANTLR4_TOOLS_ANTLR_VERSION
$env:ANTLR4_TOOLS_ANTLR_VERSION = $Version
Push-Location $ParserDir
try {
Write-Host "[antlr] antlr4 (v$Version) -Dlanguage=Python3 -visitor -o `"$OutDir`" `"$grammarLeaf`""
& $antlrCmd.Source -Dlanguage=Python3 -visitor -o $OutDir $grammarLeaf
if ($LASTEXITCODE -ne 0) { throw "antlr4 failed with exit code $LASTEXITCODE" }
} finally {
Pop-Location
$env:ANTLR4_TOOLS_ANTLR_VERSION = $previousVersionEnv
}
return $true
}
$javaCmd = Get-Command java -ErrorAction SilentlyContinue
if (-not $javaCmd) {
return $false
}
if (-not $Jar) {
$default = Join-Path $NodeRoot 'tools\antlr.jar'
if (Test-Path $default) { $Jar = $default }
}
if (-not $Jar -or -not (Test-Path $Jar)) {
return $false
}
Push-Location $ParserDir
try {
Write-Host "[antlr] java -jar `"$Jar`" -Dlanguage=Python3 -visitor -o `"$OutDir`" `"$grammarLeaf`""
& $javaCmd.Source -jar $Jar -Dlanguage=Python3 -visitor -o $OutDir $grammarLeaf
if ($LASTEXITCODE -ne 0) { throw "ANTLR jar invocation failed with exit code $LASTEXITCODE" }
} finally {
Pop-Location
}
return $true
}
if ($SkipAntlr) {
Write-Host "[antlr] Skipping grammar compilation (-SkipAntlr)."
} else {
$ok = Invoke-AntlrCompile -Grammar $GrammarFile -OutDir $GrammarOutDir -Jar $AntlrJar
if (-not $ok) {
Write-Warning ("ANTLR toolchain not found (no 'antlr4' on PATH and no usable jar). " +
"Skipping grammar compilation; '$GrammarOutDir' was left untouched. " +
"Install Java + antlr4-tools ('pip install antlr4-tools'), or set `$env:ANTLR_JAR " +
"to an antlr-*-complete.jar, then re-run.")
}
if ($AntlrLegacyJar -and -not (Test-Path $AntlrLegacyJar)) {
Write-Warning "AntlrLegacyJar '$AntlrLegacyJar' does not exist; falling back to the antlr4 launcher for the legacy target."
$AntlrLegacyJar = $null
}
$legacyOk = Invoke-AntlrCompile -Grammar $GrammarFile -OutDir $LegacyOutDir -Jar $AntlrLegacyJar -Version $AntlrLegacyVersion
if (-not $legacyOk) {
Write-Warning ("ANTLR toolchain not found for the legacy (ANTLR 4.9.x) target. " +
"Skipping legacy grammar compilation; '$LegacyOutDir' was left untouched. " +
"Install antlr4-tools, or set `$env:ANTLR_LEGACY_JAR to an antlr-4.9.x-complete.jar, then re-run.")
}
}
# ---------------------------------------------------------------------------
# Step 2: parse MathExpr.g4 for its built-in symbol table
# ---------------------------------------------------------------------------
function Get-GrammarSections {
param([Parameter(Mandatory)][string]$Text)
$funcStart = $Text.IndexOf('func0:')
$lexerMarker = '// LEXER RULES'
$lexerStart = $Text.IndexOf($lexerMarker)
if ($funcStart -lt 0 -or $lexerStart -lt 0 -or $lexerStart -le $funcStart) {
throw "Could not locate expected grammar sections ('func0:' / '$lexerMarker') - has MathExpr.g4's layout changed?"
}
[pscustomobject]@{
FuncSection = $Text.Substring($funcStart, $lexerStart - $funcStart)
LexerSection = $Text.Substring($lexerStart)
}
}
function Get-LexerTokens {
# Returns an ordered map of TOKEN_NAME -> raw string-literal alternatives,
# for every lexer rule that defines at least one word-like literal
# (i.e. skips punctuation/operator tokens and regex-only tokens like
# NUMBER/STRING/VARIABLE/WS/comments, which have no literal alternatives).
param([Parameter(Mandatory)][string]$LexerSection)
$tokenRegex = [regex]'(?ms)^[ \t]*([A-Z][A-Z0-9_]*)[ \t]*:[ \t]*(.*?);'
$literalRegex = [regex]"'([a-zA-Z_][a-zA-Z0-9_]*)'"
$tokens = [ordered]@{}
foreach ($m in $tokenRegex.Matches($LexerSection)) {
$name = $m.Groups[1].Value
$body = $m.Groups[2].Value
$literals = @($literalRegex.Matches($body) | ForEach-Object { $_.Groups[1].Value })
if ($literals.Count -gt 0) {
$tokens[$name] = $literals
}
}
return $tokens
}
function Get-FunctionTokenNames {
# A token counts as a "function" token when it is used as `TOKEN LPAREN`
# inside the func0..funcNoise rules (the atom productions for calls).
# This deliberately excludes structural keywords like IF/WHILE/FOR, which
# are also followed by LPAREN but in ifStmt/whileStmt/forStmt, outside
# this section.
param([Parameter(Mandatory)][string]$FuncSection)
$names = New-Object System.Collections.Generic.HashSet[string]
foreach ($m in [regex]::Matches($FuncSection, '\b([A-Z][A-Z0-9_]*)\s+LPAREN\b')) {
[void]$names.Add($m.Groups[1].Value)
}
return $names
}
function Get-UniqueLowerWords {
# De-duplicates literal alternatives that only differ by case (e.g.
# 'pi'/'PI', 'none'/'None'/'null'/'NULL') down to one lowercase spelling
# per distinct word, preserving first-seen order.
param([string[]]$Words)
$seen = New-Object System.Collections.Generic.HashSet[string]
$result = New-Object System.Collections.Generic.List[string]
foreach ($w in $Words) {
$lw = $w.ToLowerInvariant()
if ($seen.Add($lw)) { [void]$result.Add($lw) }
}
return $result
}
function Get-FunctionDocEntries {
# Pairs each `/** ... */` doc comment in the func section with the
# lexer token of the call it documents (the `TOKEN LPAREN` that follows
# the comment, skipping over an optional leading `|` alternation).
param([Parameter(Mandatory)][string]$FuncSection)
$pattern = [regex]'(?s)/\*\*(?<doc>.*?)\*/\s*\|?\s*(?<token>[A-Z][A-Z0-9_]*)\s+LPAREN'
$entries = New-Object System.Collections.Generic.List[object]
foreach ($m in $pattern.Matches($FuncSection)) {
$lines = @($m.Groups['doc'].Value -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
if ($lines.Count -eq 0) { continue }
$entries.Add([pscustomobject]@{
Token = $m.Groups['token'].Value
FirstLine = $lines[0]
})
}
return $entries
}
function Split-ArgSpec {
# Parses a comma-separated arg list like "x, [dims]" or "x, ..." into
# min/max argument counts and display names (brackets/ellipsis stripped).
param([string]$ArgsText)
if ([string]::IsNullOrWhiteSpace($ArgsText)) {
return [pscustomobject]@{ Min = 0; Max = 0; Variadic = $false; Display = @() }
}
$min = 0
$max = 0
$variadic = $false
$display = New-Object System.Collections.Generic.List[string]
foreach ($raw in ($ArgsText -split ',')) {
$p = $raw.Trim()
if ($p -eq '') { continue }
if ($p -eq '...') { $variadic = $true; [void]$display.Add('...'); continue }
$isOptional = $p.StartsWith('[') -and $p.EndsWith(']')
$clean = $p.Trim('[', ']').Trim()
[void]$display.Add($clean)
$max++
if (-not $isOptional) { $min++ }
}
[pscustomobject]@{ Min = $min; Max = $max; Variadic = $variadic; Display = $display.ToArray() }
}
function ConvertTo-FunctionSignature {
# Parses a doc-comment first line such as:
# "sum(x, [dims]) - computes the sum of elements of x, ..."
# "rgb_to_hsv(rgb, [degrees]) / rgb_to_hsv(r, g, b, [degrees]) - converts ..."
# into arg-count bounds, display arg names (from the first signature) and
# the trailing description text.
param([Parameter(Mandatory)][string]$Line)
$sigMatch = [regex]::Match($Line, '^[a-zA-Z_][a-zA-Z0-9_]*\((?<args>.*?)\)')
if (-not $sigMatch.Success) { return $null }
$rest = $Line.Substring($sigMatch.Index + $sigMatch.Length)
$spec1 = Split-ArgSpec -ArgsText $sigMatch.Groups['args'].Value
# Some functions document two call forms separated by " / "; fold the
# second form's bounds in so min/max cover both (e.g. rgb_to_hsv).
$altMatch = [regex]::Match($rest, '^\s*/\s*[a-zA-Z_][a-zA-Z0-9_]*\((?<args>.*?)\)')
$spec2 = $null
if ($altMatch.Success) {
$spec2 = Split-ArgSpec -ArgsText $altMatch.Groups['args'].Value
$rest = $rest.Substring($altMatch.Index + $altMatch.Length)
}
$descMatch = [regex]::Match($rest, '^\s*-\s*(?<desc>.*)$')
$desc = if ($descMatch.Success) { $descMatch.Groups['desc'].Value.Trim() } else { $rest.Trim() }
$min = $spec1.Min
$variadic = $spec1.Variadic
$max = $spec1.Max
if ($spec2) {
if ($spec2.Min -lt $min) { $min = $spec2.Min }
if ($spec2.Variadic) { $variadic = $true }
if ($spec2.Max -gt $max) { $max = $spec2.Max }
}
[pscustomobject]@{
MinArgs = $min
MaxArgs = if ($variadic) { $null } else { $max }
ArgNames = $spec1.Display
Description = $desc
}
}
Write-Host "[symbols] Parsing $GrammarFile"
$grammarText = Get-Content -Raw -LiteralPath $GrammarFile
$sections = Get-GrammarSections -Text $grammarText
$allTokens = Get-LexerTokens -LexerSection $sections.LexerSection
$functionTokenNames = Get-FunctionTokenNames -FuncSection $sections.FuncSection
$constantWords = @()
$keywordWords = New-Object System.Collections.Generic.List[string]
$functionWords = New-Object System.Collections.Generic.List[string]
$functionTokenToWords = [ordered]@{}
$tokenNames = @($allTokens.psbase.Keys)
foreach ($name in $tokenNames) {
$words = Get-UniqueLowerWords -Words $allTokens[$name]
if ($name -eq 'CONSTANT') {
$constantWords = $words
continue
}
if ($functionTokenNames.Contains($name)) {
$functionTokenToWords[$name] = $words
foreach ($w in $words) { [void]$functionWords.Add($w) }
} else {
foreach ($w in $words) { [void]$keywordWords.Add($w) }
}
}
$KEYWORDS = @($keywordWords | Sort-Object -Unique)
$CONSTANTS = @($constantWords | Sort-Object -Unique)
$FUNCTIONS = @($functionWords | Sort-Object -Unique)
$docEntries = Get-FunctionDocEntries -FuncSection $sections.FuncSection
$sigByToken = @{}
foreach ($e in $docEntries) {
$sig = ConvertTo-FunctionSignature -Line $e.FirstLine
if ($sig) { $sigByToken[$e.Token] = $sig }
}
$functionTokens = @($functionTokenToWords.psbase.Keys)
$undocumented = @($functionTokens | Where-Object { -not $sigByToken.ContainsKey($_) })
if ($undocumented.Count -gt 0) {
Write-Warning "No parsable doc comment found for function token(s): $($undocumented -join ', '). They will get placeholder metadata."
}
$FUNCTION_META = [ordered]@{}
foreach ($token in $functionTokens) {
$sig = $sigByToken[$token]
foreach ($alias in $functionTokenToWords[$token]) {
if ($sig) {
$snippet = if ($sig.ArgNames.Count -gt 0) { "$alias($($sig.ArgNames -join ', '))" } else { "$alias()" }
$FUNCTION_META[$alias] = [pscustomobject]@{
MinArgs = $sig.MinArgs
MaxArgs = $sig.MaxArgs
Snippet = $snippet
Description = $sig.Description
}
} else {
$FUNCTION_META[$alias] = [pscustomobject]@{
MinArgs = 0
MaxArgs = $null
Snippet = "$alias()"
Description = ''
}
}
}
}
# ---------------------------------------------------------------------------
# Step 3: emit the generated files
# ---------------------------------------------------------------------------
function Set-Utf8NoBom {
# Windows PowerShell 5.1's Set-Content has no utf8NoBOM encoding, so write
# the file directly via .NET to avoid a BOM in the generated .py/.js files.
param([string]$Path, [string]$Content)
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
}
function ConvertTo-JsString {
param([string]$Value)
$escaped = $Value.Replace('\', '\\').Replace('"', '\"')
return '"' + $escaped + '"'
}
function ConvertTo-PyString {
param([string]$Value)
$escaped = $Value.Replace('\', '\\').Replace("'", "\'")
return "'" + $escaped + "'"
}
function Write-JsSymbols {
param(
[string]$Path,
[string[]]$Keywords,
[string[]]$Constants,
[string[]]$Functions,
[System.Collections.Specialized.OrderedDictionary]$FunctionMeta
)
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('// Generated automatically by compile.ps1. Do not edit.')
[void]$sb.AppendLine()
[void]$sb.AppendLine('export const KEYWORDS = new Set([')
[void]$sb.AppendLine(($Keywords | ForEach-Object { ConvertTo-JsString $_ }) -join ', ')
[void]$sb.AppendLine(']);')
[void]$sb.AppendLine()
[void]$sb.AppendLine('export const CONSTANTS = new Set([')
[void]$sb.AppendLine(($Constants | ForEach-Object { ConvertTo-JsString $_ }) -join ', ')
[void]$sb.AppendLine(']);')
[void]$sb.AppendLine()
[void]$sb.AppendLine('export const FUNCTIONS = new Set([')
[void]$sb.AppendLine(($Functions | ForEach-Object { ConvertTo-JsString $_ }) -join ', ')
[void]$sb.AppendLine(']);')
[void]$sb.AppendLine()
[void]$sb.AppendLine('export const FUNCTION_META = {')
foreach ($name in @($FunctionMeta.psbase.Keys)) {
$meta = $FunctionMeta[$name]
$maxArgsJs = if ($null -eq $meta.MaxArgs) { 'null' } else { $meta.MaxArgs }
[void]$sb.AppendLine(" $(ConvertTo-JsString $name): { minArgs: $($meta.MinArgs), maxArgs: $maxArgsJs, snippet: $(ConvertTo-JsString $meta.Snippet), description: $(ConvertTo-JsString $meta.Description) },")
}
[void]$sb.AppendLine('};')
Set-Utf8NoBom -Path $Path -Content $sb.ToString()
}
function Write-PySymbols {
param(
[string]$Path,
[string[]]$Keywords,
[string[]]$Constants,
[string[]]$Functions,
[System.Collections.Specialized.OrderedDictionary]$FunctionMeta
)
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('# Generated automatically by compile.ps1. Do not edit.')
[void]$sb.AppendLine('INBUILT_KEYWORDS = {')
[void]$sb.AppendLine(($Keywords | ForEach-Object { ConvertTo-PyString $_ }) -join ', ')
[void]$sb.AppendLine('}')
[void]$sb.AppendLine('INBUILT_CONSTANTS = {')
[void]$sb.AppendLine(($Constants | ForEach-Object { ConvertTo-PyString $_ }) -join ', ')
[void]$sb.AppendLine('}')
[void]$sb.AppendLine('INBUILT_FUNCTIONS = {')
[void]$sb.AppendLine(($Functions | ForEach-Object { ConvertTo-PyString $_ }) -join ', ')
[void]$sb.AppendLine('}')
[void]$sb.AppendLine()
[void]$sb.AppendLine('INBUILT_FUNCTION_META = {')
foreach ($name in @($FunctionMeta.psbase.Keys)) {
$meta = $FunctionMeta[$name]
$maxArgsPy = if ($null -eq $meta.MaxArgs) { 'None' } else { $meta.MaxArgs }
[void]$sb.AppendLine(" $(ConvertTo-PyString $name): {'min_args': $($meta.MinArgs), 'max_args': $maxArgsPy, 'snippet': $(ConvertTo-PyString $meta.Snippet), 'description': $(ConvertTo-PyString $meta.Description)},")
}
[void]$sb.AppendLine('}')
Set-Utf8NoBom -Path $Path -Content $sb.ToString()
}
Write-Host "[symbols] Writing $PySymbolsPath"
Write-PySymbols -Path $PySymbolsPath -Keywords $KEYWORDS -Constants $CONSTANTS -Functions $FUNCTIONS -FunctionMeta $FUNCTION_META
Write-Host "[symbols] Writing $JsSymbolsPath"
Write-JsSymbols -Path $JsSymbolsPath -Keywords $KEYWORDS -Constants $CONSTANTS -Functions $FUNCTIONS -FunctionMeta $FUNCTION_META
Write-Host "[symbols] $($FUNCTIONS.Count) functions, $($KEYWORDS.Count) keywords, $($CONSTANTS.Count) constants."
Write-Host 'Done.'
File diff suppressed because one or more lines are too long
+412
View File
@@ -0,0 +1,412 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
LINSPACE=53
LOGSPACE=54
TOPK=55
BOTK=56
PINV=57
SUM=58
MEAN=59
STD=60
VAR=61
QUARTILE=62
PERCENTILE=63
QUANTILE=64
DOT=65
MOMENT=66
ERF=67
ERFINV=68
ANY=69
ALL=70
EDGE=71
GAUSSIAN=72
MEDIAN=73
MODE=74
CUMSUM=75
CUMPROD=76
TOPK_IND=77
BOTK_IND=78
CUBIC_EASE=79
ELASTIC_EASE=80
SINE_EASE=81
SMOOTHERSTEP=82
DIST=83
REMAP=84
COSSIM=85
COUNT=86
KEYS=87
ENABLE_TRACKING=88
DISABLE_TRACKING=89
RESET_TRACKING=90
GET_CHANGED_KEYS=91
GET_ADDED_KEYS=92
GET_REMOVED_KEYS=93
GET_UNCHANGED_KEYS=94
REPEAT=95
FLATTEN=96
APPEND=97
GET_VALUE=98
FLOW_APPLY=99
BATCH_SHUFFLE=100
MOTION_MASK=101
FLOW_TO_IMAGE=102
FLOW_MAG=103
FLOW_ANG=104
WHERE=105
HISTOGRAM=106
OVERLAY=107
PAD=108
CROSS=109
MATMUL=110
RIFE=111
BNOT=112
BITCOUNT=113
SHAPE=114
BAND=115
XOR=116
BOR=117
TENSOR=118
ADD_KEY=119
REMOVE_KEY=120
PUSH=121
POP=122
CLEAR=123
HAS=124
GET=125
CONCAT=126
INT=127
FLOAT=128
UPPER=129
LOWER=130
TRIM=131
SPLIT=132
JOIN=133
SUBSTRING=134
FIND=135
REPLACE=136
DILATE=137
ERODE=138
MORPH_OPEN=139
MORPH_CLOSE=140
RGB_TO_OKLAB=141
RGB_TO_CIELAB=142
OKLAB_TO_RGB=143
CIELAB_TO_RGB=144
RGB_TO_HSV=145
HSV_TO_RGB=146
INT_TO_RGB=147
RGB_TO_INT=148
INTERPOLATE_LINEAR=149
INTERPOLATE_AREA=150
INTERPOLATE_NEAREST=151
TEXT_IMAGE=152
AS_NESTED=153
SVD=154
DIAG=155
IF=156
ELSE=157
WHILE=158
FOR=159
IN=160
BREAK=161
CONTINUE=162
RETURN=163
TIMESTAMP=164
SORT=165
ARGSORT=166
ARGMIN=167
ARGMAX=168
SOFTMAX=169
SOFTMIN=170
UNIQUE=171
FLIP=172
STARTSWITH=173
ENDSWITH=174
SQUEEZE=175
UNSQUEEZE=176
ROLL=177
COV=178
CORR=179
ENTROPY=180
CROP=181
NONE=182
COORDS=183
NOISE=184
RAND=185
CAUCHY=186
EXPONENTIAL=187
LOGNORMAL=188
BERNOULLI=189
POISSON=190
GAMMADIST=191
BETADIST=192
LAPLACEDIST=193
GUMBELDIST=194
WEIBULLDIST=195
CHI2DIST=196
STUDENTTDIST=197
PERLIN=198
CELLULAR=199
PLASMA=200
RIDGED=201
DOMAIN_WARP=202
PLUS=203
MINUS=204
MULT=205
DIV=206
MOD=207
POW=208
LSHIFT=209
RSHIFT=210
GE=211
GT=212
LE=213
LT=214
EQ=215
EQUEALS=216
PLUS_EQ=217
MINUS_EQ=218
MULT_EQ=219
DIV_EQ=220
MOD_EQ=221
NE=222
PIPE=223
LPAREN=224
RPAREN=225
COMMA=226
SEMICOLON=227
ARROW=228
LBRACKET=229
RBRACKET=230
QUESTION=231
COLON=232
LBRACE=233
RBRACE=234
NUMBER=235
CONSTANT=236
STRING=237
VARIABLE=238
SL_COMMENT=239
ML_COMMENT=240
WS=241
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'linspace'=53
'logspace'=54
'topk'=55
'botk'=56
'pinv'=57
'sum'=58
'mean'=59
'std'=60
'var'=61
'quantile'=64
'dot'=65
'moment'=66
'erf'=67
'erfinv'=68
'any'=69
'all'=70
'edge'=71
'median'=73
'mode'=74
'cumsum'=75
'cumprod'=76
'smootherstep'=82
'remap'=84
'keys'=87
'repeat'=95
'flatten'=96
'append'=97
'get_value'=98
'flow_apply'=99
'motion_mask'=101
'flow_to_image'=102
'where'=105
'overlay'=107
'pad'=108
'cross'=109
'matmul'=110
'rife'=111
'shape'=114
'tensor'=118
'add_key'=119
'stack_push'=121
'stack_pop'=122
'stack_clear'=123
'stack_has'=124
'stack_get'=125
'int'=127
'float'=128
'upper'=129
'lower'=130
'trim'=131
'split'=132
'join'=133
'find'=135
'replace'=136
'dilate'=137
'erode'=138
'morph_open'=139
'morph_close'=140
'rgb_to_oklab'=141
'rgb_to_cielab'=142
'oklab_to_rgb'=143
'cielab_to_rgb'=144
'rgb_to_hsv'=145
'hsv_to_rgb'=146
'int_to_rgb'=147
'rgb_to_int'=148
'interpolate_linear'=149
'interpolate_area'=150
'text_image'=152
'if'=156
'else'=157
'while'=158
'for'=159
'in'=160
'break'=161
'continue'=162
'return'=163
'sort'=165
'argsort'=166
'argmin'=167
'argmax'=168
'softmax'=169
'softmin'=170
'unique'=171
'flip'=172
'startswith'=173
'endswith'=174
'squeeze'=175
'unsqueeze'=176
'roll'=177
'cov'=178
'entropy'=180
'crop'=181
'+'=203
'-'=204
'*'=205
'/'=206
'%'=207
'^'=208
'<<'=209
'>>'=210
'>='=211
'>'=212
'<='=213
'<'=214
'=='=215
'='=216
'+='=217
'-='=218
'*='=219
'/='=220
'%='=221
'!='=222
'|'=223
'('=224
')'=225
','=226
';'=227
'->'=228
'['=229
']'=230
'?'=231
':'=232
'{'=233
'}'=234
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,412 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
LINSPACE=53
LOGSPACE=54
TOPK=55
BOTK=56
PINV=57
SUM=58
MEAN=59
STD=60
VAR=61
QUARTILE=62
PERCENTILE=63
QUANTILE=64
DOT=65
MOMENT=66
ERF=67
ERFINV=68
ANY=69
ALL=70
EDGE=71
GAUSSIAN=72
MEDIAN=73
MODE=74
CUMSUM=75
CUMPROD=76
TOPK_IND=77
BOTK_IND=78
CUBIC_EASE=79
ELASTIC_EASE=80
SINE_EASE=81
SMOOTHERSTEP=82
DIST=83
REMAP=84
COSSIM=85
COUNT=86
KEYS=87
ENABLE_TRACKING=88
DISABLE_TRACKING=89
RESET_TRACKING=90
GET_CHANGED_KEYS=91
GET_ADDED_KEYS=92
GET_REMOVED_KEYS=93
GET_UNCHANGED_KEYS=94
REPEAT=95
FLATTEN=96
APPEND=97
GET_VALUE=98
FLOW_APPLY=99
BATCH_SHUFFLE=100
MOTION_MASK=101
FLOW_TO_IMAGE=102
FLOW_MAG=103
FLOW_ANG=104
WHERE=105
HISTOGRAM=106
OVERLAY=107
PAD=108
CROSS=109
MATMUL=110
RIFE=111
BNOT=112
BITCOUNT=113
SHAPE=114
BAND=115
XOR=116
BOR=117
TENSOR=118
ADD_KEY=119
REMOVE_KEY=120
PUSH=121
POP=122
CLEAR=123
HAS=124
GET=125
CONCAT=126
INT=127
FLOAT=128
UPPER=129
LOWER=130
TRIM=131
SPLIT=132
JOIN=133
SUBSTRING=134
FIND=135
REPLACE=136
DILATE=137
ERODE=138
MORPH_OPEN=139
MORPH_CLOSE=140
RGB_TO_OKLAB=141
RGB_TO_CIELAB=142
OKLAB_TO_RGB=143
CIELAB_TO_RGB=144
RGB_TO_HSV=145
HSV_TO_RGB=146
INT_TO_RGB=147
RGB_TO_INT=148
INTERPOLATE_LINEAR=149
INTERPOLATE_AREA=150
INTERPOLATE_NEAREST=151
TEXT_IMAGE=152
AS_NESTED=153
SVD=154
DIAG=155
IF=156
ELSE=157
WHILE=158
FOR=159
IN=160
BREAK=161
CONTINUE=162
RETURN=163
TIMESTAMP=164
SORT=165
ARGSORT=166
ARGMIN=167
ARGMAX=168
SOFTMAX=169
SOFTMIN=170
UNIQUE=171
FLIP=172
STARTSWITH=173
ENDSWITH=174
SQUEEZE=175
UNSQUEEZE=176
ROLL=177
COV=178
CORR=179
ENTROPY=180
CROP=181
NONE=182
COORDS=183
NOISE=184
RAND=185
CAUCHY=186
EXPONENTIAL=187
LOGNORMAL=188
BERNOULLI=189
POISSON=190
GAMMADIST=191
BETADIST=192
LAPLACEDIST=193
GUMBELDIST=194
WEIBULLDIST=195
CHI2DIST=196
STUDENTTDIST=197
PERLIN=198
CELLULAR=199
PLASMA=200
RIDGED=201
DOMAIN_WARP=202
PLUS=203
MINUS=204
MULT=205
DIV=206
MOD=207
POW=208
LSHIFT=209
RSHIFT=210
GE=211
GT=212
LE=213
LT=214
EQ=215
EQUEALS=216
PLUS_EQ=217
MINUS_EQ=218
MULT_EQ=219
DIV_EQ=220
MOD_EQ=221
NE=222
PIPE=223
LPAREN=224
RPAREN=225
COMMA=226
SEMICOLON=227
ARROW=228
LBRACKET=229
RBRACKET=230
QUESTION=231
COLON=232
LBRACE=233
RBRACE=234
NUMBER=235
CONSTANT=236
STRING=237
VARIABLE=238
SL_COMMENT=239
ML_COMMENT=240
WS=241
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'linspace'=53
'logspace'=54
'topk'=55
'botk'=56
'pinv'=57
'sum'=58
'mean'=59
'std'=60
'var'=61
'quantile'=64
'dot'=65
'moment'=66
'erf'=67
'erfinv'=68
'any'=69
'all'=70
'edge'=71
'median'=73
'mode'=74
'cumsum'=75
'cumprod'=76
'smootherstep'=82
'remap'=84
'keys'=87
'repeat'=95
'flatten'=96
'append'=97
'get_value'=98
'flow_apply'=99
'motion_mask'=101
'flow_to_image'=102
'where'=105
'overlay'=107
'pad'=108
'cross'=109
'matmul'=110
'rife'=111
'shape'=114
'tensor'=118
'add_key'=119
'stack_push'=121
'stack_pop'=122
'stack_clear'=123
'stack_has'=124
'stack_get'=125
'int'=127
'float'=128
'upper'=129
'lower'=130
'trim'=131
'split'=132
'join'=133
'find'=135
'replace'=136
'dilate'=137
'erode'=138
'morph_open'=139
'morph_close'=140
'rgb_to_oklab'=141
'rgb_to_cielab'=142
'oklab_to_rgb'=143
'cielab_to_rgb'=144
'rgb_to_hsv'=145
'hsv_to_rgb'=146
'int_to_rgb'=147
'rgb_to_int'=148
'interpolate_linear'=149
'interpolate_area'=150
'text_image'=152
'if'=156
'else'=157
'while'=158
'for'=159
'in'=160
'break'=161
'continue'=162
'return'=163
'sort'=165
'argsort'=166
'argmin'=167
'argmax'=168
'softmax'=169
'softmin'=170
'unique'=171
'flip'=172
'startswith'=173
'endswith'=174
'squeeze'=175
'unsqueeze'=176
'roll'=177
'cov'=178
'entropy'=180
'crop'=181
'+'=203
'-'=204
'*'=205
'/'=206
'%'=207
'^'=208
'<<'=209
'>>'=210
'>='=211
'>'=212
'<='=213
'<'=214
'=='=215
'='=216
'+='=217
'-='=218
'*='=219
'/='=220
'%='=221
'!='=222
'|'=223
'('=224
')'=225
','=226
';'=227
'->'=228
'['=229
']'=230
'?'=231
':'=232
'{'=233
'}'=234
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+278
View File
@@ -0,0 +1,278 @@
# Generated automatically by compile.ps1. Do not edit.
INBUILT_KEYWORDS = {
'break', 'continue', 'else', 'for', 'if', 'in', 'none', 'null', 'return', 'while'
}
INBUILT_CONSTANTS = {
'e', 'pi'
}
INBUILT_FUNCTIONS = {
'abs', 'acos', 'acosh', 'add_key', 'added_keys', 'all', 'angle', 'any', 'append', 'argmax', 'argmin', 'argsort', 'as_nested', 'as_nested_tensor', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'band', 'batch_shuffle', 'bitcount', 'bitwise_and', 'bitwise_not', 'bitwise_or', 'bitwise_xor', 'blur', 'bnot', 'bor', 'botk', 'botk_ind', 'botk_indices', 'bxor', 'cat', 'ceil', 'cellular', 'cellular_noise', 'cielab_to_rgb', 'clamp', 'cnt', 'concat', 'concatenate', 'conv', 'convolution', 'coordinates', 'coords', 'corr', 'correlation', 'cos', 'cosh', 'cosine_similarity', 'cossim', 'count', 'cov', 'crop', 'cross', 'cubic', 'cubic_ease', 'cumprod', 'cumsum', 'diag', 'diagonal_matrix', 'dilate', 'disable_tracking', 'dist', 'distance', 'domain_warp', 'domain_warp_noise', 'dot', 'edge', 'elastic', 'elastic_ease', 'enable_tracking', 'endswith', 'entropy', 'erf', 'erfinv', 'erode', 'exp', 'ezconv', 'ezconvolution', 'fft', 'find', 'flatten', 'flip', 'float', 'floor', 'flow_ang', 'flow_angle', 'flow_apply', 'flow_mag', 'flow_magnitude', 'flow_to_image', 'fract', 'gamma', 'gaussian', 'gelu', 'get_added_keys', 'get_changed_keys', 'get_removed_keys', 'get_unchanged_keys', 'get_value', 'hist', 'histogram', 'hsv_to_rgb', 'changed_keys', 'ifft', 'int', 'int_to_rgb', 'interpolate_area', 'interpolate_linear', 'interpolate_nearest', 'interpolate_nearest_exact', 'join', 'keys', 'length', 'lerp', 'linspace', 'ln', 'log', 'logspace', 'lower', 'map', 'matmul', 'mean', 'median', 'mode', 'moment', 'morph_close', 'morph_open', 'motion_mask', 'nan_to_num', 'noise', 'now', 'nvl', 'oklab_to_rgb', 'overlay', 'pad', 'percentile', 'perlin', 'perlin_noise', 'perm', 'permute', 'pinv', 'plasma', 'plasma_noise', 'popcnt', 'popcount', 'pow', 'prcnt', 'print', 'print_shape', 'pshp', 'quantile', 'quartil', 'quartile', 'rand', 'randb', 'randbeta', 'randc', 'rande', 'randg', 'randgumbel', 'randchi2', 'randl', 'randln', 'randn', 'random_bernoulli', 'random_beta', 'random_cauchy', 'random_exponential', 'random_gamma', 'random_gumbel', 'random_chi2', 'random_laplace', 'random_log_normal', 'random_normal', 'random_poisson', 'random_studentt', 'random_uniform', 'random_weibull', 'randp', 'randt', 'randu', 'randw', 'range', 'relu', 'remap', 'remove_key', 'removed_keys', 'repeat', 'replace', 'reset_tracking', 'reshape', 'rgb_to_cielab', 'rgb_to_hsv', 'rgb_to_int', 'rgb_to_oklab', 'ridged', 'ridged_noise', 'rife', 'rm_key', 'roll', 'round', 'rshp', 'select', 'shape', 'shuffle', 'sigm', 'sign', 'sin', 'sine', 'sine_ease', 'singular_value_decomposition', 'sinh', 'smax', 'smin', 'smootherstep', 'smoothstep', 'snorm', 'softmax', 'softmin', 'softplus', 'sort', 'split', 'sqrt', 'squeeze', 'stack_clear', 'stack_get', 'stack_has', 'stack_pop', 'stack_push', 'startswith', 'std', 'step', 'substr', 'substring', 'sum', 'svd', 'swap', 'tan', 'tanh', 'tensor', 'text_image', 'timestamp', 'tmax', 'tmin', 'tnorm', 'topk', 'topk_ind', 'topk_indices', 'track_off', 'track_on', 'track_reset', 'trim', 'turbulence', 'unchanged_keys', 'unique', 'unsqueeze', 'upper', 'var', 'voronoi', 'voronoi_noise', 'where', 'worley'
}
INBUILT_FUNCTION_META = {
'sin': {'min_args': 1, 'max_args': 1, 'snippet': 'sin(x)', 'description': 'applies sinus function to value or each element of value'},
'cos': {'min_args': 1, 'max_args': 1, 'snippet': 'cos(x)', 'description': 'applies cosinus function to value or each element of value'},
'tan': {'min_args': 1, 'max_args': 1, 'snippet': 'tan(x)', 'description': 'applies tangents function to value or each element of value'},
'asin': {'min_args': 1, 'max_args': 1, 'snippet': 'asin(x)', 'description': 'applies arcus sinus function to value or each element of value'},
'acos': {'min_args': 1, 'max_args': 1, 'snippet': 'acos(x)', 'description': 'applies arcus cosinus function to value or each element of value'},
'atan': {'min_args': 1, 'max_args': 1, 'snippet': 'atan(x)', 'description': 'applies arcus tangents function to value or each element of value'},
'atan2': {'min_args': 2, 'max_args': 2, 'snippet': 'atan2(y, x)', 'description': 'computes the arc tangent of y/x, using the signs of both arguments to determine the quadrant'},
'sinh': {'min_args': 1, 'max_args': 1, 'snippet': 'sinh(x)', 'description': 'applies hyperbolic sinus function to value or each element of value'},
'cosh': {'min_args': 1, 'max_args': 1, 'snippet': 'cosh(x)', 'description': 'applies hyperbolic cosinus function to value or each element of value'},
'tanh': {'min_args': 1, 'max_args': 1, 'snippet': 'tanh(x)', 'description': 'applies hyperbolic tangents function to value or each element of value'},
'asinh': {'min_args': 1, 'max_args': 1, 'snippet': 'asinh(x)', 'description': 'applies hyperbolic arcus sinus function to value or each element of value'},
'acosh': {'min_args': 1, 'max_args': 1, 'snippet': 'acosh(x)', 'description': 'applies hyperbolic arcus cosinus function to value or each element of value'},
'atanh': {'min_args': 1, 'max_args': 1, 'snippet': 'atanh(x)', 'description': 'applies hyperbolic arcus tangents function to value or each element of value'},
'abs': {'min_args': 1, 'max_args': 1, 'snippet': 'abs(x)', 'description': 'applies per element absolute value function. Same as |x| for numbers.'},
'sqrt': {'min_args': 1, 'max_args': 1, 'snippet': 'sqrt(x)', 'description': 'applies sqere root per element. Negative numbers return NaN (not a number)'},
'ln': {'min_args': 1, 'max_args': 1, 'snippet': 'ln(x)', 'description': 'applies natural logarithm to value (per element). Negative numbers return NaN (not a number)'},
'log': {'min_args': 1, 'max_args': 1, 'snippet': 'log(x)', 'description': 'applies base 10 logarithm to value (per element). Negative numbers return NaN (not a number)'},
'exp': {'min_args': 1, 'max_args': 1, 'snippet': 'exp(x)', 'description': 'applies e^x to value (per element)'},
'smin': {'min_args': 1, 'max_args': None, 'snippet': 'smin(x, ...)', 'description': 'computes the minimum of inputs or elements. Tensors must have the same size or be broadcastable to same size. When called as smin(x, dims) with x a tensor/list, reduces x along dims (a single dim or list of dims) instead.'},
'smax': {'min_args': 1, 'max_args': None, 'snippet': 'smax(x, ...)', 'description': 'computes the maximum of inputs or elements. Tensors must have the same size or be broadcastable to same size. When called as smax(x, dims) with x a tensor/list, reduces x along dims (a single dim or list of dims) instead.'},
'tmin': {'min_args': 2, 'max_args': 2, 'snippet': 'tmin(x, y)', 'description': 'elementwise minimum of tensor or list. min(x,y) when inputs are floats'},
'tmax': {'min_args': 2, 'max_args': 2, 'snippet': 'tmax(x, y)', 'description': 'elementwise maximum of tensor or list. max(x,y) when inputs are floats'},
'tnorm': {'min_args': 1, 'max_args': 2, 'snippet': 'tnorm(x, dim)', 'description': 'normalises tensor or list by multiplication such that sum(x^2)==1.0 for each slice. Defaults to the last dimension; dim selects a different slicing dimension.'},
'snorm': {'min_args': 1, 'max_args': 2, 'snippet': 'snorm(x, dim)', 'description': 'frobenius norm of tensor. Along dimension if dimension specified. Dimension can be a list. Defaults to no dimension.'},
'floor': {'min_args': 1, 'max_args': 1, 'snippet': 'floor(x)', 'description': 'returns the largest integer less than or equal to x'},
'ceil': {'min_args': 1, 'max_args': 1, 'snippet': 'ceil(x)', 'description': 'returns the smallest integer greater than or equal to x'},
'round': {'min_args': 1, 'max_args': 1, 'snippet': 'round(x)', 'description': 'rounds x to the nearest integer'},
'gamma': {'min_args': 1, 'max_args': 1, 'snippet': 'gamma(x)', 'description': 'computes the gamma function (per element)'},
'pow': {'min_args': 2, 'max_args': 2, 'snippet': 'pow(x, y)', 'description': 'computes x raised to the power of y'},
'sigm': {'min_args': 1, 'max_args': 1, 'snippet': 'sigm(x)', 'description': 'applies the sigmoid function: 1 / (1 + exp(-x))'},
'clamp': {'min_args': 3, 'max_args': 3, 'snippet': 'clamp(x, min, max)', 'description': 'clamps x between min and max per element'},
'fft': {'min_args': 1, 'max_args': 1, 'snippet': 'fft(x)', 'description': 'computes fast Fourier transform of x'},
'ifft': {'min_args': 1, 'max_args': 2, 'snippet': 'ifft(x, shape)', 'description': 'inverse fast Fourier transform of x. shape (defaults to the node\'s tensor shape) sizes the K/Kx/Ky/.../frequency variables made available while evaluating x, it does not resize x itself.'},
'angle': {'min_args': 1, 'max_args': 1, 'snippet': 'angle(x)', 'description': 'returns the angle of a complex number or vector'},
'print': {'min_args': 1, 'max_args': 1, 'snippet': 'print(x)', 'description': 'prints the value of x to the console while passing output unchanged'},
'print_shape': {'min_args': 1, 'max_args': 1, 'snippet': 'print_shape(x)', 'description': 'prints the shape of the tensor x'},
'pshp': {'min_args': 1, 'max_args': 1, 'snippet': 'pshp(x)', 'description': 'prints the shape of the tensor x'},
'nvl': {'min_args': 4, 'max_args': 4, 'snippet': 'nvl(x, nan, pos_inf, neg_inf)', 'description': 'null value replacement - inspired by NVL'},
'nan_to_num': {'min_args': 4, 'max_args': 4, 'snippet': 'nan_to_num(x, nan, pos_inf, neg_inf)', 'description': 'null value replacement - inspired by NVL'},
'lerp': {'min_args': 3, 'max_args': 3, 'snippet': 'lerp(a, b, t)', 'description': 'linear interpolation between a and b by t per element. t can be float or tensor.'},
'step': {'min_args': 2, 'max_args': 2, 'snippet': 'step(x, edge)', 'description': 'returns 0 if x < edge, else 1'},
'smoothstep': {'min_args': 3, 'max_args': 3, 'snippet': 'smoothstep(x, edge0, edge1)', 'description': 'smooth interpolation between edge0 and edge1'},
'fract': {'min_args': 1, 'max_args': 1, 'snippet': 'fract(x)', 'description': 'returns the fractional part of x: x - floor(x)'},
'relu': {'min_args': 1, 'max_args': 1, 'snippet': 'relu(x)', 'description': 'applies rectified linear unit function: max(0, x)'},
'softplus': {'min_args': 1, 'max_args': 1, 'snippet': 'softplus(x)', 'description': 'applies the softplus function: ln(1 + exp(x))'},
'gelu': {'min_args': 1, 'max_args': 1, 'snippet': 'gelu(x)', 'description': 'applies the Gaussian Error Linear Unit (GELU) activation function'},
'sign': {'min_args': 1, 'max_args': 1, 'snippet': 'sign(x)', 'description': 'returns the sign of x: -1, 0, or 1'},
'map': {'min_args': 2, 'max_args': 4, 'snippet': 'map(tensor, coord1, coord2, coord3)', 'description': 'samples/remaps input tensor using coordinates'},
'ezconvolution': {'min_args': 2, 'max_args': 3, 'snippet': 'ezconvolution(tensor, kernel_sizes..., kernel)', 'description': 'applies convolution with kernel. Attempts to correctly convolve over size dimensions.'},
'ezconv': {'min_args': 2, 'max_args': 3, 'snippet': 'ezconv(tensor, kernel_sizes..., kernel)', 'description': 'applies convolution with kernel. Attempts to correctly convolve over size dimensions.'},
'convolution': {'min_args': 2, 'max_args': 3, 'snippet': 'convolution(tensor, kernel_sizes..., kernel)', 'description': 'applies convolution with kernel. Expects [batch,channel, ...]'},
'conv': {'min_args': 2, 'max_args': 3, 'snippet': 'conv(tensor, kernel_sizes..., kernel)', 'description': 'applies convolution with kernel. Expects [batch,channel, ...]'},
'swap': {'min_args': 4, 'max_args': 4, 'snippet': 'swap(x, dim, idx1, idx2)', 'description': 'swaps elements along dim between idx1 and idx2'},
'permute': {'min_args': 2, 'max_args': 2, 'snippet': 'permute(x, dims)', 'description': 'permutes the dimensions of tensor x according to dims'},
'perm': {'min_args': 2, 'max_args': 2, 'snippet': 'perm(x, dims)', 'description': 'permutes the dimensions of tensor x according to dims'},
'reshape': {'min_args': 2, 'max_args': 2, 'snippet': 'reshape(x, shape)', 'description': 'reshapes tensor x to the specified shape'},
'rshp': {'min_args': 2, 'max_args': 2, 'snippet': 'rshp(x, shape)', 'description': 'reshapes tensor x to the specified shape'},
'range': {'min_args': 3, 'max_args': 3, 'snippet': 'range(start, stop, step)', 'description': 'creates a range of values between start (inclusive) and stop (exclusive) using step. Use linspace if you want value count.'},
'linspace': {'min_args': 3, 'max_args': 3, 'snippet': 'linspace(start, stop, num)', 'description': 'creates num linearly spaced values from start to stop (inclusive). Use range if you want to control the step between values.'},
'logspace': {'min_args': 4, 'max_args': 4, 'snippet': 'logspace(start, stop, num, base)', 'description': 'creates logarithmically spaced values'},
'topk': {'min_args': 2, 'max_args': 2, 'snippet': 'topk(x, k)', 'description': 'returns the k largest elements of x. For tensors it keeps them in place.'},
'botk': {'min_args': 2, 'max_args': 2, 'snippet': 'botk(x, k)', 'description': 'returns the k smallest elements of x. For tensors it keeps them in place.'},
'pinv': {'min_args': 1, 'max_args': 1, 'snippet': 'pinv(x)', 'description': 'computes the permutative inverse of a list x (list must have uniqe elements) -- TODO: CHECK --'},
'sum': {'min_args': 1, 'max_args': 2, 'snippet': 'sum(x, dims)', 'description': 'computes the sum of elements of x, optionally along one or more dimensions'},
'mean': {'min_args': 1, 'max_args': 1, 'snippet': 'mean(x)', 'description': 'computes the arithmetic mean of elements of x'},
'std': {'min_args': 1, 'max_args': 1, 'snippet': 'std(x)', 'description': 'computes the standard deviation of elements of x'},
'var': {'min_args': 1, 'max_args': 1, 'snippet': 'var(x)', 'description': 'computes the variance of elements of x'},
'quartile': {'min_args': 2, 'max_args': 2, 'snippet': 'quartile(x, q)', 'description': 'computes the q-th quartile of x'},
'quartil': {'min_args': 2, 'max_args': 2, 'snippet': 'quartil(x, q)', 'description': 'computes the q-th quartile of x'},
'percentile': {'min_args': 2, 'max_args': 2, 'snippet': 'percentile(x, p)', 'description': 'computes the p-th percentile of x'},
'prcnt': {'min_args': 2, 'max_args': 2, 'snippet': 'prcnt(x, p)', 'description': 'computes the p-th percentile of x'},
'quantile': {'min_args': 2, 'max_args': 2, 'snippet': 'quantile(x, q)', 'description': 'computes the q-th quantile of x'},
'dot': {'min_args': 2, 'max_args': 2, 'snippet': 'dot(x, y)', 'description': 'computes the dot product of x and y after flattening both to 1D. Use cross(x, y) if you need a per-vector product with the last dimension kept.'},
'moment': {'min_args': 3, 'max_args': 3, 'snippet': 'moment(x, a, k)', 'description': 'computes the k-th moment of x around a'},
'erf': {'min_args': 1, 'max_args': 1, 'snippet': 'erf(x)', 'description': 'computes the error function (torch.erf)'},
'erfinv': {'min_args': 1, 'max_args': 1, 'snippet': 'erfinv(x)', 'description': 'computes the inverse error function (torch.erfinv)'},
'any': {'min_args': 1, 'max_args': 1, 'snippet': 'any(x)', 'description': 'returns 1 if any element of x is non-zero otherwise 0'},
'all': {'min_args': 1, 'max_args': 1, 'snippet': 'all(x)', 'description': 'returns 1 if all elements of x are non-zero otherwise 0'},
'edge': {'min_args': 1, 'max_args': 2, 'snippet': 'edge(x, matrix_size)', 'description': 'detects edges in x. Matrix size denotes the size of edge detection matrix.'},
'blur': {'min_args': 2, 'max_args': 3, 'snippet': 'blur(x, sigma, reshape)', 'description': 'applies a Gaussian blur to x with specified sigma. if reshape has value of 1.0, then it tries orienting the input such that channel is in the direction of filter. Otherwise it expect color dimension to be the last.'},
'gaussian': {'min_args': 2, 'max_args': 3, 'snippet': 'gaussian(x, sigma, reshape)', 'description': 'applies a Gaussian blur to x with specified sigma. if reshape has value of 1.0, then it tries orienting the input such that channel is in the direction of filter. Otherwise it expect color dimension to be the last.'},
'median': {'min_args': 1, 'max_args': 1, 'snippet': 'median(x)', 'description': 'computes the median of elements of x'},
'mode': {'min_args': 1, 'max_args': 1, 'snippet': 'mode(x)', 'description': 'computes the mode of elements of x'},
'cumsum': {'min_args': 1, 'max_args': 2, 'snippet': 'cumsum(x, dim)', 'description': 'computes the cumulative sum, defaulting to the first dimension.'},
'cumprod': {'min_args': 1, 'max_args': 1, 'snippet': 'cumprod(x)', 'description': 'computes the cumulative product over first dimension.'},
'topk_ind': {'min_args': 2, 'max_args': 2, 'snippet': 'topk_ind(x, k)', 'description': 'returns the indices of the k largest elements of x'},
'topk_indices': {'min_args': 2, 'max_args': 2, 'snippet': 'topk_indices(x, k)', 'description': 'returns the indices of the k largest elements of x'},
'botk_ind': {'min_args': 2, 'max_args': 2, 'snippet': 'botk_ind(x, k)', 'description': 'returns the indices of the k smallest elements of x'},
'botk_indices': {'min_args': 2, 'max_args': 2, 'snippet': 'botk_indices(x, k)', 'description': 'returns the indices of the k smallest elements of x'},
'cubic_ease': {'min_args': 3, 'max_args': 3, 'snippet': 'cubic_ease(a, b, t)', 'description': 'cubic ease between a and b'},
'cubic': {'min_args': 3, 'max_args': 3, 'snippet': 'cubic(a, b, t)', 'description': 'cubic ease between a and b'},
'elastic_ease': {'min_args': 3, 'max_args': 3, 'snippet': 'elastic_ease(a, b, t)', 'description': 'elastic ease between a and b'},
'elastic': {'min_args': 3, 'max_args': 3, 'snippet': 'elastic(a, b, t)', 'description': 'elastic ease between a and b'},
'sine_ease': {'min_args': 3, 'max_args': 3, 'snippet': 'sine_ease(a, b, t)', 'description': 'sine easing between a and b'},
'sine': {'min_args': 3, 'max_args': 3, 'snippet': 'sine(a, b, t)', 'description': 'sine easing between a and b'},
'smootherstep': {'min_args': 3, 'max_args': 3, 'snippet': 'smootherstep(x, edge0, edge1)', 'description': 'smoother interpolation between edge0 and edge1'},
'dist': {'min_args': 4, 'max_args': 4, 'snippet': 'dist(x1, y1, x2, y2)', 'description': 'computes Euclidean distance between (x1, y1) and (x2, y2)'},
'distance': {'min_args': 4, 'max_args': 4, 'snippet': 'distance(x1, y1, x2, y2)', 'description': 'computes Euclidean distance between (x1, y1) and (x2, y2)'},
'remap': {'min_args': 5, 'max_args': 5, 'snippet': 'remap(v, i_min, i_max, o_min, o_max)', 'description': 'remaps values from input range to output range'},
'cossim': {'min_args': 2, 'max_args': 2, 'snippet': 'cossim(x, y)', 'description': 'computes the cosine similarity between x and y.'},
'cosine_similarity': {'min_args': 2, 'max_args': 2, 'snippet': 'cosine_similarity(x, y)', 'description': 'computes the cosine similarity between x and y.'},
'count': {'min_args': 1, 'max_args': 1, 'snippet': 'count(x)', 'description': 'returns the number of elements in x'},
'cnt': {'min_args': 1, 'max_args': 1, 'snippet': 'cnt(x)', 'description': 'returns the number of elements in x'},
'length': {'min_args': 1, 'max_args': 1, 'snippet': 'length(x)', 'description': 'returns the number of elements in x'},
'keys': {'min_args': 1, 'max_args': 1, 'snippet': 'keys(x)', 'description': 'returns a list of dictionary keys in insertion order'},
'enable_tracking': {'min_args': 1, 'max_args': 1, 'snippet': 'enable_tracking(d)', 'description': 'enables change tracking for ChangeTrackingDict d (returns d)'},
'track_on': {'min_args': 1, 'max_args': 1, 'snippet': 'track_on(d)', 'description': 'enables change tracking for ChangeTrackingDict d (returns d)'},
'disable_tracking': {'min_args': 1, 'max_args': 1, 'snippet': 'disable_tracking(d)', 'description': 'disables change tracking for ChangeTrackingDict d (returns d)'},
'track_off': {'min_args': 1, 'max_args': 1, 'snippet': 'track_off(d)', 'description': 'disables change tracking for ChangeTrackingDict d (returns d)'},
'reset_tracking': {'min_args': 1, 'max_args': 1, 'snippet': 'reset_tracking(d)', 'description': 'resets change tracking state for ChangeTrackingDict d (returns d)'},
'track_reset': {'min_args': 1, 'max_args': 1, 'snippet': 'track_reset(d)', 'description': 'resets change tracking state for ChangeTrackingDict d (returns d)'},
'get_changed_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'get_changed_keys(d)', 'description': 'returns list of keys that were modified or added in ChangeTrackingDict d'},
'changed_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'changed_keys(d)', 'description': 'returns list of keys that were modified or added in ChangeTrackingDict d'},
'get_added_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'get_added_keys(d)', 'description': 'returns list of keys that were added (newly created) in ChangeTrackingDict d'},
'added_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'added_keys(d)', 'description': 'returns list of keys that were added (newly created) in ChangeTrackingDict d'},
'get_removed_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'get_removed_keys(d)', 'description': 'returns list of keys that were removed from ChangeTrackingDict d'},
'removed_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'removed_keys(d)', 'description': 'returns list of keys that were removed from ChangeTrackingDict d'},
'get_unchanged_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'get_unchanged_keys(d)', 'description': 'returns list of keys that remain unchanged in ChangeTrackingDict d'},
'unchanged_keys': {'min_args': 1, 'max_args': 1, 'snippet': 'unchanged_keys(d)', 'description': 'returns list of keys that remain unchanged in ChangeTrackingDict d'},
'repeat': {'min_args': 2, 'max_args': 3, 'snippet': 'repeat(x, count, dims)', 'description': 'repeats tensor elements; count may be scalar or per-dim list'},
'flatten': {'min_args': 1, 'max_args': 1, 'snippet': 'flatten(x)', 'description': 'flattens tensor into a 1D tensor'},
'append': {'min_args': 2, 'max_args': 2, 'snippet': 'append(x, y)', 'description': 'appends y to the end of x. If inputs are tensors use concatenate(x,...,dim)'},
'get_value': {'min_args': 1, 'max_args': 1, 'snippet': 'get_value(slot)', 'description': 'returns the value at slot from stack'},
'flow_apply': {'min_args': 2, 'max_args': 2, 'snippet': 'flow_apply(image, flow)', 'description': 'applies optical flow to an image'},
'batch_shuffle': {'min_args': 2, 'max_args': 2, 'snippet': 'batch_shuffle(x, indices)', 'description': 'shuffles and duplicates or skips the batch dimension of x according to indices'},
'shuffle': {'min_args': 2, 'max_args': 2, 'snippet': 'shuffle(x, indices)', 'description': 'shuffles and duplicates or skips the batch dimension of x according to indices'},
'select': {'min_args': 2, 'max_args': 2, 'snippet': 'select(x, indices)', 'description': 'shuffles and duplicates or skips the batch dimension of x according to indices'},
'motion_mask': {'min_args': 1, 'max_args': 1, 'snippet': 'motion_mask(x)', 'description': 'generates a motion mask from optical flow'},
'flow_to_image': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_to_image(x)', 'description': 'converts optical flow x to an RGB image. Up-Down is blue-yellow Left-right green-magenta.'},
'flow_mag': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_mag(x)', 'description': 'computes the magnitude of optical flow (or any other structure with last dimension of 2)'},
'flow_magnitude': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_magnitude(x)', 'description': 'computes the magnitude of optical flow (or any other structure with last dimension of 2)'},
'flow_ang': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_ang(x)', 'description': 'computes the angle of optical flow (basically angle() but real and imaginery is separate in 2 dimensions)'},
'flow_angle': {'min_args': 1, 'max_args': 1, 'snippet': 'flow_angle(x)', 'description': 'computes the angle of optical flow (basically angle() but real and imaginery is separate in 2 dimensions)'},
'where': {'min_args': 3, 'max_args': 3, 'snippet': 'where(condition, x, y)', 'description': 'returns x if condition is true, else y. Runs per element.'},
'histogram': {'min_args': 4, 'max_args': 4, 'snippet': 'histogram(x, bins, min, max)', 'description': 'computes histogram'},
'hist': {'min_args': 4, 'max_args': 4, 'snippet': 'hist(x, bins, min, max)', 'description': 'computes histogram'},
'overlay': {'min_args': 3, 'max_args': 4, 'snippet': 'overlay(base, overlay, offset, opacity)', 'description': 'overlays overlay on base at offset. Opacity controls blending. It can be tensor or float. If it is tensor it should be the same shape as overlay.'},
'pad': {'min_args': 2, 'max_args': 2, 'snippet': 'pad(x, padding)', 'description': 'pads tensor x with specified padding (dim 0 neg, dim 0 pos, dim 1 ...)'},
'cross': {'min_args': 2, 'max_args': 2, 'snippet': 'cross(x, y)', 'description': 'computes the cross product of x and y. Last dimension must be 3'},
'matmul': {'min_args': 2, 'max_args': 2, 'snippet': 'matmul(x, y)', 'description': 'computes the matrix multiplication of x and y'},
'rife': {'min_args': 2, 'max_args': 5, 'snippet': 'rife(image1, image2, tiling_size, iterations, multi_scale)', 'description': 'computes the optical flow field ([H, W, 2]) between image1 and image2 using a RAFT-based model'},
'bnot': {'min_args': 1, 'max_args': 1, 'snippet': 'bnot(x)', 'description': 'computes the bitwise NOT'},
'bitwise_not': {'min_args': 1, 'max_args': 1, 'snippet': 'bitwise_not(x)', 'description': 'computes the bitwise NOT'},
'bitcount': {'min_args': 1, 'max_args': 1, 'snippet': 'bitcount(x)', 'description': 'returns the number of set bits'},
'popcount': {'min_args': 1, 'max_args': 1, 'snippet': 'popcount(x)', 'description': 'returns the number of set bits'},
'popcnt': {'min_args': 1, 'max_args': 1, 'snippet': 'popcnt(x)', 'description': 'returns the number of set bits'},
'shape': {'min_args': 1, 'max_args': 1, 'snippet': 'shape(x)', 'description': 'returns the shape of tensor as a list'},
'band': {'min_args': 2, 'max_args': 2, 'snippet': 'band(x, y)', 'description': 'computes the element-wise bitwise AND of x and y'},
'bitwise_and': {'min_args': 2, 'max_args': 2, 'snippet': 'bitwise_and(x, y)', 'description': 'computes the element-wise bitwise AND of x and y'},
'bxor': {'min_args': 2, 'max_args': 2, 'snippet': 'bxor(x, y)', 'description': 'computes the element-wise bitwise XOR of x and y'},
'bitwise_xor': {'min_args': 2, 'max_args': 2, 'snippet': 'bitwise_xor(x, y)', 'description': 'computes the element-wise bitwise XOR of x and y'},
'bor': {'min_args': 2, 'max_args': 2, 'snippet': 'bor(x, y)', 'description': 'computes the element-wise bitwise OR of x and y'},
'bitwise_or': {'min_args': 2, 'max_args': 2, 'snippet': 'bitwise_or(x, y)', 'description': 'computes the element-wise bitwise OR of x and y'},
'tensor': {'min_args': 1, 'max_args': 3, 'snippet': 'tensor(shape, value, dtype_template)', 'description': 'creates a tensor with specified shape and optional value/type. Default value is 0 and default format FP32'},
'add_key': {'min_args': 3, 'max_args': 3, 'snippet': 'add_key(dict, key, value)', 'description': 'adds or replaces a dictionary entry and returns the updated dictionary'},
'remove_key': {'min_args': 2, 'max_args': 2, 'snippet': 'remove_key(dict, key)', 'description': 'removes a dictionary entry and returns the updated dictionary'},
'rm_key': {'min_args': 2, 'max_args': 2, 'snippet': 'rm_key(dict, key)', 'description': 'removes a dictionary entry and returns the updated dictionary'},
'stack_push': {'min_args': 2, 'max_args': 2, 'snippet': 'stack_push(slot, val)', 'description': 'pushes val onto the end of slot stack'},
'stack_pop': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_pop(slot)', 'description': 'removes and returns the last element of slot stack'},
'stack_clear': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_clear(slot)', 'description': 'clears the contents of slot stack'},
'stack_has': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_has(slot)', 'description': 'returns 1.0 if slot exists in stack and is not empty else returns 0.0'},
'stack_get': {'min_args': 1, 'max_args': 1, 'snippet': 'stack_get(slot)', 'description': 'returns the last pushed value in slot without popping it'},
'cat': {'min_args': 3, 'max_args': None, 'snippet': 'cat(x1, x2, ..., dim)', 'description': 'concatenates tensors along the specified dimension'},
'concat': {'min_args': 3, 'max_args': None, 'snippet': 'concat(x1, x2, ..., dim)', 'description': 'concatenates tensors along the specified dimension'},
'concatenate': {'min_args': 3, 'max_args': None, 'snippet': 'concatenate(x1, x2, ..., dim)', 'description': 'concatenates tensors along the specified dimension'},
'int': {'min_args': 1, 'max_args': 1, 'snippet': 'int(x)', 'description': 'casts x to integer'},
'float': {'min_args': 1, 'max_args': 1, 'snippet': 'float(x)', 'description': 'casts x to float'},
'upper': {'min_args': 1, 'max_args': 1, 'snippet': 'upper(x)', 'description': 'converts string to uppercase'},
'lower': {'min_args': 1, 'max_args': 1, 'snippet': 'lower(x)', 'description': 'converts string to lowercase'},
'trim': {'min_args': 1, 'max_args': 1, 'snippet': 'trim(x)', 'description': 'removes leading and trailing whitespace from string'},
'split': {'min_args': 1, 'max_args': 2, 'snippet': 'split(x, delimiter)', 'description': 'splits string to list of strings based on delimiter. Default is space.'},
'join': {'min_args': 1, 'max_args': 2, 'snippet': 'join(x, separator)', 'description': 'joins strings in list using separator. Defalut separator is "". If you want to join tensors use concatinate(...)'},
'substring': {'min_args': 2, 'max_args': 3, 'snippet': 'substring(s, start, length)', 'description': 'returns length characters of s starting at start. If length is not supplied, it returns the rest of the string'},
'substr': {'min_args': 2, 'max_args': 3, 'snippet': 'substr(s, start, length)', 'description': 'returns length characters of s starting at start. If length is not supplied, it returns the rest of the string'},
'find': {'min_args': 2, 'max_args': 2, 'snippet': 'find(s, sub)', 'description': 'returns the index of the first occurrence of sub in s. -1 if not found. Works only for strings.'},
'replace': {'min_args': 3, 'max_args': 3, 'snippet': 'replace(s, old, new)', 'description': 'replaces occurrences of old with new in s. Mostly for strings but can work with lists and tensors.'},
'dilate': {'min_args': 1, 'max_args': 2, 'snippet': 'dilate(x, kernel)', 'description': 'applies dilation to x. kernel is optional.'},
'erode': {'min_args': 1, 'max_args': 2, 'snippet': 'erode(x, kernel)', 'description': 'applies erosion to x. kernel is size of used matrix.'},
'morph_open': {'min_args': 1, 'max_args': 2, 'snippet': 'morph_open(x, kernel)', 'description': 'applies morphological opening to x. kernel is size of used matrix.'},
'morph_close': {'min_args': 1, 'max_args': 2, 'snippet': 'morph_close(x, kernel)', 'description': 'applies morphological closing to x. kernel is size of used matrix.'},
'rgb_to_oklab': {'min_args': 1, 'max_args': 3, 'snippet': 'rgb_to_oklab(r, g, b)', 'description': 'converts RGB to OKLab. It has 2 modes. Aither 1 or 3 parameters. With 1 parameter it expects last dimension of 3 and with 3 it concatinates them after converting.'},
'rgb_to_cielab': {'min_args': 1, 'max_args': 3, 'snippet': 'rgb_to_cielab(r, g, b)', 'description': 'converts RGB to CIELAB. It has 2 modes. Aither 1 or 3 parameters. With 1 parameter it expects last dimension of 3 and with 3 it concatinates them after converting.'},
'oklab_to_rgb': {'min_args': 1, 'max_args': 3, 'snippet': 'oklab_to_rgb(oklab)', 'description': 'converts OKLab to RGB. It has 2 modes. Aither 1 or 3 parameters. With 1 parameter it expects last dimension of 3 and with 3 it concatinates them after converting.'},
'cielab_to_rgb': {'min_args': 1, 'max_args': 3, 'snippet': 'cielab_to_rgb(cielab)', 'description': 'converts CIELAB to RGB. It has 2 modes. Aither 1 or 3 parameters. With 1 parameter it expects last dimension of 3 and with 3 it concatinates them after converting.'},
'rgb_to_hsv': {'min_args': 1, 'max_args': 4, 'snippet': 'rgb_to_hsv(rgb, degrees)', 'description': 'converts RGB to HSV. It has 2 modes. Aither 1 or 3 parameters. With 1 parameter it expects last dimension of 3 and with 3 it concatinates them after converting. If degrees is specified and nonzero it returns hue as 0-360 instead of 0-1'},
'hsv_to_rgb': {'min_args': 1, 'max_args': 4, 'snippet': 'hsv_to_rgb(hsv, degrees)', 'description': 'converts HSV to RGB. It has 2 modes. Aither 1 or 3 parameters. With 1 parameter it expects last dimension of 3 and with 3 it concatinates them after converting.'},
'int_to_rgb': {'min_args': 1, 'max_args': 1, 'snippet': 'int_to_rgb(val)', 'description': 'converts an integer to an RGB color. If input is tensor it stacks the resulting R, G and B tensors using last dimension. If It is list, List of lists is returned and if value, 3 long list is returned.'},
'rgb_to_int': {'min_args': 3, 'max_args': 3, 'snippet': 'rgb_to_int(r, g, b)', 'description': 'converts RGB components to an integer It uses last dimension of tensor (or list) as R,G,B if one value. Othervise uses whole thing as channel.'},
'interpolate_linear': {'min_args': 2, 'max_args': 2, 'snippet': 'interpolate_linear(x, y)', 'description': 'performs linear interpolation on X. Assumes that dim 0 is batch and dim 1 = channel'},
'interpolate_area': {'min_args': 2, 'max_args': 2, 'snippet': 'interpolate_area(x, y)', 'description': 'performs area interpolation X. Assumes that dim 0 is batch and dim 1 = channel'},
'interpolate_nearest': {'min_args': 2, 'max_args': 2, 'snippet': 'interpolate_nearest(x, y)', 'description': 'performs nearest neighbor interpolation X. Assumes that dim 0 is batch and dim 1 = channel'},
'interpolate_nearest_exact': {'min_args': 2, 'max_args': 2, 'snippet': 'interpolate_nearest_exact(x, y)', 'description': 'performs nearest neighbor interpolation X. Assumes that dim 0 is batch and dim 1 = channel'},
'text_image': {'min_args': 3, 'max_args': 9, 'snippet': 'text_image(text, font, size, max_width, weight, rotation_angle, line_spacing, italic, underline)', 'description': 'renders text to an 2D tensor'},
'as_nested_tensor': {'min_args': 1, 'max_args': 1, 'snippet': 'as_nested_tensor(x)', 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)'},
'as_nested': {'min_args': 1, 'max_args': 1, 'snippet': 'as_nested(x)', 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)'},
'svd': {'min_args': 1, 'max_args': 2, 'snippet': 'svd(x, full_matrices)', 'description': 'computes the singular value decomposition of x. Returns a list of 3 tensors: U, S, V such that x = matmul(matmul(U, diagonal_matrix(S,shape(U))), V)'},
'singular_value_decomposition': {'min_args': 1, 'max_args': 2, 'snippet': 'singular_value_decomposition(x, full_matrices)', 'description': 'computes the singular value decomposition of x. Returns a list of 3 tensors: U, S, V such that x = matmul(matmul(U, diagonal_matrix(S,shape(U))), V)'},
'diagonal_matrix': {'min_args': 2, 'max_args': 5, 'snippet': 'diagonal_matrix(x, shape, offset, dim1, dim2)', 'description': 'creates a diagonal matrix from x with specified shape. offset is the diagonal offset. dim1 and dim2 are the dimensions to place the diagonal on.'},
'diag': {'min_args': 2, 'max_args': 5, 'snippet': 'diag(x, shape, offset, dim1, dim2)', 'description': 'creates a diagonal matrix from x with specified shape. offset is the diagonal offset. dim1 and dim2 are the dimensions to place the diagonal on.'},
'timestamp': {'min_args': 0, 'max_args': 0, 'snippet': 'timestamp()', 'description': 'returns the current system timestamp'},
'now': {'min_args': 0, 'max_args': 0, 'snippet': 'now()', 'description': 'returns the current system timestamp'},
'sort': {'min_args': 1, 'max_args': 3, 'snippet': 'sort(x, desc, dim)', 'description': 'returns a sorted version of x (If input is tensor, it sorts the last dimension)'},
'argsort': {'min_args': 1, 'max_args': 3, 'snippet': 'argsort(x, desc, dim)', 'description': 'returns the indices that would sort x. desc defaults to ascending and dim defaults to the last dimension.'},
'argmin': {'min_args': 1, 'max_args': 2, 'snippet': 'argmin(x, as_position)', 'description': 'returns the minimum position in flattened x or coordinates as a list when requested'},
'argmax': {'min_args': 1, 'max_args': 2, 'snippet': 'argmax(x, as_position)', 'description': 'returns the maximum position in flattened x or coordinates as a list when requested'},
'softmax': {'min_args': 1, 'max_args': 2, 'snippet': 'softmax(x, axis)', 'description': 'applies the softmax function to x (sum of slice == 1.0 and max value <= 1.0). axis defaults to last.'},
'softmin': {'min_args': 1, 'max_args': 2, 'snippet': 'softmin(x, axis)', 'description': 'applies the softmin function to x (same as softmax(-x,[axis])). axis defaults to last.'},
'unique': {'min_args': 1, 'max_args': 1, 'snippet': 'unique(x)', 'description': 'returns the unique elements (sorted, flatened)'},
'flip': {'min_args': 2, 'max_args': 2, 'snippet': 'flip(x, dims)', 'description': 'reverses the order of elements along the specified dimensions. Works also for text and list with dims=0'},
'startswith': {'min_args': 2, 'max_args': 2, 'snippet': 'startswith(text, prefix)', 'description': 'returns 1 if text starts with prefix, else 0'},
'endswith': {'min_args': 2, 'max_args': 2, 'snippet': 'endswith(text, suffix)', 'description': 'returns 1 if text ends with suffix, else 0'},
'squeeze': {'min_args': 1, 'max_args': 2, 'snippet': 'squeeze(x, dim)', 'description': 'removes size-1 dimensions from tensor x, optionally at dim'},
'unsqueeze': {'min_args': 2, 'max_args': 2, 'snippet': 'unsqueeze(x, dim)', 'description': 'inserts a size-1 dimension into tensor x at dim'},
'roll': {'min_args': 2, 'max_args': 3, 'snippet': 'roll(x, shifts, axis)', 'description': 'rolls tensor x along axis'},
'cov': {'min_args': 2, 'max_args': 2, 'snippet': 'cov(x, y)', 'description': 'computes the covariance matrix of x and y'},
'corr': {'min_args': 2, 'max_args': 2, 'snippet': 'corr(x, y)', 'description': 'computes the correlation between x and y'},
'correlation': {'min_args': 2, 'max_args': 2, 'snippet': 'correlation(x, y)', 'description': 'computes the correlation between x and y'},
'entropy': {'min_args': 1, 'max_args': 1, 'snippet': 'entropy(x)', 'description': 'computes shanon entropy'},
'crop': {'min_args': 3, 'max_args': 3, 'snippet': 'crop(x, start, size)', 'description': 'crops tensor x. Take size and begin at start. start should be the low corner. The result is at most size (if size is bigger then rest of tensor).'},
'coords': {'min_args': 2, 'max_args': 3, 'snippet': 'coords(shape, dim, dtype)', 'description': 'generates a tensor with the specified shape whose values are the coordinates of each element along the specified dimension. The dtype can be specified to control the data type of the output tensor.'},
'coordinates': {'min_args': 2, 'max_args': 3, 'snippet': 'coordinates(shape, dim, dtype)', 'description': 'generates a tensor with the specified shape whose values are the coordinates of each element along the specified dimension. The dtype can be specified to control the data type of the output tensor.'},
'noise': {'min_args': 1, 'max_args': 2, 'snippet': 'noise(seed, shape)', 'description': 'generates normally distributed random noise'},
'randn': {'min_args': 1, 'max_args': 2, 'snippet': 'randn(seed, shape)', 'description': 'generates normally distributed random noise'},
'random_normal': {'min_args': 1, 'max_args': 2, 'snippet': 'random_normal(seed, shape)', 'description': 'generates normally distributed random noise'},
'rand': {'min_args': 1, 'max_args': 2, 'snippet': 'rand(seed, shape)', 'description': 'generates uniformly distributed random noise'},
'randu': {'min_args': 1, 'max_args': 2, 'snippet': 'randu(seed, shape)', 'description': 'generates uniformly distributed random noise'},
'random_uniform': {'min_args': 1, 'max_args': 2, 'snippet': 'random_uniform(seed, shape)', 'description': 'generates uniformly distributed random noise'},
'randc': {'min_args': 3, 'max_args': 4, 'snippet': 'randc(seed, median, sigma, shape)', 'description': 'generates Cauchy distributed random values.'},
'random_cauchy': {'min_args': 3, 'max_args': 4, 'snippet': 'random_cauchy(seed, median, sigma, shape)', 'description': 'generates Cauchy distributed random values.'},
'rande': {'min_args': 2, 'max_args': 3, 'snippet': 'rande(seed, lambda, shape)', 'description': 'generates exponentially distributed random values'},
'random_exponential': {'min_args': 2, 'max_args': 3, 'snippet': 'random_exponential(seed, lambda, shape)', 'description': 'generates exponentially distributed random values'},
'randln': {'min_args': 3, 'max_args': 4, 'snippet': 'randln(seed, mean, std, shape)', 'description': 'generates log-normally distributed random values.'},
'random_log_normal': {'min_args': 3, 'max_args': 4, 'snippet': 'random_log_normal(seed, mean, std, shape)', 'description': 'generates log-normally distributed random values.'},
'randb': {'min_args': 2, 'max_args': 3, 'snippet': 'randb(seed, probability, shape)', 'description': 'generates Bernoulli distributed random values (0 or 1). Probability can be a tensor'},
'random_bernoulli': {'min_args': 2, 'max_args': 3, 'snippet': 'random_bernoulli(seed, probability, shape)', 'description': 'generates Bernoulli distributed random values (0 or 1). Probability can be a tensor'},
'randp': {'min_args': 2, 'max_args': 3, 'snippet': 'randp(seed, lambda, shape)', 'description': 'generates Poisson distributed random values.'},
'random_poisson': {'min_args': 2, 'max_args': 3, 'snippet': 'random_poisson(seed, lambda, shape)', 'description': 'generates Poisson distributed random values.'},
'randg': {'min_args': 3, 'max_args': 4, 'snippet': 'randg(seed, shape_param, scale, shape)', 'description': 'generates Gamma distributed random values.'},
'random_gamma': {'min_args': 3, 'max_args': 4, 'snippet': 'random_gamma(seed, shape_param, scale, shape)', 'description': 'generates Gamma distributed random values.'},
'randbeta': {'min_args': 3, 'max_args': 4, 'snippet': 'randbeta(seed, alpha, beta, shape)', 'description': 'generates Beta distributed random values'},
'random_beta': {'min_args': 3, 'max_args': 4, 'snippet': 'random_beta(seed, alpha, beta, shape)', 'description': 'generates Beta distributed random values'},
'randl': {'min_args': 3, 'max_args': 4, 'snippet': 'randl(seed, loc, scale, shape)', 'description': 'generates Laplace distributed random values'},
'random_laplace': {'min_args': 3, 'max_args': 4, 'snippet': 'random_laplace(seed, loc, scale, shape)', 'description': 'generates Laplace distributed random values'},
'randgumbel': {'min_args': 3, 'max_args': 4, 'snippet': 'randgumbel(seed, loc, scale, shape)', 'description': 'generates Gumbel distributed random values'},
'random_gumbel': {'min_args': 3, 'max_args': 4, 'snippet': 'random_gumbel(seed, loc, scale, shape)', 'description': 'generates Gumbel distributed random values'},
'randw': {'min_args': 3, 'max_args': 4, 'snippet': 'randw(seed, scale, concentration, shape)', 'description': 'generates Weibull distributed random values'},
'random_weibull': {'min_args': 3, 'max_args': 4, 'snippet': 'random_weibull(seed, scale, concentration, shape)', 'description': 'generates Weibull distributed random values'},
'randchi2': {'min_args': 2, 'max_args': 3, 'snippet': 'randchi2(seed, df, shape)', 'description': 'generates Chi-squared distributed random values'},
'random_chi2': {'min_args': 2, 'max_args': 3, 'snippet': 'random_chi2(seed, df, shape)', 'description': 'generates Chi-squared distributed random values'},
'randt': {'min_args': 2, 'max_args': 3, 'snippet': 'randt(seed, df, shape)', 'description': 'generates Student-t distributed random values'},
'random_studentt': {'min_args': 2, 'max_args': 3, 'snippet': 'random_studentt(seed, df, shape)', 'description': 'generates Student-t distributed random values'},
'perlin': {'min_args': 2, 'max_args': 5, 'snippet': 'perlin(seed, scale, octaves, offset, shape)', 'description': 'generates Perlin noise'},
'perlin_noise': {'min_args': 2, 'max_args': 5, 'snippet': 'perlin_noise(seed, scale, octaves, offset, shape)', 'description': 'generates Perlin noise'},
'cellular': {'min_args': 2, 'max_args': 5, 'snippet': 'cellular(seed, scale, jitter, offset, shape)', 'description': 'generates Cellular/Voronoi noise'},
'voronoi': {'min_args': 2, 'max_args': 5, 'snippet': 'voronoi(seed, scale, jitter, offset, shape)', 'description': 'generates Cellular/Voronoi noise'},
'worley': {'min_args': 2, 'max_args': 5, 'snippet': 'worley(seed, scale, jitter, offset, shape)', 'description': 'generates Cellular/Voronoi noise'},
'cellular_noise': {'min_args': 2, 'max_args': 5, 'snippet': 'cellular_noise(seed, scale, jitter, offset, shape)', 'description': 'generates Cellular/Voronoi noise'},
'voronoi_noise': {'min_args': 2, 'max_args': 5, 'snippet': 'voronoi_noise(seed, scale, jitter, offset, shape)', 'description': 'generates Cellular/Voronoi noise'},
'plasma': {'min_args': 2, 'max_args': 5, 'snippet': 'plasma(seed, scale, octaves, offset, shape)', 'description': 'generates Plasma/Turbulence noise - same as perlin but default octaves 4'},
'turbulence': {'min_args': 2, 'max_args': 5, 'snippet': 'turbulence(seed, scale, octaves, offset, shape)', 'description': 'generates Plasma/Turbulence noise - same as perlin but default octaves 4'},
'plasma_noise': {'min_args': 2, 'max_args': 5, 'snippet': 'plasma_noise(seed, scale, octaves, offset, shape)', 'description': 'generates Plasma/Turbulence noise - same as perlin but default octaves 4'},
'ridged': {'min_args': 2, 'max_args': 5, 'snippet': 'ridged(seed, scale, octaves, offset, shape)', 'description': 'generates ridged multi-fractal noise'},
'ridged_noise': {'min_args': 2, 'max_args': 5, 'snippet': 'ridged_noise(seed, scale, octaves, offset, shape)', 'description': 'generates ridged multi-fractal noise'},
'domain_warp': {'min_args': 4, 'max_args': 8, 'snippet': 'domain_warp(seed, scale, warp_scale, warp_strength, octaves, warp_octaves, offset, shape)', 'description': 'generates domain warped noise'},
'domain_warp_noise': {'min_args': 4, 'max_args': 8, 'snippet': 'domain_warp_noise(seed, scale, warp_scale, warp_strength, octaves, warp_octaves, offset, shape)', 'description': 'generates domain warped noise'},
}
File diff suppressed because one or more lines are too long
+412
View File
@@ -0,0 +1,412 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
LINSPACE=53
LOGSPACE=54
TOPK=55
BOTK=56
PINV=57
SUM=58
MEAN=59
STD=60
VAR=61
QUARTILE=62
PERCENTILE=63
QUANTILE=64
DOT=65
MOMENT=66
ERF=67
ERFINV=68
ANY=69
ALL=70
EDGE=71
GAUSSIAN=72
MEDIAN=73
MODE=74
CUMSUM=75
CUMPROD=76
TOPK_IND=77
BOTK_IND=78
CUBIC_EASE=79
ELASTIC_EASE=80
SINE_EASE=81
SMOOTHERSTEP=82
DIST=83
REMAP=84
COSSIM=85
COUNT=86
KEYS=87
ENABLE_TRACKING=88
DISABLE_TRACKING=89
RESET_TRACKING=90
GET_CHANGED_KEYS=91
GET_ADDED_KEYS=92
GET_REMOVED_KEYS=93
GET_UNCHANGED_KEYS=94
REPEAT=95
FLATTEN=96
APPEND=97
GET_VALUE=98
FLOW_APPLY=99
BATCH_SHUFFLE=100
MOTION_MASK=101
FLOW_TO_IMAGE=102
FLOW_MAG=103
FLOW_ANG=104
WHERE=105
HISTOGRAM=106
OVERLAY=107
PAD=108
CROSS=109
MATMUL=110
RIFE=111
BNOT=112
BITCOUNT=113
SHAPE=114
BAND=115
XOR=116
BOR=117
TENSOR=118
ADD_KEY=119
REMOVE_KEY=120
PUSH=121
POP=122
CLEAR=123
HAS=124
GET=125
CONCAT=126
INT=127
FLOAT=128
UPPER=129
LOWER=130
TRIM=131
SPLIT=132
JOIN=133
SUBSTRING=134
FIND=135
REPLACE=136
DILATE=137
ERODE=138
MORPH_OPEN=139
MORPH_CLOSE=140
RGB_TO_OKLAB=141
RGB_TO_CIELAB=142
OKLAB_TO_RGB=143
CIELAB_TO_RGB=144
RGB_TO_HSV=145
HSV_TO_RGB=146
INT_TO_RGB=147
RGB_TO_INT=148
INTERPOLATE_LINEAR=149
INTERPOLATE_AREA=150
INTERPOLATE_NEAREST=151
TEXT_IMAGE=152
AS_NESTED=153
SVD=154
DIAG=155
IF=156
ELSE=157
WHILE=158
FOR=159
IN=160
BREAK=161
CONTINUE=162
RETURN=163
TIMESTAMP=164
SORT=165
ARGSORT=166
ARGMIN=167
ARGMAX=168
SOFTMAX=169
SOFTMIN=170
UNIQUE=171
FLIP=172
STARTSWITH=173
ENDSWITH=174
SQUEEZE=175
UNSQUEEZE=176
ROLL=177
COV=178
CORR=179
ENTROPY=180
CROP=181
NONE=182
COORDS=183
NOISE=184
RAND=185
CAUCHY=186
EXPONENTIAL=187
LOGNORMAL=188
BERNOULLI=189
POISSON=190
GAMMADIST=191
BETADIST=192
LAPLACEDIST=193
GUMBELDIST=194
WEIBULLDIST=195
CHI2DIST=196
STUDENTTDIST=197
PERLIN=198
CELLULAR=199
PLASMA=200
RIDGED=201
DOMAIN_WARP=202
PLUS=203
MINUS=204
MULT=205
DIV=206
MOD=207
POW=208
LSHIFT=209
RSHIFT=210
GE=211
GT=212
LE=213
LT=214
EQ=215
EQUEALS=216
PLUS_EQ=217
MINUS_EQ=218
MULT_EQ=219
DIV_EQ=220
MOD_EQ=221
NE=222
PIPE=223
LPAREN=224
RPAREN=225
COMMA=226
SEMICOLON=227
ARROW=228
LBRACKET=229
RBRACKET=230
QUESTION=231
COLON=232
LBRACE=233
RBRACE=234
NUMBER=235
CONSTANT=236
STRING=237
VARIABLE=238
SL_COMMENT=239
ML_COMMENT=240
WS=241
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'linspace'=53
'logspace'=54
'topk'=55
'botk'=56
'pinv'=57
'sum'=58
'mean'=59
'std'=60
'var'=61
'quantile'=64
'dot'=65
'moment'=66
'erf'=67
'erfinv'=68
'any'=69
'all'=70
'edge'=71
'median'=73
'mode'=74
'cumsum'=75
'cumprod'=76
'smootherstep'=82
'remap'=84
'keys'=87
'repeat'=95
'flatten'=96
'append'=97
'get_value'=98
'flow_apply'=99
'motion_mask'=101
'flow_to_image'=102
'where'=105
'overlay'=107
'pad'=108
'cross'=109
'matmul'=110
'rife'=111
'shape'=114
'tensor'=118
'add_key'=119
'stack_push'=121
'stack_pop'=122
'stack_clear'=123
'stack_has'=124
'stack_get'=125
'int'=127
'float'=128
'upper'=129
'lower'=130
'trim'=131
'split'=132
'join'=133
'find'=135
'replace'=136
'dilate'=137
'erode'=138
'morph_open'=139
'morph_close'=140
'rgb_to_oklab'=141
'rgb_to_cielab'=142
'oklab_to_rgb'=143
'cielab_to_rgb'=144
'rgb_to_hsv'=145
'hsv_to_rgb'=146
'int_to_rgb'=147
'rgb_to_int'=148
'interpolate_linear'=149
'interpolate_area'=150
'text_image'=152
'if'=156
'else'=157
'while'=158
'for'=159
'in'=160
'break'=161
'continue'=162
'return'=163
'sort'=165
'argsort'=166
'argmin'=167
'argmax'=168
'softmax'=169
'softmin'=170
'unique'=171
'flip'=172
'startswith'=173
'endswith'=174
'squeeze'=175
'unsqueeze'=176
'roll'=177
'cov'=178
'entropy'=180
'crop'=181
'+'=203
'-'=204
'*'=205
'/'=206
'%'=207
'^'=208
'<<'=209
'>>'=210
'>='=211
'>'=212
'<='=213
'<'=214
'=='=215
'='=216
'+='=217
'-='=218
'*='=219
'/='=220
'%='=221
'!='=222
'|'=223
'('=224
')'=225
','=226
';'=227
'->'=228
'['=229
']'=230
'?'=231
':'=232
'{'=233
'}'=234
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,412 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
PERM=50
RESHAPE=51
RANGE=52
LINSPACE=53
LOGSPACE=54
TOPK=55
BOTK=56
PINV=57
SUM=58
MEAN=59
STD=60
VAR=61
QUARTILE=62
PERCENTILE=63
QUANTILE=64
DOT=65
MOMENT=66
ERF=67
ERFINV=68
ANY=69
ALL=70
EDGE=71
GAUSSIAN=72
MEDIAN=73
MODE=74
CUMSUM=75
CUMPROD=76
TOPK_IND=77
BOTK_IND=78
CUBIC_EASE=79
ELASTIC_EASE=80
SINE_EASE=81
SMOOTHERSTEP=82
DIST=83
REMAP=84
COSSIM=85
COUNT=86
KEYS=87
ENABLE_TRACKING=88
DISABLE_TRACKING=89
RESET_TRACKING=90
GET_CHANGED_KEYS=91
GET_ADDED_KEYS=92
GET_REMOVED_KEYS=93
GET_UNCHANGED_KEYS=94
REPEAT=95
FLATTEN=96
APPEND=97
GET_VALUE=98
FLOW_APPLY=99
BATCH_SHUFFLE=100
MOTION_MASK=101
FLOW_TO_IMAGE=102
FLOW_MAG=103
FLOW_ANG=104
WHERE=105
HISTOGRAM=106
OVERLAY=107
PAD=108
CROSS=109
MATMUL=110
RIFE=111
BNOT=112
BITCOUNT=113
SHAPE=114
BAND=115
XOR=116
BOR=117
TENSOR=118
ADD_KEY=119
REMOVE_KEY=120
PUSH=121
POP=122
CLEAR=123
HAS=124
GET=125
CONCAT=126
INT=127
FLOAT=128
UPPER=129
LOWER=130
TRIM=131
SPLIT=132
JOIN=133
SUBSTRING=134
FIND=135
REPLACE=136
DILATE=137
ERODE=138
MORPH_OPEN=139
MORPH_CLOSE=140
RGB_TO_OKLAB=141
RGB_TO_CIELAB=142
OKLAB_TO_RGB=143
CIELAB_TO_RGB=144
RGB_TO_HSV=145
HSV_TO_RGB=146
INT_TO_RGB=147
RGB_TO_INT=148
INTERPOLATE_LINEAR=149
INTERPOLATE_AREA=150
INTERPOLATE_NEAREST=151
TEXT_IMAGE=152
AS_NESTED=153
SVD=154
DIAG=155
IF=156
ELSE=157
WHILE=158
FOR=159
IN=160
BREAK=161
CONTINUE=162
RETURN=163
TIMESTAMP=164
SORT=165
ARGSORT=166
ARGMIN=167
ARGMAX=168
SOFTMAX=169
SOFTMIN=170
UNIQUE=171
FLIP=172
STARTSWITH=173
ENDSWITH=174
SQUEEZE=175
UNSQUEEZE=176
ROLL=177
COV=178
CORR=179
ENTROPY=180
CROP=181
NONE=182
COORDS=183
NOISE=184
RAND=185
CAUCHY=186
EXPONENTIAL=187
LOGNORMAL=188
BERNOULLI=189
POISSON=190
GAMMADIST=191
BETADIST=192
LAPLACEDIST=193
GUMBELDIST=194
WEIBULLDIST=195
CHI2DIST=196
STUDENTTDIST=197
PERLIN=198
CELLULAR=199
PLASMA=200
RIDGED=201
DOMAIN_WARP=202
PLUS=203
MINUS=204
MULT=205
DIV=206
MOD=207
POW=208
LSHIFT=209
RSHIFT=210
GE=211
GT=212
LE=213
LT=214
EQ=215
EQUEALS=216
PLUS_EQ=217
MINUS_EQ=218
MULT_EQ=219
DIV_EQ=220
MOD_EQ=221
NE=222
PIPE=223
LPAREN=224
RPAREN=225
COMMA=226
SEMICOLON=227
ARROW=228
LBRACKET=229
RBRACKET=230
QUESTION=231
COLON=232
LBRACE=233
RBRACE=234
NUMBER=235
CONSTANT=236
STRING=237
VARIABLE=238
SL_COMMENT=239
ML_COMMENT=240
WS=241
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=52
'linspace'=53
'logspace'=54
'topk'=55
'botk'=56
'pinv'=57
'sum'=58
'mean'=59
'std'=60
'var'=61
'quantile'=64
'dot'=65
'moment'=66
'erf'=67
'erfinv'=68
'any'=69
'all'=70
'edge'=71
'median'=73
'mode'=74
'cumsum'=75
'cumprod'=76
'smootherstep'=82
'remap'=84
'keys'=87
'repeat'=95
'flatten'=96
'append'=97
'get_value'=98
'flow_apply'=99
'motion_mask'=101
'flow_to_image'=102
'where'=105
'overlay'=107
'pad'=108
'cross'=109
'matmul'=110
'rife'=111
'shape'=114
'tensor'=118
'add_key'=119
'stack_push'=121
'stack_pop'=122
'stack_clear'=123
'stack_has'=124
'stack_get'=125
'int'=127
'float'=128
'upper'=129
'lower'=130
'trim'=131
'split'=132
'join'=133
'find'=135
'replace'=136
'dilate'=137
'erode'=138
'morph_open'=139
'morph_close'=140
'rgb_to_oklab'=141
'rgb_to_cielab'=142
'oklab_to_rgb'=143
'cielab_to_rgb'=144
'rgb_to_hsv'=145
'hsv_to_rgb'=146
'int_to_rgb'=147
'rgb_to_int'=148
'interpolate_linear'=149
'interpolate_area'=150
'text_image'=152
'if'=156
'else'=157
'while'=158
'for'=159
'in'=160
'break'=161
'continue'=162
'return'=163
'sort'=165
'argsort'=166
'argmin'=167
'argmax'=168
'softmax'=169
'softmin'=170
'unique'=171
'flip'=172
'startswith'=173
'endswith'=174
'squeeze'=175
'unsqueeze'=176
'roll'=177
'cov'=178
'entropy'=180
'crop'=181
'+'=203
'-'=204
'*'=205
'/'=206
'%'=207
'^'=208
'<<'=209
'>>'=210
'>='=211
'>'=212
'<='=213
'<'=214
'=='=215
'='=216
'+='=217
'-='=218
'*='=219
'/='=220
'%='=221
'!='=222
'|'=223
'('=224
')'=225
','=226
';'=227
'->'=228
'['=229
']'=230
'?'=231
':'=232
'{'=233
'}'=234
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+270
View File
@@ -0,0 +1,270 @@
"""
GPU-accelerated noise generation utilities for arbitrary ND tensors.
Pure PyTorch implementation with deterministic seeding.
True Perlin noise with gradient interpolation - arbitrary dimensions.
"""
import torch
class NoiseUtils:
@staticmethod
def _fade(t):
"""
Optimalizovaná Perlinova fade křivka.
Místo t**3 používáme t * t * t, což je v PyTorchi řádově rychlejší.
"""
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
@staticmethod
def _hash_nd(coords, seed=0):
"""
PLNĚ VEKTORIZOVANÁ matematická hashovací funkce.
Žádné bitové operace. Využívá pseudo-náhodnou sinusovou vlnu.
Čas výpočtu pro 25 mil. bodů klesne na milisekundy.
"""
# Použijeme stabilní deterministické koeficienty pro míchání dimenzí
constants = [12.9898, 78.233, 45.164, 92.411, 27.531, 53.892]
# Inicializace jako float pro implicitní broadcasting místo full_like
dt = float(seed)
# Lineární kombinace souřadnic s prvočíselnými vahami
for i, coord in enumerate(coords):
# Pokud už je to float32, to() je skoro no-op, ale můžeme rovnou přičítat
dt = dt + coord.to(torch.float32) * constants[i % len(constants)]
# Velmi rychlý a osvědčený hash z GPU shaderů: fract(sin(x) * 43758.5453)
st = torch.sin(dt) * 43758.5453
h = st - torch.floor(st)
# Vrátí hodnotu bezpečně v rozsahu [0, 0.9999] jako původní funkce
return h.clamp(0.0, 1.0)
@staticmethod
def perlin_noise_nd(coords_input, scale=1.0, seed=0, device=None):
"""
N-dimensional Perlin noise with proper gradient interpolation.
coords_input: tuple of coordinate tensors with same shape
scale: frequency scale
seed: random seed
Returns: noise tensor in same shape as input coordinates
"""
ndim = len(coords_input)
# Scale coordinates
coords = tuple(c / scale for c in coords_input)
# Split into integer and fractional parts
# Keep coords_floor as float32 to avoid expensive conversions to and from long
coords_floor = tuple(torch.floor(c).to(torch.float32) for c in coords)
coords_frac = tuple(c - coords_floor[d] for d, c in enumerate(coords))
# Fade curves for each dimension
fades = tuple(NoiseUtils._fade(f) for f in coords_frac)
# Accumulate weighted corner contributions
result = torch.zeros_like(coords_input[0])
prime_seeds = [1013, 1619, 3137, 5021, 7919, 10427]
for corner_idx in range(1 << ndim):
corner_offsets = tuple((corner_idx >> d) & 1 for d in range(ndim))
corner_coords = tuple(coords_floor[d] + corner_offsets[d] for d in range(ndim))
grad_components = []
for d in range(ndim):
h = NoiseUtils._hash_nd(corner_coords, seed + prime_seeds[d % len(prime_seeds)] * (d + 1))
grad_components.append(h * 2.0 - 1.0)
# Optimalizace: výpočet normy a dot produktu iterativně bez torch.stack
grad_norm_sq = grad_components[0] * grad_components[0]
for d in range(1, ndim):
grad_norm_sq += grad_components[d] * grad_components[d]
inv_norm = torch.where(grad_norm_sq == 0.0, torch.ones_like(grad_norm_sq), torch.rsqrt(grad_norm_sq))
dot = grad_components[0] * (coords_frac[0] - corner_offsets[0]) * inv_norm
for d in range(1, ndim):
dot += grad_components[d] * (coords_frac[d] - corner_offsets[d]) * inv_norm
weight = fades[0] if corner_offsets[0] else (1.0 - fades[0])
for d in range(1, ndim):
weight = weight * (fades[d] if corner_offsets[d] else (1.0 - fades[d]))
result += dot * weight
return torch.clamp(result, -1, 1)
@staticmethod
def cellular_noise_nd(coords_input, scale=1.0, jitter=0.5, seed=0, device=None):
"""
N-dimensional Cellular (Voronoi) noise - OPRAVENÁ VERZE.
Správně odečítá jednotlivé dimenze tensorů.
"""
ndim = len(coords_input)
device = coords_input[0].device
# 1. Škálování a rozdělení souřadnic
coords = tuple(c / scale for c in coords_input)
# Optimalizace: uložit jako float32 pro rychlejší indexování a hash
coords_cell = tuple(torch.floor(c).to(torch.float32) for c in coords)
# OPRAVA: Musíme indexovat konkrétní prvek z tuple pomocí `coords_cell[i]`
coords_frac = tuple(coords[i] - coords_cell[i] for i in range(ndim))
# Pro inicializaci použijeme první tensor z mřížky, abychom dodrželi správný shape
min_dist = torch.full_like(coords_input[0], float('inf'), dtype=torch.float32, device=device)
# 2. Generování všech 3^ndim offsetů přímo v PyTorchi
base_range = torch.tensor([-1.0, 0.0, 1.0], dtype=torch.float32, device=device)
offset_grids = torch.meshgrid(*[base_range] * ndim, indexing='ij')
gpu_offsets = torch.stack([g.flatten() for g in offset_grids], dim=-1)
num_neighbors = gpu_offsets.shape[0]
prime_seeds = [1013, 1619, 3137, 5021, 7919, 10427]
# 3. Plně akcelerovaný výpočet sousedství
for n in range(num_neighbors):
current_offset = gpu_offsets[n]
# Souřadnice sousední buňky složené ze všech dimenzí naráz pro správný hash
neighbor_coords = tuple(coords_cell[i] + current_offset[i] for i in range(ndim))
dist_sq = 0.0
for i in range(ndim):
# Generování unikátního seedu pro každou dimenzi aktuálního souseda
s = seed + prime_seeds[i % len(prime_seeds)] * (i + 2)
h = NoiseUtils._hash_nd(neighbor_coords, s)
# Pozice feature pointu: offset + hash * jitter
pos_i = current_offset[i] + h * jitter
# Vzdálenost pixelu od tohoto bodu
diff = coords_frac[i] - pos_i
dist_sq += diff * diff
dist = torch.sqrt(dist_sq)
min_dist = torch.minimum(min_dist, dist)
return min_dist
@staticmethod
def plasma_noise_nd(
coords_input,
scale=1.0,
seed=0,
device=None,
octaves=4,
persistence=0.5,
lacunarity=2.0,
):
"""
N-dimensional Plasma/Turbulence noise in [0, 1].
Uses Perlin octaves with automatic octave limiting to reduce aliasing.
"""
result = torch.zeros_like(coords_input[0])
amplitude = 1.0
frequency = 1.0
max_amplitude = 0.0
for octave in range(octaves):
noise_octave = NoiseUtils.perlin_noise_nd(
tuple((c / scale) * frequency for c in coords_input),
1.0,
seed + octave * 1000,
device
)
result = result + noise_octave * amplitude
max_amplitude += amplitude
amplitude *= persistence
frequency *= lacunarity
# Normalizace zpět do [-1, 1] a potom do [0, 1]
result = result / max_amplitude
return (result*2)+0.5
@staticmethod
def _accumulate_octaves_nd(sample_fn, octaves=1, persistence=0.5, lacunarity=2.0):
octaves = max(1, int(octaves))
result = None
amplitude = 1.0
frequency = 1.0
max_amplitude = 0.0
for octave in range(octaves):
value = sample_fn(frequency, octave)
if result is None:
result = torch.zeros_like(value, dtype=torch.float32)
result = result + value * amplitude
max_amplitude += amplitude
amplitude *= persistence
frequency *= lacunarity
if max_amplitude == 0.0:
return result
return result / max_amplitude
@staticmethod
def ridged_noise_nd(coords_input, scale=1.0, seed=0, device=None, octaves=4, persistence=0.5, lacunarity=2.0, ridge_power=2.0):
"""
Ridged multifractal noise in [0, 1].
"""
def sample(frequency, octave):
noise = NoiseUtils.perlin_noise_nd(
tuple((c / scale) * frequency for c in coords_input),
1.0,
seed + octave * 1000,
device,
)
ridged = (1.0 - noise.abs()).clamp(0.0, 1.0)
return ridged.pow(ridge_power)
return NoiseUtils._accumulate_octaves_nd(sample, octaves, persistence, lacunarity).clamp(0.0, 1.0)
@staticmethod
def domain_warp_noise_nd(
coords_input,
scale=1.0,
seed=0,
device=None,
warp_scale=1.0,
warp_strength=0.35,
octaves=4,
warp_octaves=2,
persistence=0.5,
lacunarity=2.0,
):
"""
Domain-warped Perlin noise in [0, 1].
"""
ndim = len(coords_input)
warp_fields = []
for dim in range(ndim):
def sample_warp(frequency, octave, dim=dim):
return NoiseUtils.perlin_noise_nd(
tuple((c / warp_scale) * frequency for c in coords_input),
1.0,
seed + 10000 + dim * 1000 + octave * 97,
device,
)
warp_fields.append(NoiseUtils._accumulate_octaves_nd(sample_warp, warp_octaves, persistence, lacunarity))
warped_coords = tuple(
(coords_input[dim] / scale) + warp_fields[dim] * warp_strength
for dim in range(ndim)
)
def sample_base(frequency, octave):
return NoiseUtils.perlin_noise_nd(
tuple(c * frequency for c in warped_coords),
1.0,
seed + 20000 + octave * 1000,
device,
)
base = NoiseUtils._accumulate_octaves_nd(sample_base, octaves, persistence, lacunarity)
return ((base + 1.0) * 0.5).clamp(0.0, 1.0)
+426
View File
@@ -0,0 +1,426 @@
import os
import torch
import torch.nn.functional as F
from torchvision.models.optical_flow import raft_large, Raft_Large_Weights
from torchvision.utils import flow_to_image as torchvision_flow_to_image
import numpy as np
_raft_model = None
def get_raft_model(device):
global _raft_model
if _raft_model is None:
# First, try to load a user-provided checkpoint from ComfyUI's configurable
# "optical_flow" folder (preferred). If that fails, fall back to the
# torchvision default weights.
try:
# Imported here to avoid adding a hard dependency at module import time
# when this utility may be used outside of a full ComfyUI environment.
import folder_paths
import comfy.utils
names = folder_paths.get_filename_list("optical_flow")
model_name = None
if names:
# Choose the first available file by default (matching other UI lists).
model_name = names[0]
else:
# Try to use torchvision weights but save them to the optical_flow folder
try:
weights = Raft_Large_Weights.DEFAULT
# Instantiate the torchvision model (this will download weights into the torchvision cache)
try:
model_dl = raft_large(weights=weights, progress=True)
except Exception:
model_dl = raft_large(weights=weights, progress=False)
# Move state dict to CPU and detach
state_dict = {k: v.cpu() for k, v in model_dl.state_dict().items()}
# Try to infer original filename/extension from the weights metadata or URL
candidate_name = None
try:
url = getattr(weights, 'url', None) or getattr(weights, '_url', None)
if url:
candidate_name = os.path.basename(str(url).split('?')[0])
except Exception:
candidate_name = None
if not candidate_name:
try:
meta = getattr(weights, 'meta', None) or {}
if isinstance(meta, dict):
candidate_name = meta.get('filename') or meta.get('name')
except Exception:
candidate_name = None
# Ensure folder exists: use folder_paths mapping when available
save_dir = None
if hasattr(folder_paths, 'folder_names_and_paths') and 'optical_flow' in folder_paths.folder_names_and_paths:
dirs = folder_paths.folder_names_and_paths['optical_flow'][0]
if isinstance(dirs, (list, tuple)) and len(dirs) > 0:
save_dir = dirs[0]
if not save_dir:
save_dir = os.path.join(os.path.dirname(__file__), "..", "..", "models", "optical_flow")
os.makedirs(save_dir, exist_ok=True)
# Decide filename and format
if candidate_name:
model_name = candidate_name
else:
model_name = "raft_large_default.pth"
model_path = folder_paths.get_full_path("optical_flow", model_name) or os.path.join(save_dir, model_name)
# Save in original format if known (.safetensors or .pth/.pt), otherwise default to .pth
_, ext = os.path.splitext(model_name)
ext = ext.lower()
saved_ok = False
try:
if ext in ('.safetensors', '.sft'):
# Save as safetensors
try:
comfy.utils.save_torch_file(state_dict, model_path)
saved_ok = True
except Exception:
import safetensors.torch
safetensors.torch.save_file(state_dict, model_path)
saved_ok = True
else:
# Default: save as a PyTorch checkpoint (.pth)
try:
import torch as _torch
_torch.save(state_dict, model_path)
saved_ok = True
except Exception:
# Fallback to comfy.utils (may write safetensors)
try:
comfy.utils.save_torch_file(state_dict, model_path)
saved_ok = True
except Exception:
saved_ok = False
except Exception:
saved_ok = False
if not saved_ok:
model_name = None
except Exception:
model_name = None
if model_name:
try:
model_path = folder_paths.get_full_path_or_raise("optical_flow", model_name)
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
# Basic validation for a RAFT-large state dict
has_raft_keys = (
any(k.startswith("feature_encoder.") for k in sd)
and any(k.startswith("context_encoder.") for k in sd)
and any(k.startswith("update_block.") for k in sd)
)
if has_raft_keys:
model = raft_large(weights=None, progress=False)
model.load_state_dict(sd)
_raft_model = model.to(device).eval()
return _raft_model
except Exception:
pass
except Exception:
# If anything goes wrong with folder resolution / loading, fall back below.
pass
# Fallback to torchvision-provided default weights
weights = Raft_Large_Weights.DEFAULT
_raft_model = raft_large(weights=weights, progress=False).to(device).eval()
return _raft_model
def warp(x, flow, padding_mode='reflection'):
"""
Warps an image or tensor using the given flow.
x: [B, C, H, W]
flow: [B, 2, H, W] (flow in pixels)
padding_mode: passed to F.grid_sample (e.g. 'zeros' for occlusion detection)
"""
b, c, h, w = x.shape
device = flow.device # Use flow device as the master device
x = x.to(device)
# Create grid
grid_y, grid_x = torch.meshgrid(
torch.linspace(0, h - 1, h, device=device),
torch.linspace(0, w - 1, w, device=device),
indexing='ij'
)
# [H, W]
grid = torch.stack((grid_x, grid_y), dim=0).unsqueeze(0).repeat(b, 1, 1, 1) # [B, 2, H, W]
# Add flow to grid
v_grid = grid + flow # [B, 2, H, W]
# Normalize to [-1, 1] for grid_sample
v_grid_x = 2.0 * v_grid[:, 0, :, :] / max(w - 1, 1) - 1.0
v_grid_y = 2.0 * v_grid[:, 1, :, :] / max(h - 1, 1) - 1.0
v_grid = torch.stack((v_grid_x, v_grid_y), dim=3) # [B, H, W, 2] with (x, y)
return F.grid_sample(x, v_grid, mode='bilinear', padding_mode=padding_mode, align_corners=True)
def preprocess_image(img, device):
"""
img: [B, H, W, C] in range [0, 1]
Returns: [B, 3, H, W] normalized for RAFT
"""
if img.ndim == 3:
img = img.unsqueeze(0)
# ComfyUI [B, H, W, C] -> Torch [B, C, H, W]
img = img.movedim(-1, 1)
# Ensure RGB
if img.shape[1] == 1:
img = img.expand(-1, 3, -1, -1)
elif img.shape[1] > 3:
img = img[:, :3, :, :]
# Scale to [0, 255] as RAFT transforms usually expect this
img = img * 255.0
# The transform expects [0, 255] and returns normalized [-1, 1]
# It takes (img1, img2) but we can use it for one or just follow its logic
# Actually, let's just follow the logic: 2 * (img / 255.0) - 1.0
# Wait, if I do that, it's just 2 * img_01 - 1.0.
return (2.0 * (img / 255.0) - 1.0).to(device)
def get_optical_flow(img1, img2, tiling_size=0, iterations=12, multi_scale=False):
"""
img1, img2: [H, W, C] or [B, H, W, C]
Returns: [B, H, W, 2] flow vectors
"""
device = img1.device
model = get_raft_model(device)
img1_proc = preprocess_image(img1, device)
img2_proc = preprocess_image(img2, device)
b, _, h, w = img1_proc.shape
# Auto-tiling for large images to prevent OOM
# 2MPx is a reasonable limit for 8GB VRAM without tiling
if tiling_size <= 0 and (h * w) > 2000000:
tiling_size = 1024
# Handle fraction or float conversion
if 0 < tiling_size < 1:
tiling_size = int(min(h, w) * tiling_size)
else:
tiling_size = int(tiling_size)
# Ensure divisible by 8 for RAFT
h_new, w_new = (h // 8) * 8, (w // 8) * 8
if h != h_new or w != w_new:
img1_proc = F.interpolate(img1_proc, size=(h_new, w_new), mode='bilinear', align_corners=False)
img2_proc = F.interpolate(img2_proc, size=(h_new, w_new), mode='bilinear', align_corners=False)
# Process sequentially to save memory
flows = []
with torch.no_grad():
for i in range(b):
p1 = img1_proc[i:i+1]
p2 = img2_proc[i:i+1]
global_flow = None
if multi_scale:
# 1. Global Pass
# Downsample more aggressively to capture massive jumps (max 256)
g_size = 256
g_h = (min(h_new, g_size) // 8) * 8
g_w = (min(w_new, g_size) // 8) * 8
gp1 = F.interpolate(p1, size=(g_h, g_w), mode='bilinear', align_corners=False)
gp2 = F.interpolate(p2, size=(g_h, g_w), mode='bilinear', align_corners=False)
# Global flow estimation
global_flow = model(gp1, gp2, num_flow_updates=iterations)[-1]
# Upsample global flow to high-res
global_flow = F.interpolate(global_flow, size=(h_new, w_new), mode='bilinear', align_corners=False)
global_flow[:, 0] *= float(w_new) / g_w
global_flow[:, 1] *= float(h_new) / g_h
# print(f"DEBUG: Global flow at center: {global_flow[0, :, h_new//2, w_new//2]}")
# 2. Warp img2 using global flow
p2_warped = warp(p2, global_flow)
target_p2 = p2_warped
else:
target_p2 = p2
# 3. Local Refinement (Tiled or Full)
if tiling_size > 0:
f_residual = _tiled_flow(model, p1, target_p2, tiling_size, iterations)
else:
f_residual = model(p1, target_p2, num_flow_updates=iterations)[-1]
# 4. Combine
if global_flow is not None:
total_flow = global_flow + f_residual
else:
total_flow = f_residual
flows.append(total_flow)
if i % 4 == 0 and b > 4:
torch.cuda.empty_cache()
flow = torch.cat(flows, dim=0)
# Resize flow back if needed
if h != h_new or w != w_new:
flow = F.interpolate(flow, size=(h, w), mode='bilinear', align_corners=False)
# Rescale flow values
flow[:, 0] *= float(w) / w_new
flow[:, 1] *= float(h) / h_new
# [B, 2, H, W] -> [B, H, W, 2]
return flow.movedim(1, -1)
def calculate_occlusion_mask(flow):
"""
Calculates a disocclusion mask: pixels revealed by motion that have no
valid source data (e.g. background uncovered when an object moves away).
flow: [B, H, W, 2] or [H, W, 2]
Returns: [B, H, W] mask (1.0 = disoccluded/revealed, 0.0 = valid)
"""
was_single = (flow.ndim == 3)
if was_single:
flow = flow.unsqueeze(0)
# sanitize flow
flow = torch.nan_to_num(flow, nan=0.0, posinf=0.0, neginf=0.0)
b, h, w, _ = flow.shape
device = flow.device
# [B, H, W, 2] -> [B, 2, H, W] for warp()
flow_nchw = flow.movedim(-1, 1)
# Warp a solid-ones image with the flow.
# Use zero padding so samples from outside are 0, making disocclusions visible
ones = torch.ones((b, 1, h, w), device=device)
warped_ones = warp(ones, flow_nchw, padding_mode='zeros') # [B, 1, H, W]
# Pixels that are < threshold are disoccluded
mask = 1.0 - warped_ones.squeeze(1).clamp(0.0, 1.0) # [B, H, W]
return mask.squeeze(0) if was_single else mask
def apply_flow(image, flow):
"""
Warps an image using optical flow vectors.
image: [B, H, W, C] (ComfyUI format)
flow: [B, H, W, 2] (from rife())
Returns: [B, H, W, C] warped image
"""
if image.ndim == 3:
image = image.unsqueeze(0)
if flow.ndim == 3:
flow = flow.unsqueeze(0)
# [B, H, W, 2] -> [B, 2, H, W]
flow_nchw = flow.movedim(-1, 1)
# Ensure image is on the same device as flow
img_nchw = image.movedim(-1, 1).to(flow.device)
warped = warp(img_nchw, flow_nchw)
# [B, C, H, W] -> [B, H, W, C]
return warped.movedim(1, -1)
def flow_to_image(flow):
"""
Converts flow vectors [B, H, W, 2] to RGB images [B, H, W, 3] for visualization.
"""
if flow.ndim == 3:
flow = flow.unsqueeze(0)
# [B, H, W, 2] -> [B, 2, H, W]
flow_torch = flow.movedim(-1, 1)
# torchvision_flow_to_image expects (N, 2, H, W)
img_uint8 = torchvision_flow_to_image(flow_torch)
# [B, 3, H, W] -> [B, H, W, 3]
img = img_uint8.movedim(1, -1).float() / 255.0
return img
def _tiled_flow(model, img1, img2, tiling_size, iterations):
"""
Robust tiling implementation with weighted blending and edge coverage.
"""
b, c, h, w = img1.shape
device = img1.device
# Enforce minimum tile size for RAFT
if tiling_size < 256:
# If user provided a very small value, maybe they meant fraction?
# But for ComfyUI, we'll just enforce a safe minimum.
tiling_size = max(256, tiling_size)
# Ensure tiling_size is divisible by 8
tiling_size = (tiling_size // 8) * 8
# Clip tiling_size to image dimensions
t_h = int(min(tiling_size, h))
t_w = int(min(tiling_size, w))
# If image is smaller than tiling_size after divisibility adjustments
if t_h < 8 or t_w < 8:
return model(img1, img2, num_flow_updates=iterations)[-1]
output_flow = torch.zeros((b, 2, h, w), device=device)
weights = torch.zeros((b, 1, h, w), device=device)
# Stride of 50% overlap is usually good
stride_h = int(t_h // 2)
stride_w = int(t_w // 2)
# Generate coordinates for tiles
y_coords = list(range(0, h - t_h, stride_h)) + [max(0, h - t_h)]
x_coords = list(range(0, w - t_w, stride_w)) + [max(0, w - t_w)]
# Remove duplicates if any (when image size matches stride)
y_coords = sorted(list(set(y_coords)))
x_coords = sorted(list(set(x_coords)))
for y in y_coords:
for x in x_coords:
patch1 = img1[:, :, y:y+t_h, x:x+t_w]
patch2 = img2[:, :, y:y+t_h, x:x+t_w]
# RAFT expects inputs divisible by 8, but we already ensured t_h, t_w are.
patch_flow = model(patch1, patch2, num_flow_updates=iterations)[-1]
# Create local mask for this tile
# Linear tapering only on overlapping edges
layer_mask = torch.ones((1, 1, t_h, t_w), device=device)
overlap_h = stride_h
overlap_w = stride_w
if y > 0: # Fade in top
layer_mask[:, :, :overlap_h, :] *= torch.linspace(0, 1, overlap_h, device=device).view(1, 1, overlap_h, 1)
if y + t_h < h: # Fade out bottom
layer_mask[:, :, -overlap_h:, :] *= torch.linspace(1, 0, overlap_h, device=device).view(1, 1, overlap_h, 1)
if x > 0: # Fade in left
layer_mask[:, :, :, :overlap_w] *= torch.linspace(0, 1, overlap_w, device=device).view(1, 1, 1, overlap_w)
if x + t_w < w: # Fade out right
layer_mask[:, :, :, -overlap_w:] *= torch.linspace(1, 0, overlap_w, device=device).view(1, 1, 1, overlap_w)
output_flow[:, :, y:y+t_h, x:x+t_w] += patch_flow * layer_mask
weights[:, :, y:y+t_h, x:x+t_w] += layer_mask
return output_flow / torch.clamp(weights, min=1e-6)
+25
View File
@@ -0,0 +1,25 @@
from comfy_api.latest import io
from .helper_functions import parse_expr
from .ParseTree import MrmthParseTree
class ScriptTextInput(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ScriptInput",
category="More math",
display_name="Script input",
inputs=[
io.String.Input(id="script", multiline=True),
],
outputs=[
MrmthParseTree.Output()
],
)
@classmethod
def execute(cls, script):
expr = parse_expr(script)
return (expr,)
+685
View File
@@ -0,0 +1,685 @@
import copy
import torch
import comfy.model_patcher
from comfy_api.latest import io
import comfy.hooks
import logging
import comfy.patcher_extension
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from .helper_functions import checkLazyNew, generate_dim_variables, parse_expr, as_tensor
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
class SelectiveGuiderMathNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_SelectiveGuiderMathNode",
category="More math/math",
display_name="Selective guider math",
inputs=[
io.Guider.Input("V"),
io.Autogrow.Input(
id="F",
template=io.Autogrow.TemplatePrefix(
io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True),
prefix="F",
min=1,
max=50,
),
),
io.MultiType.Input(
io.String.Input("Expression", default="print([layer, layer_key, hook_kind, cond_side, activations_shape]); inp", multiline=False),
types=[io.String, MrmthParseTree],
),
io.Int.Input("layer_x", default=0, min=-999, max=999),
io.Combo.Input(
"hook_target",
options=["all", "dit_block", "unet_block", "attn1", "attn2", "double_block_attn", "single_block_attn", "model_begin", "model_end"],
default="all"
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes", optional=True),
io.Boolean.Input(id="remember_stack", default=False, display_name="Remember stack across batch"),
io.Boolean.Input(id="debug_hooks", default=False, display_name="Debug hook logs"),
],
outputs=[io.Guider.Output(), MrmthStack.Output()],
)
@classmethod
def check_lazy_status(
cls,
V,
F,
Expression,
layer_x=0,
hook_target="all",
remember_stack=False,
debug_hooks=False,
stack={},
):
return checkLazyNew(Expression, {}, F)
@classmethod
def execute(
cls,
V,
F,
Expression,
layer_x=0,
hook_target="all",
remember_stack=False,
debug_hooks=False,
stack=None,
):
if V is None or not hasattr(V, "model_options"):
raise ValueError("Vstupní guider musí mít model_options.")
stack = stack if remember_stack else (copy.deepcopy(stack) if stack is not None else {})
tree = parse_expr(Expression) if isinstance(Expression, str) else Expression
hit_flags = {
"attn1": False,
"attn2": False,
"double_block_attn": False,
"single_block_attn": False,
"dit": False,
"attn_unknown": False,
"unet": False,
}
debug_hook_events = []
def dbg(msg: str):
if debug_hooks:
logging.warning(f"[SelectiveGuiderMathNode] {msg}")
def record_hook(kind: str, side: str, layer_id=None, stage=None, key=None):
if not debug_hooks:
return
parts = [f"hook={kind}", f"side={side}"]
if stage is not None:
parts.append(f"stage={stage}")
if layer_id is not None:
parts.append(f"layer_id={layer_id}")
if key is not None:
parts.append(f"key={key}")
line = " ".join(parts)
debug_hook_events.append(line)
dbg(f"HOOK {line}")
def resolve_attn_kind(transformer_options: dict, q: torch.Tensor | None = None, k: torch.Tensor | None = None, v: torch.Tensor | None = None) -> str:
# Flux / MM-DiT
btype = transformer_options.get("block_type", None)
if btype == "double":
return "double_block_attn"
if btype == "single":
return "single_block_attn"
raw = transformer_options.get(
"attn_name",
transformer_options.get(
"attn_type",
transformer_options.get(
"block_attn",
transformer_options.get("transformer_index", None),
),
),
)
blk = transformer_options.get("block", None)
if isinstance(blk, (tuple, list)) and len(blk) > 0:
blk_name = str(blk[0]).lower()
if "self" in blk_name or "attn1" in blk_name:
return "attn1"
if "cross" in blk_name or "attn2" in blk_name:
return "attn2"
if isinstance(raw, str):
r = raw.lower()
if r in ("attn1", "self", "self_attn", "self_attention", "self_attn_1"):
return "attn1"
if r in ("attn2", "cross", "cross_attn", "cross_attention", "self_attn_2"):
return "attn2"
return "attn_unknown"
if isinstance(raw, (int, float)):
if int(raw) == 0:
return "attn1"
if int(raw) == 1:
return "attn2"
# Bezpečný fallback podle délek tokenů (funguje i pro FLOW self-attn)
if isinstance(q, torch.Tensor) and isinstance(k, torch.Tensor):
if q.ndim >= 2 and k.ndim >= 2:
q_tokens = int(q.shape[-2])
k_tokens = int(k.shape[-2])
if q_tokens == k_tokens:
return "attn1"
return "attn2"
return "attn_unknown"
def resolve_x(total_blocks: int | None) -> int:
x = int(layer_x)
if x < 0 and total_blocks is not None:
x = total_blocks + x
if total_blocks is not None:
x = max(0, min(x, total_blocks - 1))
return x
def match_index(idx: int, total_blocks: int | None, stage: str, topts: dict, hook_kind: str) -> bool:
x = resolve_x(total_blocks)
return idx == x
def target_indices():
return [int(layer_x)]
def side_from_cond_or_uncond(cond_or_uncond):
if isinstance(cond_or_uncond, (list, tuple)) and len(cond_or_uncond) > 0:
vals = list(cond_or_uncond)
uniq = set(vals)
if len(uniq) == 1:
idx = int(vals[0])
if idx == 0:
return ("positive", 0)
if idx == 1:
return ("negative", 1)
return ("mixed", -1)
return ("unknown", -1)
def run_expr(inp: torch.Tensor, extra_vars: dict | None = None) -> torch.Tensor:
# v run_expr(...) rozšiř výchozí proměnné:
variables = {
"inp": inp,
"sample": inp,
"hook_kind": "unknown",
"hook_domain": "unknown",
"attn_kind": "none",
"attn_mode": "unknown",
"transformer_index": -1.0,
"is_dit": 0.0,
"is_unet_block": 0.0,
"is_time_emb": 0.0,
"is_attn1": 0.0,
"is_attn2": 0.0,
"is_self_attention": 0.0,
"is_cross_attention": 0.0,
"has_context": 0.0,
"block_name": "",
"layer_key": "",
"layer_id": -1.0,
"layer": -1.0,
"i": -1.0,
"total_blocks": -1.0,
"has_qkv": 0.0,
"q": inp,
"k": inp,
"v": inp,
"heads": 0.0,
"dim_head": 0.0,
"query_tokens": -1.0,
"context_tokens": -1.0,
"value_tokens": -1.0,
"activations_shape": [],
"activation_rank": -1.0,
"activation_b": -1.0,
"activation_c": -1.0,
"activation_t": -1.0,
"activation_h": -1.0,
"activation_w": -1.0,
"cond_side": "unknown",
"cond_index": -1.0,
"is_positive": 0.0,
"is_negative": 0.0,
"is_attn1_hook": 0.0,
"is_attn2_hook": 0.0,
"is_double_block_attn_hook": 0.0,
"is_single_block_attn_hook": 0.0,
"attention_relation": "unknown",
} | generate_dim_variables(inp)
if extra_vars:
variables.update(extra_vars)
for kf, vf in F.items():
variables[kf] = vf if vf is not None else 0.0
act_shape = variables.get("activations_shape", [])
if isinstance(act_shape, (list, tuple)):
variables["activation_rank"] = float(len(act_shape))
if len(act_shape) >= 1:
variables["activation_b"] = float(act_shape[0])
if len(act_shape) >= 2:
variables["activation_c"] = float(act_shape[1])
if len(act_shape) == 4:
variables["activation_h"] = float(act_shape[2])
variables["activation_w"] = float(act_shape[3])
elif len(act_shape) >= 5:
variables["activation_t"] = float(act_shape[2])
variables["activation_h"] = float(act_shape[3])
variables["activation_w"] = float(act_shape[4])
qv = variables.get("q", inp)
kv = variables.get("k", inp)
vv = variables.get("v", inp)
if variables.get("attn_kind") == "attn1":
variables["is_attn1_hook"] = 1.0
elif variables.get("attn_kind") == "attn2":
variables["is_attn2_hook"] = 1.0
elif variables.get("attn_kind") == "double_block_attn":
variables["is_double_block_attn_hook"] = 1.0
elif variables.get("attn_kind") == "single_block_attn":
variables["is_single_block_attn_hook"] = 1.0
if hasattr(qv, "shape") and len(qv.shape) >= 2:
variables["query_tokens"] = float(qv.shape[-2])
if hasattr(kv, "shape") and len(kv.shape) >= 2:
variables["context_tokens"] = float(kv.shape[-2])
variables["has_context"] = 1.0
if hasattr(vv, "shape") and len(vv.shape) >= 2:
variables["value_tokens"] = float(vv.shape[-2])
qt = variables["query_tokens"]
kt = variables["context_tokens"]
if variables["has_context"] == 1.0 and qt > 0 and kt > 0:
if qt != kt:
variables["is_cross_attention"] = 1.0
variables["is_self_attention"] = 0.0
variables["attention_relation"] = "cross"
else:
variables["is_cross_attention"] = 0.0
variables["is_self_attention"] = 1.0
variables["attention_relation"] = "self"
else:
variables["is_cross_attention"] = 0.0
variables["is_self_attention"] = 0.0
variables["attention_relation"] = "unknown"
visitor = UnifiedMathVisitor(variables, inp.shape, inp.device, state_storage=stack)
out = visitor.visit(tree)
# preserve dtype/device (float8-safe vs float16 promotion side effects)
return as_tensor(out, inp.shape).to(device=inp.device, dtype=inp.dtype)
def register_dit(patched_dict, forced_side=None):
topts = patched_dict["transformer_options"]
def dit_wrap(args, extra_args):
idx = int(args.get("transformer_options", {}).get("block_index", -1))
total_blocks = args.get("transformer_options", {}).get("total_blocks", None)
if not match_index(idx, total_blocks, "dit", args.get("transformer_options", {}), "dit_block"):
return extra_args["original_block"](args)
if not hit_flags["dit"]:
dbg(f"DIT HIT idx={idx} total={total_blocks}")
hit_flags["dit"] = True
btype = args.get("transformer_options", {}).get("block_type", "dit")
co = args.get("transformer_options", {}).get("cond_or_uncond", None)
auto_side, auto_idx = side_from_cond_or_uncond(co)
side = forced_side if forced_side is not None else auto_side
idx_side = 0 if side == "positive" else (1 if side == "negative" else auto_idx)
meta = {
"hook_kind": "dit_block",
"hook_domain": "diffusion",
"attn_kind": "none",
"is_dit": 1.0,
"is_attn1": 0.0,
"is_attn2": 0.0,
"layer_id": float(idx),
"layer": float(idx),
"i": float(idx),
"total_blocks": float(total_blocks) if total_blocks is not None else -1.0,
"block_name": btype,
"layer_key": f"dit.{btype}.{idx}",
"has_qkv": 0.0,
"cond_side": side,
"cond_index": float(idx_side),
"is_positive": 1.0 if side == "positive" else 0.0,
"is_negative": 1.0 if side == "negative" else 0.0,
"activations_shape": tensor_shape(out["img"]) if isinstance(out, dict) and "img" in out else [],
}
out = extra_args["original_block"](args)
if isinstance(out, dict) and "img" in out:
out2 = dict(out)
out2["img"] = run_expr(out["img"], meta)
return out2
return out
patches_replace = topts.get("patches_replace", {}).copy()
dit_map = patches_replace.get("dit", {}).copy()
for li in target_indices():
dit_map[("double_block", li)] = dit_wrap
dit_map[("single_block", li)] = dit_wrap
patches_replace["dit"] = dit_map
topts["patches_replace"] = patches_replace
patched_dict["transformer_options"] = topts
def register_attn(patched_dict, forced_side=None):
topts = patched_dict["transformer_options"].copy()
def attn_override(original_attn, q, k, v, heads, **kwargs):
transformer_options = kwargs.get("transformer_options", {})
attn_kind = resolve_attn_kind(transformer_options, q=q, k=k, v=v)
blk = transformer_options.get("block", None)
btype = str(transformer_options.get("block_type", "unknown"))
bindex = int(transformer_options.get("block_index", -1))
if isinstance(blk, (tuple, list)) and len(blk) >= 2:
stage = str(blk[0])
layer_id = int(blk[1])
else:
# lepší fallback pro Flux/FLOW
stage = btype if btype in ("double", "single") else str(transformer_options.get("model_type", "unknown")).lower()
layer_id = bindex
t_index_raw = transformer_options.get("transformer_index", None)
t_index = int(t_index_raw) if isinstance(t_index_raw, (int, float)) else layer_id
if hook_target != "all" and hook_target in ("attn1", "attn2", "double_block_attn", "single_block_attn"):
if hook_target != attn_kind:
return original_attn(q, k, v, heads, **kwargs)
idx = layer_id
total_blocks = None
if not match_index(idx, total_blocks, stage, transformer_options, attn_kind):
return original_attn(q, k, v, heads, **kwargs)
co = transformer_options.get("cond_or_uncond", None)
auto_side, auto_idx = side_from_cond_or_uncond(co)
side = forced_side if forced_side is not None else auto_side
if not hit_flags.get(attn_kind, False):
dbg(f"{attn_kind.upper()} HIT stage={stage} layer_id={layer_id} side={side}")
hit_flags[attn_kind] = True
act_shape = transformer_options.get("activations_shape", [])
if not isinstance(act_shape, (list, tuple)):
act_shape = []
meta = {
"hook_kind": attn_kind,
"hook_domain": "attention",
"attn_kind": attn_kind,
"transformer_index": float(t_index),
"is_dit": 0.0,
"is_attn1": 1.0 if attn_kind == "attn1" else 0.0,
"is_attn2": 1.0 if attn_kind == "attn2" else 0.0,
"block_name": stage,
"layer_id": float(layer_id),
"layer": float(layer_id),
"i": float(layer_id),
"total_blocks": -1.0,
"has_qkv": 1.0,
"q": q,
"k": k,
"v": v,
"heads": float(heads),
"dim_head": float(q.shape[-1] // heads) if heads > 0 else 0.0, # <- oprava
"layer_key": f"{stage}.{layer_id}.{attn_kind}.{t_index}",
"cond_side": side,
"cond_index": float(0 if side == "positive" else (1 if side == "negative" else auto_idx)),
"is_positive": 1.0 if side == "positive" else 0.0,
"is_negative": 1.0 if side == "negative" else 0.0,
} | {
"activations_shape": list(act_shape),
"activation_b": float(act_shape[0]) if len(act_shape) > 0 else -1.0,
"activation_c": float(act_shape[1]) if len(act_shape) > 1 else -1.0,
"activation_h": float(act_shape[2]) if len(act_shape) > 2 else -1.0,
"activation_w": float(act_shape[3]) if len(act_shape) > 3 else -1.0,
}
out = original_attn(q, k, v, heads, **kwargs)
return run_expr(out, meta)
topts["optimized_attention_override"] = attn_override
patched_dict["transformer_options"] = topts
return patched_dict
def register_unet_blocks(patched_dict, forced_side=None):
topts = patched_dict["transformer_options"].copy()
patches = topts.get("patches", {}).copy()
def _parse_block(transformer_options: dict, fallback_stage: str):
blk = transformer_options.get("block", None)
if isinstance(blk, (tuple, list)) and len(blk) >= 2:
return str(blk[0]), int(blk[1])
return fallback_stage, int(transformer_options.get("block_index", -1))
def _apply_block_expr(t: torch.Tensor, transformer_options: dict, stage: str, idx: int):
if not match_index(idx, None, stage, transformer_options, "unet_block"):
return t
co = transformer_options.get("cond_or_uncond", None)
auto_side, auto_idx = side_from_cond_or_uncond(co)
side = forced_side if forced_side is not None else auto_side
idx_side = 0 if side == "positive" else (1 if side == "negative" else auto_idx)
if not hit_flags["unet"]:
dbg(f"UNET BLOCK HIT stage={stage} layer_id={idx} side={side}")
hit_flags["unet"] = True
meta = {
"hook_kind": "unet_block",
"hook_domain": "diffusion",
"attn_kind": "none",
"is_dit": 0.0,
"is_unet_block": 1.0,
"is_time_emb": 1.0 if stage == "time_emb" else 0.0,
"is_attn1": 0.0,
"is_attn2": 0.0,
"block_name": stage,
"layer_id": float(idx),
"layer": float(idx),
"i": float(idx),
"total_blocks": -1.0,
"has_qkv": 0.0,
"layer_key": f"unet.{stage}.{idx}",
"cond_side": side,
"cond_index": float(idx_side),
"is_positive": 1.0 if side == "positive" else 0.0,
"is_negative": 1.0 if side == "negative" else 0.0,
"activations_shape": tensor_shape(t),
}
return run_expr(t, meta)
def input_block_patch(h, transformer_options):
stage, idx = _parse_block(transformer_options, "input")
return _apply_block_expr(h, transformer_options, stage, idx)
def output_block_patch(h, hsp, transformer_options):
stage, idx = _parse_block(transformer_options, "output")
h2 = _apply_block_expr(h, transformer_options, stage, idx)
return h2, hsp
def middle_patch(h, transformer_options):
stage, idx = _parse_block(transformer_options, "middle")
return _apply_block_expr(h, transformer_options, stage, idx)
def emb_patch(emb, model_channels, transformer_options):
# začátek timestep pipeline
return _apply_block_expr(emb, transformer_options, "time_emb", 0)
patches["input_block_patch"] = list(patches.get("input_block_patch", [])) + [input_block_patch]
patches["output_block_patch"] = list(patches.get("output_block_patch", [])) + [output_block_patch]
patches["middle_patch"] = list(patches.get("middle_patch", [])) + [middle_patch]
patches["emb_patch"] = list(patches.get("emb_patch", [])) + [emb_patch]
patches["middle_block_after_patch"] = list(patches.get("middle_block_after_patch", [])) + [middle_patch]
topts["patches"] = patches
patched_dict["transformer_options"] = topts
return patched_dict
def register_model_edges(patched_dict, forced_side=None):
topts = patched_dict["transformer_options"].copy()
wrappers = topts.get("wrappers", {}).copy()
wtype = comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL
bucket = wrappers.get(wtype, {}).copy()
key = "mrmth_model_edges"
def model_edges_wrapper(executor, x, *args, **kwargs):
if hook_target in ("all", "model_begin", "unet_block"):
pre_meta = {
"hook_kind": "model_begin",
"hook_domain": "diffusion",
"attn_kind": "none",
"block_name": "model",
"layer_id": -1.0,
"layer": -1.0,
"i": -1.0,
"total_blocks": -1.0,
"has_qkv": 0.0,
"layer_key": "model.begin",
"activations_shape": tensor_shape(x),
}
xin = run_expr(x, pre_meta) if isinstance(x, torch.Tensor) else x
else:
xin = x
out = executor(xin, *args, **kwargs)
if isinstance(out, torch.Tensor) and hook_target in ("all", "model_end", "unet_block"):
post_meta = {
"hook_kind": "model_end",
"hook_domain": "diffusion",
"attn_kind": "none",
"block_name": "model",
"layer_id": -1.0,
"layer": -1.0,
"i": -1.0,
"total_blocks": -1.0,
"has_qkv": 0.0,
"layer_key": "model.end",
"activations_shape": tensor_shape(out),
}
return run_expr(out, post_meta)
return out
bucket[key] = list(bucket.get(key, [])) + [model_edges_wrapper]
wrappers[wtype] = bucket
topts["wrappers"] = wrappers
patched_dict["transformer_options"] = topts
return patched_dict
def build_transformers_for_side(side_label=None):
p = comfy.model_patcher.create_model_options_clone(V.model_options)
p.setdefault("transformer_options", {})
if hook_target in ("all", "dit_block"):
register_dit(p, forced_side=side_label)
if hook_target in ("all", "unet_block"):
p = register_unet_blocks(p, forced_side=side_label)
if hook_target in ("all", "attn1", "attn2", "double_block_attn", "single_block_attn"):
p = register_attn(p, forced_side=side_label)
if hook_target in ("all", "unet_block", "model_begin", "model_end"):
p = register_model_edges(p, forced_side=side_label)
return p["transformer_options"]
def attach_side_hook(side_name: str):
if not hasattr(V, "original_conds") or side_name not in V.original_conds:
return
tdict = build_transformers_for_side(side_name)
hook = comfy.hooks.TransformerOptionsHook(
transformers_dict=tdict,
hook_scope=comfy.hooks.EnumHookScope.HookedOnly,
)
hg = comfy.hooks.HookGroup()
hg.add(hook)
new_conds = []
for c in V.original_conds[side_name]:
cc = c.copy()
old = cc.get("hooks", None)
cc["hooks"] = comfy.hooks.HookGroup.combine_all_hooks([old, hg])
new_conds.append(cc)
V.original_conds[side_name] = new_conds
# po parse_expr(...)
if hasattr(V, "original_conds"):
def make_shallow_cond_copy(conds_dict):
new_dict = {}
for k, v_list in conds_dict.items():
# Kondice obvykle obsahují fragmentová pole (slovníky nebo listy)
new_dict[k] = [item.copy() if hasattr(item, "copy") else copy.copy(item) for item in v_list]
return new_dict
if not hasattr(V, "_mrmth_base_original_conds"):
V._mrmth_base_original_conds = make_shallow_cond_copy(V.original_conds)
else:
V.original_conds = make_shallow_cond_copy(V._mrmth_base_original_conds)
if hasattr(V, "original_conds"):
attach_side_hook("positive")
attach_side_hook("negative")
dbg(f"has original_conds: {list(V.original_conds.keys())}")
for side in ("positive", "negative"):
if side in V.original_conds:
cnt = sum(1 for c in V.original_conds[side] if c.get("hooks", None) is not None)
dbg(f"side={side} conds_with_hooks={cnt}/{len(V.original_conds[side])}")
else:
patched = comfy.model_patcher.create_model_options_clone(V.model_options)
patched.setdefault("transformer_options", {})
if hook_target in ("all", "dit_block"):
register_dit(patched, forced_side=None)
if hook_target in ("all", "unet_block"):
patched = register_unet_blocks(patched, forced_side=None)
if hook_target in ("all", "attn1", "attn2", "double_block_attn", "single_block_attn"):
patched = register_attn(patched, forced_side=None)
if hook_target in ("all", "unet_block", "model_begin", "model_end"):
patched = register_model_edges(patched, forced_side=None)
V.model_options = patched
if hasattr(V, "model_patcher") and V.model_patcher is not None and not getattr(V, "_mrmth_debug_callbacks_added", False):
def _on_register_all_hook_patches(model_patcher, hooks, target_dict, model_options, registered):
try:
reg_len = len(registered) if registered is not None else 0
in_len = len(hooks) if hooks is not None else 0
tr_count = len(registered.get_type(comfy.hooks.EnumHookType.TransformerOptions)) if registered is not None else 0
dbg(f"REGISTER hooks_in={in_len} registered={reg_len} transformer_registered={tr_count}")
if model_options is not None:
rh = model_options.get("registered_hooks", None)
dbg(f"model_options.registered_hooks={'set' if rh is not None else 'None'}")
except Exception as e:
logging.warning(f"[SelectiveGuiderMathNode] REGISTER debug error: {e}")
def _on_apply_hooks(model_patcher, hooks):
try:
hl = len(hooks) if hooks is not None else 0
dbg(f"APPLY hooks_len={hl}")
except Exception as e:
logging.warning(f"[SelectiveGuiderMathNode] APPLY debug error: {e}")
V.model_patcher.add_callback_with_key(
comfy.patcher_extension.CallbacksMP.ON_REGISTER_ALL_HOOK_PATCHES,
"mrmth_selective_debug",
_on_register_all_hook_patches,
)
V.model_patcher.add_callback_with_key(
comfy.patcher_extension.CallbacksMP.ON_APPLY_HOOKS,
"mrmth_selective_debug",
_on_apply_hooks,
)
V._mrmth_debug_callbacks_added = True
stack = stack if remember_stack else copy.deepcopy(stack)
result = (V, stack)
# 6) Before return add compact summary:
if debug_hooks:
uniq = sorted(set(debug_hook_events))
dbg(f"HOOK SUMMARY total={len(debug_hook_events)} unique={len(uniq)}")
for item in uniq:
dbg(f"HOOK SUMMARY ITEM {item}")
return result
def tensor_shape(x):
return list(x.shape) if isinstance(x, torch.Tensor) else []
+113
View File
@@ -0,0 +1,113 @@
from .helper_functions import generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, prepare_inputs, make_zero_like, normalize_to_common_shape, checkLazyNew
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
class SigmasMathNode(io.ComfyNode):
"""
Enables math expressions on Images with autogrow support.
Inputs:
I: Autogrow image inputs (I0, I1, ...)
F: Autogrow float inputs (F0, F1, ...)
Image: Expression
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_SigmasMathNode",
category="More math/math",
display_name="Sigmas math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Sigmas.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on input images",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Sigmas.Output(),
MrmthStack.Output(),
],
)
@classmethod
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile",stack={}):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, length_mismatch="tile",stack={}):
# I and F are Autogrow.Type which is dict[str, Any]
# Determine reference image for zero-initialization (fallback for a,b,c,d)
ref_image = None
for img in V.values():
if img is not None:
ref_image = img
break
if ref_image is None:
raise ValueError("At least one input is required.")
stack = copy.deepcopy(stack) if stack is not None else {}
a = V.get("V0")
b = V.get("V1")
c = V.get("V2")
d = V.get("V3")
if a is None:
a = make_zero_like(ref_image)
ae, be, ce, de = prepare_inputs(a, b, c, d)
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
if(length_mismatch == "error"):
max_length = ae.shape[0]
for name, tensor in V.items():
if tensor is not None and tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"T": ae.shape[0],
"batch_count": ae.shape[0],
} | generate_dim_variables(ae)
# Add all dynamic inputs
for k, v in V.items():
if v is not None:
# Normalize all images in I to match ae.shape
norm_v = normalize_to_common_shape(ae, v, mode=length_mismatch)[1]
variables[k] = norm_v
for k, v in F.items():
variables[k] = v if v is not None else 0.0
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, ae.shape,ae.device,state_storage=stack)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
return (result,stack)
+53
View File
@@ -0,0 +1,53 @@
import torch
from comfy_api.latest import io
windows = {"bartlett": torch.bartlett_window, "blackman": torch.blackman_window, "hamming": torch.hamming_window, "hann": torch.hann_window, "kaiser": torch.kaiser_window}
class SpectrogramToAudio(io.ComfyNode):
"""
Converts an Image spectrogram back to Audio.
Red is real part and blue is imaginary. Green is ignored.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ImageSpectrogramToAudio",
category="More math/converters",
display_name="Spectrogram -> Audio",
inputs=[
io.Image.Input(id="image", tooltip="Input spectrogram image (R=Real, G=Magnitude, B=Imaginary)"),
io.Int.Input(id="channel_count", default=1, min=1, tooltip="Number of audio channels"),
io.Int.Input(id="sample_rate", default=44100, min=1, tooltip="Sample rate of the output audio"),
io.Int.Input(id="window_length", default=1024, min=16, tooltip="Window length in samples"),
io.Int.Input(id="hop_length", default=256, min=1, tooltip="Stride of the window (hop length) in samples"),
io.Combo.Input(id="window_type", default="hann", options=list(windows.keys()), tooltip="Type of window function to apply"),
],
outputs=[
io.Audio.Output(id="audio", tooltip="Output audio"),
],
)
@classmethod
def execute(cls, image, channel_count, sample_rate, window_length, hop_length, window_type):
B, H, W, _ = image.shape
bucket_count = H // channel_count
n_fft = (bucket_count - 1) * 2
real = image[..., 0].reshape(B * channel_count, bucket_count, W)
imag = image[..., 2].reshape(B * channel_count, bucket_count, W)
stft_complex = torch.complex(real, imag)
window = windows[window_type](window_length, device=image.device)
waveform = torch.istft(
stft_complex,
n_fft=n_fft,
hop_length=hop_length,
win_length=window_length,
window=window,
center=True,
normalized=False,
onesided=True,
)
waveform = waveform.reshape(B, channel_count, -1)
return ({"waveform": waveform, "sample_rate": sample_rate},)
+13
View File
@@ -0,0 +1,13 @@
from comfy_api.latest import io
@io.comfytype(io_type="STACK")
class MrmthStack(io.ComfyTypeIO):
Type = dict # Python type hint
class Input(io.Input):
def __init__(self, id: str, **kwargs):
super().__init__(id, **kwargs)
class Output(io.Output):
def __init__(self, **kwargs):
super().__init__(**kwargs)
+24
View File
@@ -0,0 +1,24 @@
from comfy_api.latest import io
from .Stack import MrmthStack
class StackUnBatcher(io.ComfyNode):
"""
Takes a list of stack created during operation running on batched values and outputs the last stack, making it possible to use the stack in non-batched operations after. When used on non-batched stack, it will do nothing.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_StackUnBatcher",
category="More math",
display_name="Stack UnBatcher",
is_input_list=True,
inputs=[
MrmthStack.Input(id="stack", tooltip="Input stack to unbatch"),
],
outputs=[
MrmthStack.Output(id="unbatched", tooltip="Unbatched stack output"),
])
@classmethod
def execute(cls, stack):
return (stack[-1],)
+112
View File
@@ -0,0 +1,112 @@
from .helper_functions import (
parse_expr,
get_v_variable,
get_f_variable,
checkLazyNew
)
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
import torch
from .Stack import MrmthStack
import copy
from .ParseTree import MrmthParseTree
class StringMathNode(io.ComfyNode):
"""
Enables math expressions on String values.
Inputs:
V: Autogrow string inputs (V0, V1, ...)
F: Autogrow float inputs (F0, F1, ...)
Expression: Math expression to evaluate
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_StringMathNode",
category="More math/math",
display_name="String math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.String.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on weights",
),
io.Int.Input(id="batching", default=0,min=-1),
io.Boolean.Input(
id="remember_stack",
default=False,
display_name="Remember stack across batch",
tooltip=(
"If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack."
),
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.String.Output(is_output_list=True),
MrmthStack.Output(),
],
)
@classmethod
def check_lazy_status(cls, Expression, V, F,batching=0, remember_stack=False,stack={}):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, batching=0, remember_stack=False, stack={}):
variables = {
"a": V.get("V0",""), "b": V.get("V1",""), "c": V.get("V2",""), "d": V.get("V3",""),
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0
}
v_stacked, v_cnt = get_v_variable(V, length_mismatch="do nothing")
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
variables["V_count"] = float(v_cnt)
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
for k, val in F.items():
variables[k] = val if val is not None else 0.0
for k, val in V.items():
variables[k] = val if val is not None else ""
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, len(V["V0"]),torch.device("cpu"),state_storage=stack)
result = visitor.visit(tree)
if batching == -1:
return (result, stack)
result = str(result)
if batching and batching > 0:
def chunks(s, n):
"""Produce `n`-character chunks from `s`."""
for start in range(0, len(s), n):
yield s[start:start+n]
stack = stack if remember_stack else copy.deepcopy(stack)
return (chunks(result,batching), stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([result], stack)
+98
View File
@@ -0,0 +1,98 @@
from inspect import cleandoc
from comfy_api.latest import io
import copy
from .helper_functions import checkLazyNew
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import comfy.utils
class VAEMathNode(io.ComfyNode):
"""
This node enables the use of math expressions on VAE weights using Autogrow inputs.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_VAEMathNode",
display_name="VAE Math",
category="More math/math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Vae.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on weights",
),
io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
),
io.Boolean.Input(
id="dict_mode",
default=False,
display_name="Dictionary mode",
tooltip="Evaluates VAE weights as dictionaries. Per-layer and dimension variables are unavailable.",
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies VAE tensors to the compute device for math and moves the patches back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Vae.Output(),
MrmthStack.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Expression, V, F, length_mismatch="tile",use_compute_device=True,stack={},dict_mode=False):
return checkLazyNew(Expression,V,F)
@classmethod
def execute(cls, V, F, Expression, length_mismatch="tile",use_compute_device=True,stack={},dict_mode=False) -> io.NodeOutput:
# Determine reference VAE
a = V.get("V0")
if a is None:
for m in V.values():
if m is not None:
a = m
break
if a is None:
raise ValueError("At least one input VAE is required.")
stack = copy.deepcopy(stack) if stack is not None else {}
# Prepare VAE patchers for calculation
# We need to map VAE wrappers to their patchers for `calculate_patches`
patchers_V = {}
for k, v in V.items():
if v is not None:
patchers_V[k] = v.patcher
# Calculate patches using the patchers (weights are in patcher.model.state_dict)
from .modelLikeCommon import calculate_patches_autogrow, calculate_patches_dict_mode
aliases = {"a": "V0", "b": "V1", "c": "V2", "d": "V3", "w": "F0", "x": "F1", "y": "F2", "z": "F3"}
ref_patcher = getattr(V.get("V0"), "patcher", None)
layer_count = ref_patcher.model.state_dict().__len__() if ref_patcher is not None and hasattr(ref_patcher, "model") and hasattr(ref_patcher.model, "state_dict") else 0
pbar = comfy.utils.ProgressBar(layer_count)
calculate_patches = calculate_patches_dict_mode if dict_mode else calculate_patches_autogrow
patches = calculate_patches(Expression, V=patchers_V, F=F, pbar=pbar, mapping=aliases, stack=stack, use_compute_device=use_compute_device)
# VAE does not have a clone method, so we shallow copy and clone the patcher
out_vae = copy.copy(a)
out_vae.patcher = a.patcher.clone()
if patches:
out_vae.patcher.add_patches(patches, 1.0, 1.0)
return (out_vae,stack)
+283
View File
@@ -0,0 +1,283 @@
from .helper_functions import generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, normalize_to_common_shape, make_zero_like, get_v_variable, get_f_variable, checkLazyNew, LazyVariableDict, get_tensor_device, move_to_device
from .Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io, InputImpl, Types
from .Stack import MrmthStack
from .ParseTree import MrmthParseTree
import copy
import comfy
def _get_video_components(video):
if hasattr(video, "get_components"):
return video.get_components()
if isinstance(video, dict):
return video
raise TypeError(f"Unsupported video input type: {type(video).__name__}")
def _get_video_tensor(video):
components = _get_video_components(video)
return components["images"] if isinstance(components, dict) else components.images
def _get_video_audio(video):
components = _get_video_components(video)
audio = components.get("audio") if isinstance(components, dict) else components.audio
if audio is None:
return None
return audio["waveform"] if isinstance(audio, dict) else audio.waveform
def _get_video_sample_rate(video):
components = _get_video_components(video)
audio = components.get("audio") if isinstance(components, dict) else components.audio
if isinstance(audio, dict):
return audio.get("sample_rate", 44100)
if audio is not None:
return getattr(audio, "sample_rate", 44100)
return 44100
def _build_video_output(images, waveform, sample_rate, source_video):
components = _get_video_components(source_video)
metadata = components.get("metadata") if isinstance(components, dict) else components.metadata
frame_rate = components.get("frame_rate") if isinstance(components, dict) else components.frame_rate
audio = {"waveform": waveform, "sample_rate": sample_rate} if waveform is not None else None
return InputImpl.VideoFromComponents(Types.VideoComponents(images=images, audio=audio, frame_rate=frame_rate, metadata=metadata))
class VideoMathNode(io.ComfyNode):
"""
Enables math expressions on Audio.
Inputs:
I: Autogrow image inputs (I0, I1, ...)
F: Autogrow float inputs (F0, F1, ...)
Image: Expression
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ag_VideoMathNode",
category="More math/math",
display_name="Video math",
inputs=[
io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.Video.Input("values"), prefix="V", min=1, max=50)),
io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)),
io.MultiType.Input(
io.String.Input("Expression", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on tensor part of video",
),
io.MultiType.Input(
io.String.Input("Expression_pi", default="V0", multiline=False),
types=[io.String,MrmthParseTree],
tooltip="Expression to apply on audio part of video",
)
, io.Combo.Input(
id="length_mismatch",
options=["do nothing","error","tile", "pad"],
display_name="on size mismatch",
default="error",
tooltip="How to handle mismatched video lengths. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
),
io.Boolean.Input(
id="use_compute_device",
default=True,
display_name="Move tensors to GPU",
tooltip="Temporarily copies video tensors to the compute device for math and moves the result back afterwards.",
),
MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True)
],
outputs=[
io.Video.Output(),
MrmthStack.Output(),
],
)
@classmethod
def check_lazy_status(cls, Expression,Expression_pi, V, F, length_mismatch="tile",use_compute_device=True,stack={}):
d = checkLazyNew(Expression,V,F)
b = checkLazyNew(Expression_pi,V,F)
return d|b
@classmethod
def execute(cls, V, F, Expression, Expression_pi, length_mismatch="tile",use_compute_device=True,stack={}):
tensor_keys = [k for k, v in V.items() if v is not None]
if not tensor_keys:
raise ValueError("At least one input is required.")
needed_vars = checkLazyNew(Expression, V, F) | checkLazyNew(Expression_pi, V, F)
needed_tensor_keys = [k for k in tensor_keys if k in needed_vars]
if not needed_tensor_keys:
needed_tensor_keys = tensor_keys
tensors = [_get_video_tensor(V[k]) for k in needed_tensor_keys]
original_device = get_tensor_device(tensors[0])
compute_device = comfy.model_management.get_torch_device() if use_compute_device else original_device
working_V = move_to_device(V, compute_device) if compute_device is not None and compute_device != original_device else V
# Normalize all tensors together to find the common target shape
tensors = [_get_video_tensor(working_V[k]) for k in needed_tensor_keys]
normalized_tensors = normalize_to_common_shape(*tensors, mode=length_mismatch)
V_norm = dict(zip(needed_tensor_keys, normalized_tensors))
# Use first normalized tensor to establish the reference shape
ref_tensor = normalized_tensors[0]
common_shape = ref_tensor.shape
stack = copy.deepcopy(stack) if stack is not None else {}
sample_rates = {k + "sr": _get_video_sample_rate(V[k]) for k in needed_tensor_keys}
# Setup legacy variables a, b, c, d
ae = V_norm.get("V0", make_zero_like(ref_tensor))
be = V_norm.get("V1", make_zero_like(ae))
ce = V_norm.get("V2", make_zero_like(ae))
de = V_norm.get("V3", make_zero_like(ae))
# Ensure legacy variables are normalized in case they were zero-initialized
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
if(length_mismatch == "error"):
for name, video in V.items():
if video is not None and _get_video_tensor(video).shape[0] != common_shape[0]:
raise ValueError(f"Input '{name}' has shape {_get_video_tensor(video).shape[0]}, expected {common_shape[0]} to match input.")
variables = LazyVariableDict({
"a": ae, "b": be, "c": ce, "d": de,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"X": getIndexTensorAlongDim(ae, 2),
"Y": getIndexTensorAlongDim(ae, 1),
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"C": getIndexTensorAlongDim(ae, 3),
"channel": getIndexTensorAlongDim(ae, 3),
"W": float(ae.shape[2]),
"width": float(ae.shape[2]),
"H": float(ae.shape[1]),
"height": float(ae.shape[1]),
"T": float(ae.shape[0]),
"batch_count": float(ae.shape[0]),
"N": float(ae.shape[3]),
"channel_count": float(ae.shape[3]),
} | generate_dim_variables(ae) | V_norm | sample_rates)
for v_name, v_tensor in V_norm.items():
variables[v_name] = (lambda t=v_tensor: t)
variables[v_name].is_lazy_var = True
def lazy_v_stack():
return get_v_variable(V_norm, length_mismatch=length_mismatch)[0]
lazy_v_stack.is_lazy_var = True
def lazy_v_count():
return float(get_v_variable(V_norm, length_mismatch=length_mismatch)[1])
lazy_v_count.is_lazy_var = True
variables["V"] = lazy_v_stack
variables["Vcnt"] = lazy_v_count
variables["V_count"] = lazy_v_count
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
for k, val in F.items():
variables[k] = val if val is not None else 0.0
tree = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
else:
tree = Expression
visitor = UnifiedMathVisitor(variables, ae.shape,ae.device,state_storage=stack)
result_video = visitor.visit(tree)
result_video = as_tensor(result_video, ae.shape)
waveforms = {}
for k in needed_tensor_keys:
waveform = _get_video_audio(working_V[k])
if waveform is None:
raise ValueError(f"Input '{k}' does not contain audio data.")
waveforms[k] = waveform
# Normalize all waveforms together
normalized_waveforms = normalize_to_common_shape(*waveforms.values(), mode=length_mismatch)
V_norm_waveforms = dict(zip(needed_tensor_keys, normalized_waveforms))
ref_waveform = normalized_waveforms[0]
common_shape = ref_waveform.shape
sample_rate = _get_video_sample_rate(V[needed_tensor_keys[0]])
if(length_mismatch == "error"):
for name in needed_tensor_keys:
if waveforms[name].shape != common_shape:
raise ValueError(f"Input '{name}' has shape ({waveforms[name].shape[0]}, {waveforms[name].shape[2]}), expected ({common_shape[0]}, {common_shape[2]}) to match input.")
# Setup legacy variables a, b, c, d
a_w = V_norm_waveforms.get("V0", make_zero_like(ref_waveform))
b_w = V_norm_waveforms.get("V1", make_zero_like(a_w))
c_w = V_norm_waveforms.get("V2", make_zero_like(a_w))
d_w = V_norm_waveforms.get("V3", make_zero_like(a_w))
# Ensure legacy are normalized
a_w, b_w, c_w, d_w = normalize_to_common_shape(a_w, b_w, c_w, d_w, mode=length_mismatch)
variables = LazyVariableDict({
"a": a_w, "b": b_w, "c": c_w, "d": d_w,
"w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0,
"x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0,
"y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0,
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0,
"B": getIndexTensorAlongDim(a_w, 0),
"C": getIndexTensorAlongDim(a_w, 1),
"channel": getIndexTensorAlongDim(a_w, 1),
"S": getIndexTensorAlongDim(a_w, 2),
"sample": getIndexTensorAlongDim(a_w, 2),
"R": sample_rate,
"sample_rate": sample_rate,
"batch": getIndexTensorAlongDim(a_w, 0),
"T": float(a_w.shape[0]),
"batch_count": float(a_w.shape[0]),
} | generate_dim_variables(a_w) | sample_rates)
for v_name, v_tensor in V_norm_waveforms.items():
variables[v_name] = (lambda t=v_tensor: t)
variables[v_name].is_lazy_var = True
def lazy_v_stack():
return get_v_variable(V_norm_waveforms, length_mismatch=length_mismatch)[0]
lazy_v_stack.is_lazy_var = True
def lazy_v_count():
return float(get_v_variable(V_norm_waveforms, length_mismatch=length_mismatch)[1])
lazy_v_count.is_lazy_var = True
variables["V"] = lazy_v_stack
variables["Vcnt"] = lazy_v_count
variables["V_count"] = lazy_v_count
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
for k, val in F.items():
variables[k] = val if val is not None else 0.0
tree_pi = None
if isinstance(Expression_pi,str):
tree_pi = parse_expr(Expression_pi)
else:
tree_pi = Expression_pi
visitor = UnifiedMathVisitor(variables, a_w.shape,a_w.device,state_storage=stack)
result_waveform = visitor.visit(tree_pi)
result_waveform = as_tensor(result_waveform, a_w.shape)
result_waveform = move_to_device(result_waveform, original_device)
result_video = move_to_device(result_video, original_device)
return (_build_video_output(result_video, result_waveform, sample_rate, V[needed_tensor_keys[0]]), stack)
View File
+91
View File
@@ -0,0 +1,91 @@
import torch
from ..helper_functions import generate_dim_variables,parse_expr, getIndexTensorAlongDim, as_tensor, commonLazy, normalize_to_common_shape,prepare_inputs
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
class AudioMathNodeOLD(io.ComfyNode):
"""
Enables math expressions on Audio.
Inputs:
a, b, c, d: Audio inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
Audio: Expression to apply on input audio
Outputs:
AUDIO: Result of applying expression to input audio
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
is_deprecated=True,
node_id="mrmth_AudioMathNode",
inputs=[
io.Audio.Input(id="a"),
io.Audio.Input(id="b", optional=True, lazy=True),
io.Audio.Input(id="c", optional=True, lazy=True),
io.Audio.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Audio", default="a*(1-w)+b*w", tooltip="Expression to apply on input audio waveforms"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched audio sample counts. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing samples as zero."
)
],
outputs=[
io.Audio.Output(),
],
)
@classmethod
def check_lazy_status(cls, Audio, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Audio, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Audio, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
a, b, c, d = prepare_inputs(a, b, c, d)
av = a["waveform"]
sample_rate = a["sample_rate"]
bv = None if b is None else b["waveform"]
cv = None if c is None else c["waveform"]
dv = None if d is None else d["waveform"]
if(length_mismatch == "error"):
# For audio, we check the length dimension (last one) as well as batch
tensors_to_check = [t for t in [av, bv, cv, dv] if t is not None]
max_batch = max(t.shape[0] for t in tensors_to_check)
max_len = max(t.shape[-1] for t in tensors_to_check)
for tensor, name in zip([av, bv, cv, dv], ["a", "b", "c", "d"]):
if tensor is not None:
if tensor.shape[0] != max_batch or tensor.shape[-1] != max_len:
raise ValueError(f"Input '{name}' has shape ({tensor.shape[0]}, {tensor.shape[-1]}), expected ({max_batch}, {max_len}) to match largest input.")
ae, be, ce, de = normalize_to_common_shape(av, bv, cv, dv, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"C": getIndexTensorAlongDim(ae, 1),
"channel": getIndexTensorAlongDim(ae, 1),
"S": getIndexTensorAlongDim(ae, 2),
"sample": getIndexTensorAlongDim(ae, 2),
"R": torch.full_like(ae, sample_rate, dtype=torch.float32),
"sample_rate": torch.full_like(ae, sample_rate, dtype=torch.float32),
"T": torch.full_like(ae, ae.shape[2], dtype=torch.float32),
"sample_count": torch.full_like(ae, ae.shape[2], dtype=torch.float32),
"N": ae.shape[1],
"channel_count": ae.shape[1],
} | generate_dim_variables(ae)
tree = parse_expr(Audio);
visitor = UnifiedMathVisitor(variables, ae.shape)
result = visitor.visit(tree)
final_res = as_tensor(result, ae.shape)
return ({"waveform": final_res, "sample_rate": sample_rate},)
+58
View File
@@ -0,0 +1,58 @@
from comfy_api.latest import io
from ..modelLikeCommon import calculate_patches
from inspect import cleandoc
from ..helper_functions import commonLazy
class CLIPMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on CLIP weights.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_CLIPMathNode",
display_name="CLIP Math",
is_deprecated=True,
inputs=[
io.Clip.Input(id="a", tooltip="Main CLIP (base)"),
io.Clip.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd CLIP"),
io.Clip.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd CLIP"),
io.Clip.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th CLIP"),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
outputs=[
io.Clip.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
patcher_a = a.patcher
patcher_b = b.patcher if b else None
patcher_c = c.patcher if c else None
patcher_d = d.patcher if d else None
patches = calculate_patches(Model, patcher_a, patcher_b, patcher_c, patcher_d, w, x, y, z)
out_clip = a.clone()
if patches:
out_clip.add_patches(patches, 1.0, 1.0)
return (out_clip,)
@@ -0,0 +1,107 @@
from ..helper_functions import commonLazy,parse_expr, generate_dim_variables, as_tensor, prepare_inputs, getIndexTensorAlongDim, normalize_to_common_shape
from comfy_api.latest import io
import torch
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
class ConditioningMathNodeOLD(io.ComfyNode):
"""
Enables math operations on conditionings.
Inputs:
a, b, c, d: Conditioning inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
Tensor: Expression for the tensor part (describes image composition)
pooled_output: Expression for the pooled output (condensed representation)
Outputs:
CONDITIONING: Result of applying expressions to input conditionings
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ConditioningMathNode",
display_name="Conditioning math",
is_deprecated=True,
inputs=[
io.Conditioning.Input(id="a"),
io.Conditioning.Input(id="b", optional=True, lazy=True),
io.Conditioning.Input(id="c", optional=True, lazy=True),
io.Conditioning.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Tensor", default="a*(1-w)+b*w", tooltip="Expression for tensor part (image composition)"),
io.String.Input(
id="pooled_output", default="a*(1-w)+b*w", tooltip="Expression for pooled output (condensed representation)"
),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched conditioning segment counts. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing as zero."
)
],
outputs=[
io.Conditioning.Output(),
],
)
@classmethod
def check_lazy_status(cls, Tensor, pooled_output, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
tensor_needs = set(commonLazy(Tensor, a, b, c, d, w, x, y, z))
pooled_needs = set(commonLazy(pooled_output, a, b, c, d, w, x, y, z))
return list(tensor_needs.union(pooled_needs))
@classmethod
def execute(cls, Tensor, pooled_output, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile"):
# Default missing conditionings to zero
a_c, b_c, c_c, d_c = prepare_inputs(a, b, c, d)
# We process the first segment of each conditioning
ta_full, da = a_c[0]
tb_full, db = b_c[0]
tc_full, dc = c_c[0]
td_full, dd = d_c[0]
ta, tb, tc, td = normalize_to_common_shape(ta_full, tb_full, tc_full, td_full, mode=length_mismatch)
B_val = getIndexTensorAlongDim(ta, 0)
variables = {
"a": ta, "b": tb, "c": tc, "d": td, "w": w, "x": x, "y": y, "z": z,
"B": B_val, "batch": B_val,
"T": ta.shape[0], "batch_count": ta.shape[0],
"N": ta.shape[1], "channel_count": ta.shape[1],
} | generate_dim_variables(ta)
tree = parse_expr(Tensor)
visitor = UnifiedMathVisitor(variables, ta.shape)
result_tensor = as_tensor(visitor.visit(tree), ta.shape)
new_dict = da.copy()
pa = da.get("pooled_output")
if pa is not None:
pb = db.get("pooled_output")
pc = dc.get("pooled_output")
pd = dd.get("pooled_output")
pb = pb if pb is not None else torch.zeros_like(pa)
pc = pc if pc is not None else torch.zeros_like(pa)
pd = pd if pd is not None else torch.zeros_like(pa)
pa, pb, pc, pd = normalize_to_common_shape(pa, pb, pc, pd, mode=length_mismatch)
variables_pooled = {"a": pa, "b": pb, "c": pc, "d": pd, "w": w, "x": x, "y": y, "z": z} | generate_dim_variables(pa)
tree_p = parse_expr(pooled_output)
visitor_p = UnifiedMathVisitor(variables_pooled, pa.shape)
result_pooled = as_tensor(visitor_p.visit(tree_p), pa.shape)
new_dict["pooled_output"] = result_pooled
# Create new conditioning list
output_cond = [(result_tensor, new_dict)]
if len(a) > 1:
output_cond.extend(a[1:])
return (output_cond,)
+67
View File
@@ -0,0 +1,67 @@
from inspect import cleandoc
from ..helper_functions import parse_expr
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from ..helper_functions import commonLazy
from comfy_api.latest import io
class FloatMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on Floats.
Inputs:
a, b, c, d: Floats, bound to variables with the same name.
w, x, y, z: Floats, bound to variables of the expression.
FloatFunc: String, describing math expression.
Valid functions: sin, cos, tan, abs, sqrt, min, max, norm, etc.
Operators: +, -, *, /, ^, %.
Constants: e, pi.
Outputs:
FLOAT: The result of evaluating the math expression.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_FloatMathNode",
is_deprecated=True,
display_name="Float math",
inputs=[
io.Float.Input(id="a", force_input=True),
io.Float.Input(id="b", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="c", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="d", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="FloatFunc", default="a*(1-w)+b*w", tooltip="Expression to use on inputs"),
],
outputs=[
io.Float.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, FloatFunc, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0,):
return commonLazy(FloatFunc, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, FloatFunc, a, b=0.0, c=0.0, d=0.0, w=0.0, x=0.0, y=0.0, z=0.0):
variables = {
"a": a,
"b": b,
"c": c,
"d": d,
"w": w, "x": x, "y": y, "z": z
}
tree = parse_expr(FloatFunc);
visitor = UnifiedMathVisitor(variables, [1])
result = visitor.visit(tree)
return (result,)
+85
View File
@@ -0,0 +1,85 @@
from ..helper_functions import generate_dim_variables, parse_expr, getIndexTensorAlongDim, as_tensor, prepare_inputs, commonLazy, normalize_to_common_shape
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
class ImageMathNodeOLD(io.ComfyNode):
"""
Enables math expressions on Images.
Inputs:
a, b, c, d: Image inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
Image: Expression to apply on input images
Outputs:
IMAGE: Result of applying expression to input images
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ImageMathNode",
display_name="Image math",
is_deprecated=True,
inputs=[
io.Image.Input(id="a"),
io.Image.Input(id="b", optional=True, lazy=True),
io.Image.Input(id="c", optional=True, lazy=True),
io.Image.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Image", default="a*(1-w)+b*w", tooltip="Expression to apply on input images"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched image batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
],
outputs=[
io.Image.Output(),
],
)
@classmethod
def check_lazy_status(cls, Image, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Image, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Image, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile"):
ae, be, ce, de = prepare_inputs(a, b, c, d)
print(f"DEBUG: shapes {ae.shape[0]}, {be.shape[0]}, {ce.shape[0]}, {de.shape[0]}")
if(length_mismatch == "error"):
max_length = max(ae.shape[0], be.shape[0], ce.shape[0], de.shape[0])
for tensor, name in zip([ae, be, ce, de], ["a", "b", "c", "d"]):
if tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(ae, be, ce, de, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"X": getIndexTensorAlongDim(ae, 3),
"Y": getIndexTensorAlongDim(ae, 2),
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"C": getIndexTensorAlongDim(ae, 1),
"channel": getIndexTensorAlongDim(ae, 1),
"W": ae.shape[2],
"width": ae.shape[2],
"H": ae.shape[1],
"height": ae.shape[1],
"T": ae.shape[0],
"batch_count": ae.shape[0],
"N": ae.shape[3],
"channel_count": ae.shape[3],
} | generate_dim_variables(ae)
tree = parse_expr(Image);
visitor = UnifiedMathVisitor(variables, ae.shape)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
return (result,)
+159
View File
@@ -0,0 +1,159 @@
from inspect import cleandoc
from comfy_api.latest import io
from ..helper_functions import (
generate_dim_variables,
getIndexTensorAlongDim,
parse_expr,
as_tensor,
normalize_to_common_shape,
prepare_inputs
)
from ..helper_functions import commonLazy
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
import torch
class LatentMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on Latents.
inputs:
a, b, c, d:
Latent, bound to variables with the same name. Defaults to zero latent if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Latent expression:
String, describing expression to aply to latents.
outputs:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
pass
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_LatentMathNode",
display_name="Latent math",
is_deprecated=True,
inputs=[
io.Latent.Input(id="a"),
io.Latent.Input(id="b", optional=True, lazy=True),
io.Latent.Input(id="c", optional=True, lazy=True),
io.Latent.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Latent", default="a*(1-w)+b*w", tooltip="Expression to apply on input latents"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched latent batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
],
outputs=[
io.Latent.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Latent, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Latent, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Latent, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
# Identify if any input is a NestedTensor and track original sizes for restoration
stacked = False
orig_split_sizes = None
for item in [a, b, c, d]:
if item is not None:
samples = item.get("samples")
if getattr(samples, "is_nested", False):
stacked = True
# Store original split sizes (batch dimension)
orig_split_sizes = [t.shape[0] for t in samples.tensors]
break
if stacked:
if a is not None and getattr(a.get("samples"), "is_nested", False):
a = a.copy()
a["samples"] = torch.cat(a["samples"].tensors, dim=0)
if b is not None and getattr(b.get("samples"), "is_nested", False):
b = b.copy()
b["samples"] = torch.cat(b["samples"].tensors, dim=0)
if c is not None and getattr(c.get("samples"), "is_nested", False):
c = c.copy()
c["samples"] = torch.cat(c["samples"].tensors, dim=0)
if d is not None and getattr(d.get("samples"), "is_nested", False):
d = d.copy()
d["samples"] = torch.cat(d["samples"].tensors, dim=0)
a_c, b_c, c_c, d_c = prepare_inputs(a, b, c, d)
at,bt,ct,dt = a_c["samples"],b_c["samples"],c_c["samples"],d_c["samples"]
if(length_mismatch == "error"):
# Check only available tensors
tensors_to_check = [t for t in [at, bt, ct, dt] if t is not None]
max_length = max(t.shape[0] for t in tensors_to_check)
for tensor, name in zip([at, bt, ct, dt], ["a", "b", "c", "d"]):
if tensor is not None:
if tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(at, bt, ct, dt, mode=length_mismatch)
# parse expression once
tree = parse_expr(Latent)
ndim = ae.ndim
batch_dim = 0
channel_dim = -3
height_dim = -2
width_dim = -1
time_dim = None
if ndim >= 5:
time_dim = -4
frame_count = ae.shape[time_dim] if time_dim is not None else ae.shape[batch_dim]
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"X": getIndexTensorAlongDim(ae, width_dim),
"Y": getIndexTensorAlongDim(ae, height_dim),
"B": getIndexTensorAlongDim(ae, batch_dim),
"batch": getIndexTensorAlongDim(ae, batch_dim),
"C": getIndexTensorAlongDim(ae, channel_dim),
"channel": getIndexTensorAlongDim(ae, channel_dim),
"W": ae.shape[width_dim],
"width": ae.shape[width_dim],
"H": ae.shape[height_dim],
"height": ae.shape[height_dim],
"T": frame_count,
"batch_count": ae.shape[batch_dim],
"N": ae.shape[channel_dim],
"channel_count": ae.shape[channel_dim],
} | generate_dim_variables(ae)
if time_dim is not None:
F = getIndexTensorAlongDim(ae, time_dim)
variables.update({"frame_idx": F, "frame": F, "frame_count": frame_count})
visitor = UnifiedMathVisitor(variables, ae.shape)
result_t = as_tensor(visitor.visit(tree), ae.shape)
result_latent = a_c.copy()
if stacked and orig_split_sizes is not None:
from comfy.nested_tensor import NestedTensor
# Restore original split sizes
result_latent["samples"] = NestedTensor(torch.split(result_t, orig_split_sizes, dim=0))
else:
result_latent["samples"] = result_t
return (result_latent,)
+82
View File
@@ -0,0 +1,82 @@
from ..helper_functions import generate_dim_variables,parse_expr, getIndexTensorAlongDim, as_tensor, commonLazy, normalize_to_common_shape,prepare_inputs
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
class MaskMathNodeOLD(io.ComfyNode):
"""
Enables math expressions on Masks.
Inputs:
a, b, c, d: Mask inputs (b, c, d default to zero if not provided)
w, x, y, z: Float variables for expressions
Mask: Expression to apply on input masks
Outputs:
MASK: Result of applying expression to input masks
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_MaskMathNode",
display_name="Mask math",
is_deprecated=True,
inputs=[
io.Mask.Input(id="a"),
io.Mask.Input(id="b", optional=True, lazy=True),
io.Mask.Input(id="c", optional=True, lazy=True),
io.Mask.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Mask", default="a*(1-w)+b*w", tooltip="Expression to apply on input masks"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched mask batch sizes. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing frames as zero."
)
],
outputs=[
io.Mask.Output(),
],
)
@classmethod
def check_lazy_status(cls, Mask, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Mask, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Mask, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile"):
a, b, c, d = prepare_inputs(a, b, c, d)
if(length_mismatch == "error"):
max_length = max(a.shape[0], b.shape[0], c.shape[0], d.shape[0])
for tensor, name in zip([a, b, c, d], ["a", "b", "c", "d"]):
if tensor.shape[0] != max_length:
raise ValueError(f"Input '{name}' has shape {tensor.shape[0]}, expected {max_length} to match largest input.")
ae, be, ce, de = normalize_to_common_shape(a, b, c, d, mode=length_mismatch)
variables = {
"a": ae, "b": be, "c": ce, "d": de,
"w": w, "x": x, "y": y, "z": z,
"X": getIndexTensorAlongDim(ae, 2),
"Y": getIndexTensorAlongDim(ae, 1),
"B": getIndexTensorAlongDim(ae, 0),
"batch": getIndexTensorAlongDim(ae, 0),
"W": ae.shape[2],
"width": ae.shape[2],
"H": ae.shape[1],
"height": ae.shape[1],
"T": ae.shape[0],
"batch_count": ae.shape[0],
} | generate_dim_variables(ae)
tree = parse_expr(Mask);
visitor = UnifiedMathVisitor(variables, ae.shape)
result = visitor.visit(tree)
result = as_tensor(result, ae.shape)
return (result,)
+53
View File
@@ -0,0 +1,53 @@
from inspect import cleandoc
from comfy_api.latest import io
from ..helper_functions import commonLazy
from ..modelLikeCommon import calculate_patches
class ModelMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on Model weights (state_dict).
Functionally acts as a custom model merge.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_ModelMathNode",
display_name="Model Math",
is_deprecated=True,
inputs=[
io.Model.Input(id="a", tooltip="Main model (base)"),
io.Model.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd model"),
io.Model.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd model"),
io.Model.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th model"),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
outputs=[
io.Model.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
patches = calculate_patches(Model, a, b, c, d, w, x, y, z)
out_model = a.clone()
if patches:
out_model.add_patches(patches, 1.0, 1.0)
return (out_model,)
+210
View File
@@ -0,0 +1,210 @@
from inspect import cleandoc
import torch
from ..helper_functions import (
generate_dim_variables,
getIndexTensorAlongDim,
commonLazy,
parse_expr,
make_zero_like,
as_tensor,
)
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
import comfy.nested_tensor as _nested_tensor_module
class NoiseMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on Latents.
inputs:
a, b, c, d:
Noise generators, bound to variables with the same name. Defaults to zero latent if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Latent expression:
String, describing expression to mix noise.
outputs:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
pass
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
is_deprecated=True,
node_id="mrmth_NoiseMathNode",
#category="More math",
display_name="Noise math",
inputs=[
io.Noise.Input(id="a"),
io.Noise.Input(id="b", optional=True, lazy=True),
io.Noise.Input(id="c", optional=True, lazy=True),
io.Noise.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Noise", default="a*(1-w)+b*w", tooltip="Expression to apply on input noise generators"),
],
outputs=[
io.Noise.Output(),
],
)
# RETURN_NAMES = ("image_output_name",)
tooltip = cleandoc(__doc__)
# OUTPUT_NODE = False
# OUTPUT_TOOLTIPS = ("",) # Tooltips for the output node
CATEGORY = "More math"
@classmethod
def check_lazy_status(cls, Noise, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0):
return commonLazy(Noise, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Noise, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0):
return (NoiseExecutor(a, b, c, d, w, x, y, z, Noise),)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
# @classmethod
# def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
class NoiseExecutor:
def __init__(self, a, b, c, d, w, x, y, z, Noise):
self.a = a
self.b = b
self.c = c
self.d = d
self.w = w
self.x = x
self.y = y
self.z = z
self.expr = Noise
self.tree = parse_expr(Noise)
seed = -1
def generate_noise(self, input_latent: torch.Tensor) -> torch.Tensor:
samples = input_latent["samples"]
merged_result=None
a_val = self.a.generate_noise(input_latent) if self.a is not None else make_zero_like(samples)
b_val = self.b.generate_noise(input_latent) if self.b is not None else make_zero_like(samples)
c_val = self.c.generate_noise(input_latent) if self.c is not None else make_zero_like(samples)
d_val = self.d.generate_noise(input_latent) if self.d is not None else make_zero_like(samples)
# nested case: merge subtensors, evaluate once, split back
if hasattr(samples, "is_nested") and getattr(samples, "is_nested"):
sample_list = samples.unbind()
sizes = [t.shape[0] for t in sample_list]
merged_samples = torch.cat(sample_list, dim=0)
def merge_to_tensor(val, ref):
if val is None:
return make_zero_like(ref)
if hasattr(val, "is_nested") and getattr(val, "is_nested"):
lst = val.unbind()
return torch.cat(lst, dim=0)
if isinstance(val, (list, tuple)):
return torch.cat(list(val), dim=0)
if torch.is_tensor(val):
if val.shape == ref.shape:
return val
if val.shape[0] == sum(sizes):
return val
# if val has per-subtensor batches, try to split and cat
try:
if val.shape[0] == len(sample_list):
return torch.cat(
[val[i].unsqueeze(0).expand(sample_list[i].shape[0], *val.shape[1:]) for i in range(len(sample_list))],
dim=0,
)
except Exception:
pass
return make_zero_like(ref)
merged_a = merge_to_tensor(a_val, merged_samples)
merged_b = merge_to_tensor(b_val, merged_samples)
merged_c = merge_to_tensor(c_val, merged_samples)
merged_d = merge_to_tensor(d_val, merged_samples)
else:
merged_samples = samples
merged_a = a_val
merged_b = b_val
merged_c = c_val
merged_d = d_val
# evaluate once
ndim = merged_samples.ndim
batch_dim = 0
channel_dim = -3
height_dim = -2
width_dim = -1
time_dim = None
if ndim >= 5:
time_dim = -4
frame_count = merged_samples.shape[time_dim] if time_dim is not None else merged_samples.shape[batch_dim]
B = getIndexTensorAlongDim(merged_samples, batch_dim)
W = getIndexTensorAlongDim(merged_samples, width_dim)
H = getIndexTensorAlongDim(merged_samples, height_dim)
C = getIndexTensorAlongDim(merged_samples, channel_dim)
variables = {
"a": merged_a,
"b": merged_b,
"c": merged_c,
"d": merged_d,
"w": self.w,
"x": self.x,
"y": self.y,
"z": self.z,
"B": B,
"X": W,
"Y": H,
"C": C,
"W": merged_samples.shape[width_dim],
"H": merged_samples.shape[height_dim],
"I": merged_samples,
"T": frame_count,
"N": merged_samples.shape[channel_dim],
"batch": B,
"width": merged_samples.shape[width_dim],
"height": merged_samples.shape[height_dim],
"channel": C,
"batch_count": merged_samples.shape[0],
"channel_count": merged_samples.shape[1],
"input_latent": merged_samples,
} | generate_dim_variables(merged_samples)
if time_dim is not None:
F = getIndexTensorAlongDim(merged_samples, time_dim)
variables.update({"frame": F, "frame_count": frame_count})
tree = parse_expr(self.expr);
visitor = UnifiedMathVisitor(variables, merged_samples.shape)
result_tensor = visitor.visit(tree)
merged_result = as_tensor(result_tensor, variables["a"].shape)
if hasattr(samples, "is_nested") and getattr(samples, "is_nested"):
split_results = list(merged_result.split(sizes, dim=0))
return _nested_tensor_module.NestedTensor(split_results)
return merged_result
+63
View File
@@ -0,0 +1,63 @@
from inspect import cleandoc
from comfy_api.latest import io
import copy
from ..modelLikeCommon import calculate_patches
from ..helper_functions import commonLazy
class VAEMathNodeOLD(io.ComfyNode):
"""
This node enables the use of math expressions on VAE weights.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_VAEMathNode",
display_name="VAE Math",
is_deprecated=True,
inputs=[
io.Vae.Input(id="a", tooltip="Main VAE (base)"),
io.Vae.Input(id="b", optional=True, lazy=True, tooltip="Optional 2nd VAE"),
io.Vae.Input(id="c", optional=True, lazy=True, tooltip="Optional 3rd VAE"),
io.Vae.Input(id="d", optional=True, lazy=True, tooltip="Optional 4th VAE"),
io.Float.Input(id="w", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="x", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="y", default=0.0, lazy=True, optional=True, force_input=True),
io.Float.Input(id="z", default=0.0, lazy=True, optional=True, force_input=True),
io.String.Input(id="Model", default="a*(1-w)+b*w", tooltip="Expression to apply on weights"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched layer counts. For models, this usually defaults to broadcast (zero for missing layers)."
)
],
outputs=[
io.Vae.Output(),
],
)
tooltip = cleandoc(__doc__)
@classmethod
def check_lazy_status(cls, Model, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Model, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
patcher_a = a.patcher
patcher_b = b.patcher if b else None
patcher_c = c.patcher if c else None
patcher_d = d.patcher if d else None
patches = calculate_patches(Model, patcher_a, patcher_b, patcher_c, patcher_d, w, x, y, z)
# VAE does not have a clone method, so we shallow copy and clone the patcher
out_vae = copy.copy(a)
out_vae.patcher = a.patcher.clone()
if patches:
out_vae.patcher.add_patches(patches, 1.0, 1.0)
return (out_vae,)
+158
View File
@@ -0,0 +1,158 @@
import torch
import math
from ..helper_functions import (
generate_dim_variables,
getIndexTensorAlongDim,
parse_expr,
commonLazy,
normalize_to_common_shape,
as_tensor,
prepare_inputs
)
from ..Parser.UnifiedMathVisitor import UnifiedMathVisitor
from comfy_api.latest import io
from comfy_api.latest import VideoComponents, VideoFromComponents
class VideoMathNodeOLD(io.ComfyNode):
"""
Enables math expressions on Videos (Images + Audio).
Video object is expected to have 'images' and 'audio' components.
Expressions for 'Images' and 'Audio' are handled separately.
"""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="mrmth_VideoMathNode",
is_deprecated=True,
display_name="Video math",
inputs=[
io.Video.Input(id="a"),
io.Video.Input(id="b", optional=True, lazy=True),
io.Video.Input(id="c", optional=True, lazy=True),
io.Video.Input(id="d", optional=True, lazy=True),
io.Float.Input(id="w", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="x", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="y", default=0.0, optional=True, lazy=True, force_input=True),
io.Float.Input(id="z", default=0.0, optional=True, lazy=True, force_input=True),
io.String.Input(id="Images", default="a*(1-w)+b*w", tooltip="Expression for the image frames"),
io.String.Input(id="Audio", default="a*(1-w)+b*w", tooltip="Expression for the audio component"),
io.Combo.Input(
id="length_mismatch",
options=["tile", "error", "pad"],
default="error",
tooltip="How to handle mismatched frame/sample counts. tile: repeat shorter inputs; error: raise error on mismatch; pad: treat missing as zero."
)
],
outputs=[
io.Video.Output(),
],
)
@classmethod
def check_lazy_status(cls, Images, Audio, a, b=[], c=[], d=[], w=0, x=0, y=0, z=0, length_mismatch="tile"):
return commonLazy(Images, a, b, c, d, w, x, y, z) + commonLazy(Audio, a, b, c, d, w, x, y, z)
@classmethod
def execute(cls, Images, Audio, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, length_mismatch="tile") -> io.NodeOutput:
ac, bc, cc, dc = prepare_inputs(a,b,c,d)
imgs_ae = ac.images
imgs_be = bc.images
imgs_ce = cc.images
imgs_de = dc.images
imgs_ae, imgs_be, imgs_ce, imgs_de = normalize_to_common_shape(imgs_ae, imgs_be, imgs_ce, imgs_de, mode=length_mismatch)
img_vars = {
"a": imgs_ae, "b": imgs_be, "c": imgs_ce, "d": imgs_de,
"w": w, "x": x, "y": y, "z": z,
"B": getIndexTensorAlongDim(imgs_ae, 0),
"frame": getIndexTensorAlongDim(imgs_ae, 0),
"C": getIndexTensorAlongDim(imgs_ae, 1),
"channel": getIndexTensorAlongDim(imgs_ae, 1),
"H": imgs_ae.shape[2],
"height": imgs_ae.shape[2],
"W": imgs_ae.shape[3],
"width": imgs_ae.shape[3],
"T": imgs_ae.shape[0],
"frame_count": imgs_ae.shape[0],
"R": float(ac.frame_rate),
"frame_rate": float(ac.frame_rate),
"N": imgs_ae.shape[1],
"channel_count": imgs_ae.shape[1],
} | generate_dim_variables(imgs_ae)
tree = parse_expr(Images);
visitor = UnifiedMathVisitor(img_vars, imgs_ae.shape)
result_tensor = visitor.visit(tree)
result_imgs = as_tensor(result_tensor, imgs_ae.shape)
# --- Process Audio ---
wav_a = ac.audio["waveform"]
sample_rate = ac.audio["sample_rate"]
wav_b = bc.audio["waveform"] if bc is not None else None
wav_c = cc.audio["waveform"] if cc is not None else None
wav_d = dc.audio["waveform"] if dc is not None else None
# Determine target_wav_len as max of all provided audio
wav_lengths = [wav_a.shape[2]]
if bc is not None: wav_lengths.append(wav_b.shape[2])
if cc is not None: wav_lengths.append(wav_c.shape[2])
if dc is not None: wav_lengths.append(wav_d.shape[2])
target_wav_len = max(wav_lengths)
def resolve_wav(tensor, comp, name):
if comp is None:
return 0.0
curr_len = tensor.shape[2]
if curr_len == target_wav_len:
return tensor
if length_mismatch == "tile":
return tensor.repeat(1, 1, math.ceil(target_wav_len / curr_len))[:, :, :target_wav_len]
elif length_mismatch == "pad":
out = torch.zeros((tensor.shape[0], tensor.shape[1], target_wav_len), device=tensor.device, dtype=tensor.dtype)
out[:, :, :curr_len] = tensor
return out
else: # error
raise ValueError(f"Audio samples mismatch in {name}: expected {target_wav_len} to match longest input, got {curr_len}. Set length_mismatch to 'broadcast' or 'pad' to handle this.")
wav_ae = resolve_wav(wav_a, True, "input a")
wav_be = resolve_wav(wav_b, bc, "input b")
wav_ce = resolve_wav(wav_c, cc, "input c")
wav_de = resolve_wav(wav_d, dc, "input d")
wav_ae, wav_be, wav_ce, wav_de = normalize_to_common_shape(wav_ae, wav_be, wav_ce, wav_de, mode=length_mismatch)
wav_vars = {
"a": wav_ae, "b": wav_be, "c": wav_ce, "d": wav_de,
"w": w, "x": x, "y": y, "z": z,
"B": getIndexTensorAlongDim(wav_ae, 0),
"batch": getIndexTensorAlongDim(wav_ae, 0),
"C": getIndexTensorAlongDim(wav_ae, 1),
"channel": getIndexTensorAlongDim(wav_ae, 1),
"S": getIndexTensorAlongDim(wav_ae, 2),
"sample": getIndexTensorAlongDim(wav_ae, 2),
"R": sample_rate,
"sample_rate": sample_rate,
"T": wav_ae.shape[2],
"sample_count": wav_ae.shape[2],
"N": wav_ae.shape[1],
"channel_count": wav_ae.shape[1],
} | generate_dim_variables(wav_ae)
tree = parse_expr(Audio);
visitor = UnifiedMathVisitor(wav_vars, wav_ae.shape)
result_tensor = visitor.visit(tree)
result_audio_wav = as_tensor(result_tensor, wav_ae.shape)
output = VideoFromComponents(
VideoComponents(
images=result_imgs, audio={"waveform": result_audio_wav, "sample_rate": sample_rate}, frame_rate=ac.frame_rate
)
)
return (output,)
+455
View File
@@ -0,0 +1,455 @@
from antlr4.error.ErrorListener import ErrorListener
from antlr4 import InputStream, CommonTokenStream
import torch
from .Parser.antlr_router import get_antlr_modules
MathExprLexer, MathExprParser, _ = get_antlr_modules()
import re
class LazyVariableDict(dict):
def __getitem__(self, key):
val = super().__getitem__(key)
if callable(val) and getattr(val, "is_lazy_var", False):
val = val()
self[key] = val
return val
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def copy(self):
return LazyVariableDict(self)
def __or__(self, other):
res = self.copy()
res.update(other)
return res
def __ror__(self, other):
res = LazyVariableDict(other)
res.update(self)
return res
def __ior__(self, other):
self.update(other)
return self
class ThrowingErrorListener(ErrorListener):
"""Error listener that raises ValueError on syntax errors."""
def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
raise ValueError(f"Syntax error in expression at line {line}, col {column}: {msg}")
def as_tensor(value, shape):
if getattr(value, "is_nested", False):
return value
if isinstance(value, torch.Tensor):
# Pass tensors through unchanged; the expression defines the output shape.
return value.contiguous()
if isinstance(value, (float, int)):
value = (value,)
return torch.broadcast_to(torch.Tensor(value).to(dtype=torch.float32), shape).contiguous()
if isinstance(value, (list, tuple)) and value and isinstance(value[0], torch.Tensor):
# Keep lists of tensors as-is (e.g. intentionally returning a list of latents).
return value
return torch.cat(value)
def parse_expr(expr: str):
"""Parse a math expression and return the parse tree."""
input_stream = InputStream(expr)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
parser.addErrorListener(ThrowingErrorListener())
return parser.start()
def getIndexTensorAlongDim(tensor, dim):
"""Create a tensor of indices along a dimension, broadcasted to full shape."""
shape = tensor.shape
values = torch.arange(shape[dim], dtype=torch.float32, device=tensor.device)
view_shape = [1] * len(shape)
view_shape[dim] = shape[dim]
values = values.view(*view_shape)
return values.expand(*shape)
def commonLazy(expr, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0):
"""Determine which lazy inputs are needed based on expression variables."""
variables = {"a": a, "b": b, "c": c, "d": d, "w": w, "x": x, "y": y, "z": z}
need_eval = []
input_stream = InputStream(expr)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
stream.fill()
for token in filter(lambda t: t.type == MathExprParser.VARIABLE, stream.tokens):
if token.text in variables and variables[token.text] is None:
need_eval.append(token.text)
return need_eval
def generate_dim_variables(tensor: torch.Tensor):
"""Generate index and size tensors for each dimension of the input tensor."""
variables = {}
for dim, size in enumerate(tensor.shape):
variables[f"D{dim}"] = getIndexTensorAlongDim(tensor, dim)
variables[f"S{dim}"] = float(size) # Scalar size, not tensor
return variables
def prepare_inputs(a, b, c, d):
"""
Ensures optional inputs b, c, d are zero-initialized like a if None.
Returns the prepared (a, b, c, d).
"""
b = b if b is not None else make_zero_like(a)
c = c if c is not None else make_zero_like(a)
d = d if d is not None else make_zero_like(a)
return a, b, c, d
def make_zero_like(ref):
"""
Create a zero-initialized version of the reference object, maintaining its structure.
Handles Conditioning, Audio, Latent, VideoComponents, and Tensors.
"""
if ref is None:
return None
# raw torch tensor
if torch.is_tensor(ref):
return torch.zeros_like(ref)
# Conditioning: list of tuples [(tensor, dict)]
if isinstance(ref, list) and len(ref) > 0 and isinstance(ref[0], (list, tuple)) and len(ref[0]) >= 2:
zeroed_cond = []
for entry in ref:
ref_tensor = entry[0]
ref_dict = entry[1]
if torch.is_tensor(ref_tensor):
new_dict = ref_dict.copy()
if "pooled_output" in new_dict and torch.is_tensor(new_dict["pooled_output"]):
new_dict["pooled_output"] = torch.zeros_like(new_dict["pooled_output"])
zeroed_cond.append((torch.zeros_like(ref_tensor), new_dict))
return zeroed_cond
# Audio or Latent: dict
if isinstance(ref, dict):
if "waveform" in ref: # Audio
return {"waveform": make_zero_like(ref["waveform"]), "sample_rate": ref["sample_rate"]}
if "samples" in ref: # Latent
return {"samples": make_zero_like(ref["samples"])}
# NestedTensor
if getattr(ref, "is_nested", False):
from comfy.nested_tensor import NestedTensor
return NestedTensor([torch.zeros_like(t) for t in ref.tensors])
# VideoComponents or other objects with images/audio attributes
if hasattr(ref, "images") and hasattr(ref, "audio"):
# Dynamically create same type (e.g. VideoComponents)
return type(ref)(
images=torch.zeros_like(ref.images),
audio={"waveform": torch.zeros_like(ref.audio["waveform"]), "sample_rate": ref.audio["sample_rate"]},
frame_rate=ref.frame_rate,
metadata=None,
)
return None
def get_tensor_device(value):
if torch.is_tensor(value):
return value.device
if getattr(value, "is_nested", False):
for tensor in value.tensors:
if torch.is_tensor(tensor):
return tensor.device
if isinstance(value, dict):
for item in value.values():
device = get_tensor_device(item)
if device is not None:
return device
if isinstance(value, (list, tuple)):
for item in value:
device = get_tensor_device(item)
if device is not None:
return device
return None
def move_to_device(value, device):
if value is None or device is None:
return value
if torch.is_tensor(value):
return value.to(device=device)
if getattr(value, "is_nested", False):
from comfy.nested_tensor import NestedTensor
return NestedTensor([move_to_device(t, device) for t in value.tensors])
if isinstance(value, dict):
return {k: move_to_device(v, device) for k, v in value.items()}
if isinstance(value, list):
return [move_to_device(v, device) for v in value]
if isinstance(value, tuple):
return tuple(move_to_device(v, device) for v in value)
if hasattr(value, "images") and hasattr(value, "audio"):
images = move_to_device(getattr(value, "images", None), device)
audio = move_to_device(getattr(value, "audio", None), device)
try:
return type(value)(
images=images,
audio=audio,
frame_rate=getattr(value, "frame_rate", None),
metadata=getattr(value, "metadata", None),
)
except Exception:
return value
return value
def normalize_to_common_shape(*tensors, mode="pad"):
"""
Normalize multiple tensors to a common shape.
- mode="pad": Pad with zeros to the max shape.
- mode="tile": Tile to match (or exceed) dimensions, then truncate excess.
"""
if mode not in ("pad", "tile"):
return tensors
valid_tensors = [t for t in tensors if torch.is_tensor(t)]
if len(valid_tensors) <= 1:
return tensors
# Check if all shapes are already the same
if all(t.shape == valid_tensors[0].shape for t in valid_tensors[1:]):
return tensors
# Determine common shape
target_ndim = max(t.ndim for t in valid_tensors)
max_shape = [0] * target_ndim
for t in valid_tensors:
for i in range(t.ndim):
# Align from trailing dimensions
max_shape[-(i + 1)] = max(max_shape[-(i + 1)], t.shape[-(i + 1)])
target_shape = tuple(max_shape)
def normalize_one(t, shape):
if t.shape == shape:
return t.contiguous()
# Match ndim first
curr_t = t
while curr_t.ndim < len(shape):
curr_t = curr_t.unsqueeze(0)
if mode == "tile":
# Calculate required repeats for each dimension
repeats = []
for s1, s2 in zip(curr_t.shape, shape):
if s1 == 0: # Handle zero-sized tensors
return curr_t
# Ceiling division to ensure coverage
repeat_times = (s2 + s1 - 1) // s1
repeats.append(repeat_times)
# Repeat and truncate
curr_t = curr_t.repeat(repeats)
slices = tuple(slice(0, s2) for s2 in shape)
return curr_t[slices].contiguous()
# Fallback to padding if not tile mode
out = torch.zeros(shape, dtype=curr_t.dtype, device=curr_t.device)
slices = tuple(slice(0, d) for d in curr_t.shape)
out[slices] = curr_t
return out.contiguous()
result = []
for t in tensors:
if torch.is_tensor(t):
result.append(normalize_one(t, target_shape))
else:
result.append(t.contiguous())
return tuple(result)
def get_v_variable(v_norm_dict, length_mismatch="error"):
"""
Collects V0, V1, ... from the dict (ignoring NestedTensor component keys
like V0_0, V1_1), and returns them as a list and count.
NestedTensor entries are represented as a list of their components so that
V[idx][comp] indexing works as expected.
"""
base_keys = sorted(
[k for k in v_norm_dict.keys() if re.fullmatch(r"V\d+", k)],
key=lambda x: int(x[1:])
)
listt = []
for k in base_keys:
val = v_norm_dict[k]
if getattr(val, "is_nested", False):
listt.append(list(val.tensors))
else:
listt.append(val)
return listt, len(listt)
def get_f_variable(f_dict):
"""
Collects F0, F1, ... from the dict, stacks them into an F 1D tensor,
and returns (F_stacked, F_count).
"""
sorted_keys = sorted([k for k in f_dict.keys() if k.startswith("F")], key=lambda x: int(x[1:]))
ordered_values = []
for k in sorted_keys:
val = f_dict[k]
if torch.is_tensor(val):
# If it's a tensor, just get the first element if it's a scalar or keep as is if it's already 1D?
# Usually F inputs are floats. If they are tensors, we'll try to treat them as values.
ordered_values.append(val.flatten()[0] if val.numel() > 0 else torch.tensor(0.0))
elif isinstance(val, (int, float)):
ordered_values.append(torch.tensor(float(val)))
else:
ordered_values.append(torch.tensor(0.0))
if not ordered_values:
return None, 0
try:
stacked = torch.stack(ordered_values)
return stacked, len(ordered_values)
except Exception:
return None, len(ordered_values)
def _collect_reads_only( node, needed_vars, assigned_vars, shadowed_vars):
if node is None:
return
node_type = type(node).__name__
if node_type == 'VariableExpContext':
var_name = node.VARIABLE().getText()
if var_name in shadowed_vars:
return
if var_name not in assigned_vars:
needed_vars.add(var_name)
return
if node_type == 'FunctionDefContext':
return
if node_type == 'VarDefContext':
for expr in node.expr():
_collect_reads_only(expr, needed_vars, assigned_vars, shadowed_vars)
return
for i in range(node.getChildCount()):
_collect_reads_only(node.getChild(i), needed_vars, assigned_vars, shadowed_vars)
def _collect_vars_from_node( node, needed_vars, assigned_vars, shadowed_vars):
"""Recursively collect variable reads from an AST node"""
if node is None:
return
node_type = type(node).__name__
# Found a variable read
if node_type == 'VariableExpContext':
var_name = node.VARIABLE().getText()
# Skip if shadowed
if var_name in shadowed_vars:
return
if var_name not in assigned_vars:
needed_vars.add(var_name)
return
# Skip
if node_type == 'FunctionDefContext':
return
# Recursively visit children
for i in range(node.getChildCount()):
_collect_vars_from_node(node.getChild(i), needed_vars, assigned_vars, shadowed_vars)
def checkLazyNew(Expression, V, F):
tree = None
parser = None
if isinstance(Expression,str):
tree = parse_expr(Expression)
parser = tree.parser
else:
tree = Expression
parser = tree.parser
# Support aliases
aliases = {"a": "V0", "b": "V1", "c": "V2", "d": "V3",
"w": "F0", "x": "F1", "y": "F2", "z": "F3"}
assigned_vars = set()
needed_vars = set()
# Process all top-level statements
for child in tree.children:
if not hasattr(child, 'getRuleIndex'):
continue
rule_name = parser.ruleNames[child.getRuleIndex()] if child.getRuleIndex() < len(parser.ruleNames) else None
# Process function definitions: scan for reads but ignore writes
if rule_name == 'funcDef':
func_params = set()
if child.paramList():
for param in child.paramList().VARIABLE():
func_params.add(param.getText())
_collect_reads_only(child, needed_vars, assigned_vars, func_params)
# Top-level assignments
elif rule_name == 'varDef':
var_name = child.VARIABLE().getText()
_collect_vars_from_node(child, needed_vars, assigned_vars, set())
assigned_vars.add(var_name)
# Track other top-level statements
else:
_collect_vars_from_node(child, needed_vars, assigned_vars, set())
# Normalize variable names through aliases
needed = set()
for var in needed_vars:
norm = aliases.get(var, var)
if var == "V":
needed.update(V.keys())
if var == "F":
needed.update(F.keys())
# Handle NestedTensor component variables like V1_0 or F2_3 by mapping back to V1/F2
comp_match = re.fullmatch(r"([VF]\d+)_(\d+)", norm)
if comp_match:
needed.add(comp_match.group(1))
elif re.fullmatch(r"[VF][0-9]+", norm):
needed.add(norm)
elif norm.endswith("_d"):
# Delta references like V0_d or the aliased a_d still require
# their underlying model, even when the bare name is never
# referenced elsewhere in the expression.
delta_base = aliases.get(norm[:-2], norm[:-2])
if re.fullmatch(r"[VF][0-9]+", delta_base):
needed.add(delta_base)
return needed
+228
View File
@@ -0,0 +1,228 @@
from .helper_functions import generate_dim_variables, parse_expr, as_tensor, get_v_variable, get_f_variable, get_tensor_device, move_to_device, LazyVariableDict, checkLazyNew
from .Parser.UnifiedMathVisitor import MathDict, UnifiedMathVisitor
import comfy.model_management
import torch
from collections.abc import Mapping
def calculate_lora_dict_autogrow(Expr, V, F, pbar=None, mapping=None, stack=[], use_compute_device=False):
"""
Calculate a new LORA_MODEL dict[str, Tensor] from math expressions applied
over the UNION of keys from all input LoRA dicts, using Autogrow inputs.
Unlike calculate_patches_autogrow (Model/VAE/CLIP), there is no
ModelPatcher/backup involved here - a LORA_MODEL is just raw tensors
(lora_up.weight, lora_down.weight, alpha, diff, ...). So V{n} is simply
that tensor as-is (zero-filled when a given key is missing from that
input), and there is no V{n}_d - these tensors already *are* deltas.
"""
if V is None: V = {}
if F is None: F = {}
if mapping is None: mapping = {}
needed_vars = checkLazyNew(Expr, V, F)
needed_v_names = [v_name for v_name in V.keys() if v_name in needed_vars]
if not needed_v_names:
needed_v_names = [v_name for v_name in V.keys() if V.get(v_name) is not None]
if not any(V.get(v_name) is not None for v_name in needed_v_names):
return {}
# Collect all unique keys from all inputs, preserving order from first input
all_keys_list = []
seen_keys = set()
for d in V.values():
if d is None:
continue
for key in d.keys():
if key not in seen_keys:
seen_keys.add(key)
all_keys_list.append(key)
tree = parse_expr(Expr) if isinstance(Expr, str) else Expr
original_device = None
compute_device = comfy.model_management.get_torch_device() if use_compute_device else None
const_f_vars = {k: val if val is not None else 0.0 for k, val in F.items()}
const_f_alias_vars = {
alias: (F[target] if F[target] is not None else 0.0)
for alias, target in mapping.items()
if target in F
}
mapping_items = tuple(mapping.items())
input_dicts = {}
for v_name in needed_v_names:
d = V.get(v_name)
if d is None:
continue
if original_device is None:
original_device = get_tensor_device(d)
if compute_device is not None and original_device is not None and compute_device != original_device:
d = move_to_device(d, compute_device)
input_dicts[v_name] = d
layer_count = len(all_keys_list)
def make_lazy(value_fn):
def lazy_eval():
return value_fn()
lazy_eval.is_lazy_var = True
return lazy_eval
result = {}
for layer_idx, key in enumerate(all_keys_list):
variables = LazyVariableDict(const_f_vars)
variables.update(const_f_alias_vars)
variables["L"] = float(layer_idx)
variables["layer"] = float(layer_idx)
variables["LC"] = float(layer_count)
variables["layer_count"] = float(layer_count)
variables["K"] = key
variables["key"] = key
ref_tensor = None
present = {}
for v_name in V.keys():
d = input_dicts.get(v_name)
if d is None:
continue
t = d.get(key, None)
if t is not None:
present[v_name] = t
if ref_tensor is None:
ref_tensor = t
if ref_tensor is None:
continue
for v_name in V.keys():
if v_name in present:
t = present[v_name]
variables[v_name] = make_lazy(lambda t=t: t)
else:
variables[v_name] = make_lazy(lambda rt=ref_tensor: torch.zeros_like(rt))
# Populate aliases for V (a, b, c, d)
for alias, target in mapping_items:
if target in variables:
variables[alias] = variables[target]
elif target.startswith("V") and target[1:].isdigit():
variables[alias] = make_lazy(lambda rt=ref_tensor: torch.zeros_like(rt))
v_cache = [None]
def lazy_v_stack():
if v_cache[0] is None:
v_cache[0] = get_v_variable(variables)
return v_cache[0][0]
lazy_v_stack.is_lazy_var = True
def lazy_v_count():
if v_cache[0] is None:
v_cache[0] = get_v_variable(variables)
return float(v_cache[0][1])
lazy_v_count.is_lazy_var = True
variables["V"] = lazy_v_stack
variables["Vcnt"] = lazy_v_count
variables["V_count"] = lazy_v_count
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
variables = variables | generate_dim_variables(ref_tensor)
visitor = UnifiedMathVisitor(variables, ref_tensor.shape, state_storage=stack)
res = visitor.visit(tree)
res = as_tensor(res, ref_tensor.shape)
if compute_device is not None and original_device is not None and original_device != compute_device:
res = res.to(device=original_device)
result[key] = res
if pbar is not None:
pbar.update(1)
return result
def calculate_lora_dict_dict_mode(Expr, V, F, pbar=None, mapping=None, stack=[], use_compute_device=False):
if V is None:
V = {}
if F is None:
F = {}
if mapping is None:
mapping = {}
needed_vars = checkLazyNew(Expr, V, F)
needed_v_names = [name for name in V if name in needed_vars]
if "V0" in V and "V0" not in needed_v_names:
needed_v_names.append("V0")
if not needed_v_names:
needed_v_names = [name for name, value in V.items() if value is not None]
input_dicts = {name: V[name] for name in needed_v_names if V.get(name) is not None}
if not input_dicts:
return {}
all_keys = []
seen_keys = set()
for value in input_dicts.values():
for key in value:
if key not in seen_keys:
seen_keys.add(key)
all_keys.append(key)
compute_device = comfy.model_management.get_torch_device() if use_compute_device else None
original_device = get_tensor_device(next(iter(input_dicts.values())))
if compute_device is not None and original_device is not None and compute_device != original_device:
input_dicts = {name: move_to_device(value, compute_device) for name, value in input_dicts.items()}
references = {}
for key in all_keys:
for value in input_dicts.values():
tensor = value.get(key)
if tensor is not None:
references[key] = tensor
break
def weight_dict(name):
value = input_dicts.get(name, {})
return MathDict({key: value.get(key, torch.zeros_like(references[key])) for key in all_keys})
variables = LazyVariableDict({name: value if value is not None else 0.0 for name, value in F.items()})
for alias, target in mapping.items():
if target in F:
variables[alias] = F[target] if F[target] is not None else 0.0
for name in needed_v_names:
variables[name] = weight_dict(name)
for alias, target in mapping.items():
if target in variables:
variables[alias] = variables[target]
visitor_device = compute_device if compute_device is not None else next(iter(references.values())).device
result = UnifiedMathVisitor(variables, device=visitor_device, state_storage=stack).visit(parse_expr(Expr) if isinstance(Expr, str) else Expr)
if not isinstance(result, Mapping):
if isinstance(result, (float, int)):
result = MathDict({key: torch.full_like(reference, result) for key, reference in references.items()})
else:
raise ValueError("Dictionary mode expressions must return a dictionary")
if tuple(result) != tuple(all_keys):
raise ValueError("Dictionary mode expressions must preserve all LoRA keys")
if compute_device is not None and original_device is not None and original_device != compute_device:
result = {key: value.to(device=original_device) for key, value in result.items()}
if pbar is not None:
pbar.update(len(result))
return dict(result)
+541
View File
@@ -0,0 +1,541 @@
from .helper_functions import generate_dim_variables, parse_expr, as_tensor, get_v_variable, get_f_variable, get_tensor_device, move_to_device, LazyVariableDict, checkLazyNew
from .Parser.UnifiedMathVisitor import MathDict, ChangeTrackingDict, UnifiedMathVisitor
import comfy
import comfy.model_management
import comfy.model_patcher
import comfy.lora
import torch
def _get_model_patcher(obj):
# A MODEL input *is* a ModelPatcher already - not something wrapping one.
if isinstance(obj, comfy.model_patcher.ModelPatcher):
return obj
patcher = getattr(obj, "patcher", None)
if patcher is None and hasattr(obj, "model"):
patcher = getattr(obj.model, "patcher", None)
return patcher
def get_effective_state_dict(obj):
if hasattr(obj, "patcher") and hasattr(obj.patcher, "model_state_dict"):
return obj.patcher.model_state_dict()
if hasattr(obj, "model_state_dict"):
return obj.model_state_dict()
if hasattr(obj, "model") and hasattr(obj.model, "patcher") and hasattr(obj.model.patcher, "model_state_dict"):
return obj.model.patcher.model_state_dict()
if hasattr(obj, "model") and hasattr(obj.model, "model_state_dict"):
return obj.model.model_state_dict()
if hasattr(obj, "model") and hasattr(obj.model, "state_dict"):
return obj.model.state_dict()
if hasattr(obj, "state_dict"):
return obj.state_dict()
return None
def get_effective_weight(obj, key):
patcher = _get_model_patcher(obj)
if patcher is not None and hasattr(patcher, "patch_weight_to_device"):
try:
return patcher.patch_weight_to_device(key, return_weight=True)
except Exception:
pass
if hasattr(obj, "patch_weight_to_device"):
try:
return obj.patch_weight_to_device(key, return_weight=True)
except Exception:
pass
sd = get_effective_state_dict(obj)
if sd is not None:
return sd.get(key, None)
return None
def get_base_weight(obj, key):
"""True pre-patch weight for `key`, regardless of whether the patcher
currently has this key's patches baked into the live model (`backup`
holds the pristine value whenever that happened; `use_ejected` does not
undo weight patches, only injections, so it can't be relied on here)."""
patcher = _get_model_patcher(obj)
if patcher is not None:
backup = getattr(patcher, "backup", None)
if backup is not None:
bk = backup.get(key, None)
if bk is not None:
return bk.weight
model = getattr(patcher, "model", None)
if model is not None:
try:
weight, _, _ = comfy.model_patcher.get_key_weight(model, key)
return weight
except Exception:
pass
sd = get_effective_state_dict(obj)
if sd is not None:
return sd.get(key, None)
return None
def get_patched_weight(obj, key, base_weight):
"""Fully patched weight for `key`, computed fresh from `base_weight` so
the result never depends on whether the model happens to be baked
in-place already (reusing an already-baked weight as input would
double-apply patches whose strength_model != 1.0)."""
if base_weight is None:
return None
patcher = _get_model_patcher(obj)
if patcher is not None:
patches = getattr(patcher, "patches", None)
if patches is not None and key in patches:
try:
return comfy.lora.calculate_weight(patches[key], base_weight.clone(), key)
except Exception:
pass
return base_weight
def coerce_like(weight, reference, device=None):
if isinstance(weight, torch.Tensor) and isinstance(reference, torch.Tensor):
target_device = device if device is not None else reference.device
if weight.device != target_device or weight.dtype != reference.dtype:
return weight.to(device=target_device, dtype=reference.dtype)
return weight
def calculate_patches(Model, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0, use_compute_device=False):
"""Legacy calculate_patches for backward compatibility."""
return calculate_patches_autogrow(Model, V={"V0": a, "V1": b, "V2": c, "V3": d}, F={"F0": w, "F1": x, "F2": y, "F3": z}, pbar=None, mapping={"a": "V0", "b": "V1", "c": "V2", "d": "V3", "w": "F0", "x": "F1", "y": "F2", "z": "F3"}, use_compute_device=use_compute_device)
def calculate_patches_autogrow(Expr, V, F, pbar=None, mapping=None, stack=[], use_compute_device=False):
"""
Calculate patches for model-like objects (Model, VAE, CLIP) using Autogrow inputs.
Iterates over the UNION of keys from all input models to support merging disjoint architectures/patches.
In autogrow mode:
- The expression is evaluated once PER KEY (per-layer iteration)
- V0, V1, V2, V3 contain only the weight tensor for the current key (not all keys)
- Expression must return a TENSOR (the patch for this key)
- Returning a dict will raise an error; use dict_mode=True instead
Performance note:
- If expression applies the same operation to all keys (e.g., V0 *= 2), it will iterate
through each key in Python, making it O(num_keys) in Python overhead
- For efficiency with bulk operations on multiple keys, use dict_mode=True instead
- dict_mode evaluates the expression once with V0 as a complete dict, allowing batch operations
Args:
Expr: The math expression string or parse tree.
V: Dictionary of input models (Autogrow Input).
F: Dictionary of input floats (Autogrow Float).
mapping: Optional dict mapping legacy alias names to V keys (e.g. {"a": "V0"}).
"""
if V is None: V = {}
if F is None: F = {}
if mapping is None: mapping = {}
needed_vars = checkLazyNew(Expr, V, F)
needed_v_names = [v_name for v_name in V.keys() if v_name in needed_vars]
if not needed_v_names:
needed_v_names = [v_name for v_name in V.keys() if V.get(v_name) is not None]
# Collect all unique keys from all models, preserving order from first model
all_keys_list = [] # Preserves order
seen_keys = set() # Fast O(1) duplicate checking
models = [v for v in V.values() if v is not None]
if not models:
return {}
for m in models:
sd = get_effective_state_dict(m)
if sd is None:
sd_keys = []
else:
sd_keys = sd.keys()
for key in sd_keys:
if key not in seen_keys:
seen_keys.add(key)
all_keys_list.append(key)
# Function to get weight from a valid object
def get_weight(obj, key):
return get_effective_weight(obj, key)
tree = None
if isinstance(Expr,str):
tree = parse_expr(Expr)
else:
tree = Expr
patches = {}
original_device = None
compute_device = comfy.model_management.get_torch_device() if use_compute_device else None
needs_deltas = not (isinstance(Expr, str) and "_d" not in Expr)
const_f_vars = {k: val if val is not None else 0.0 for k, val in F.items()}
const_f_alias_vars = {
alias: (F[target] if F[target] is not None else 0.0)
for alias, target in mapping.items()
if target in F
}
mapping_items = tuple(mapping.items())
current_state_dicts = {}
for v_name in needed_v_names:
v_val = V.get(v_name)
if v_val is None:
continue
current_sd = get_effective_state_dict(v_val)
if current_sd is None:
continue
if original_device is None:
original_device = get_tensor_device(current_sd)
if compute_device is not None and original_device is not None and compute_device != original_device:
current_sd = move_to_device(current_sd, compute_device)
current_state_dicts[v_name] = current_sd
# Progress bar if possible (comfy.utils.ProgressBar might assume unthreaded?)
# Just skip for utility or use if substantial.
layer_count = len(all_keys_list)
def make_lazy(value_fn):
def lazy_eval():
return value_fn()
lazy_eval.is_lazy_var = True
return lazy_eval
for layer_idx, key in enumerate(all_keys_list):
variables = LazyVariableDict(const_f_vars)
# Also populate mapped aliases for F (w, x, y, z)
variables.update(const_f_alias_vars)
variables["L"] = float(layer_idx)
variables["layer"] = float(layer_idx)
variables["LC"] = float(layer_count)
variables["layer_count"] = float(layer_count)
variables["K"] = key
variables["key"] = key
# Inject weights for this key from V models
valid_key = False
ref_tensor = None
present_weights = {}
present_bases = {}
for v_name in V.keys():
current_sd = current_state_dicts.get(v_name)
if current_sd is None:
continue
raw_tensor = current_sd.get(key, None)
if raw_tensor is not None:
v_val = V.get(v_name)
base_tensor = get_base_weight(v_val, key) if v_val is not None else None
if base_tensor is None:
base_tensor = raw_tensor
base_tensor = coerce_like(base_tensor, raw_tensor, device=compute_device)
patched_tensor = get_patched_weight(v_val, key, base_tensor) if v_val is not None else None
if patched_tensor is None:
patched_tensor = raw_tensor
patched_tensor = coerce_like(patched_tensor, raw_tensor, device=compute_device)
present_weights[v_name] = patched_tensor
if needs_deltas:
present_bases[v_name] = base_tensor
if ref_tensor is None:
ref_tensor = patched_tensor
valid_key = True
if not valid_key:
continue
# Find reference shape
if ref_tensor is None:
continue # Should not happen if valid_key is true
# Fill missing models with lazy values so V[n] keeps working
v_cache = [None]
def lazy_v_stack():
if v_cache[0] is None:
v_cache[0] = get_v_variable(variables)
return v_cache[0][0]
lazy_v_stack.is_lazy_var = True
def lazy_v_count():
if v_cache[0] is None:
v_cache[0] = get_v_variable(variables)
return float(v_cache[0][1])
lazy_v_count.is_lazy_var = True
for v_name in V.keys():
if v_name in present_weights:
w_tensor = present_weights[v_name]
variables[v_name] = make_lazy(lambda wt=w_tensor: wt)
if needs_deltas:
base_tensor = present_bases.get(v_name, w_tensor)
variables[f"{v_name}_d"] = make_lazy(lambda wt=w_tensor, bt=base_tensor: wt - bt)
else:
variables[v_name] = make_lazy(lambda rt=ref_tensor: torch.zeros_like(rt))
if needs_deltas:
variables[f"{v_name}_d"] = make_lazy(lambda rt=ref_tensor: torch.zeros_like(rt))
# Populate aliases for V (a, b, c, d)
for alias, target in mapping_items:
if target in variables:
variables[alias] = variables[target]
elif target in V:
# V key exists in input dict but this specific layer key is missing
variables[alias] = make_lazy(lambda rt=ref_tensor: torch.zeros_like(rt))
else:
# Target doesn't exist at all (e.g., V1 not provided)
# Check if it's a V-key pattern and zero-fill
if target.startswith("V") and target[1:].isdigit():
variables[alias] = make_lazy(lambda rt=ref_tensor: torch.zeros_like(rt))
variables["V"] = lazy_v_stack
variables["Vcnt"] = lazy_v_count
variables["V_count"] = lazy_v_count
f_stacked, f_cnt = get_f_variable(F)
if f_stacked is not None:
variables["F"] = f_stacked
variables["Fcnt"] = float(f_cnt)
variables["F_count"] = float(f_cnt)
variables = variables | generate_dim_variables(ref_tensor)
# Execute math
# Store original V0 before visiting, since visitor may modify it
original_v0 = variables.get("V0")
if original_v0 is not None:
# If it's a tensor, clone it; if it's a dict, copy its items
if isinstance(original_v0, torch.Tensor):
original_v0 = original_v0.clone()
elif isinstance(original_v0, dict):
original_v0 = MathDict({k: (v.clone() if isinstance(v, torch.Tensor) else v) for k, v in original_v0.items()})
visitor = UnifiedMathVisitor(variables, ref_tensor.shape,state_storage=stack)
res = visitor.visit(tree)
# In autogrow mode, expression must return a single tensor (patch for this key)
# If a dict is returned, that's an error - use dict_mode instead
if isinstance(res, dict) and not isinstance(res, torch.Tensor):
raise ValueError(f"Autogrow mode expects tensor output, got dict. Use dict_mode=True for dict output.")
res = as_tensor(res, ref_tensor.shape)
# Extract original for this specific key
original = None
if original_v0 is not None:
if isinstance(original_v0, torch.Tensor):
original = original_v0
elif isinstance(original_v0, dict):
# Should not happen in autogrow mode - V0 is always a tensor
original = original_v0.get(key)
if original is None:
original = torch.zeros_like(res)
diff = res - original
# Clean up: don't store zero patches
if not torch.all(diff == 0):
patches[key] = (diff,)
if pbar is not None:
pbar.update(1)
if compute_device is not None and original_device is not None and original_device != compute_device:
patches = {
key: ((value[0].to(device=original_device),) if isinstance(value, tuple) and value and torch.is_tensor(value[0]) else value)
for key, value in patches.items()
}
return patches
def calculate_patches_dict_mode(Expr, V, F, pbar=None, mapping=None, stack=[], use_compute_device=False):
"""
Calculate patches for model-like objects (Model, VAE, CLIP) using Dictionary mode.
Evaluates the expression once for all keys simultaneously.
In dict_mode:
- The expression is evaluated ONCE (not per-key)
- V0, V1, V2, V3 are MathDict objects containing all weights across all keys
- Expression can modify individual keys: V0[key] *= 2
- Expression can apply bulk operations: V0 *= 2
- Expression must return a DICT/MATHDICT with exactly the same keys as the input models
- The returned dict must preserve the original key structure (cannot add/remove keys)
Performance:
- More efficient than autogrow_mode for bulk operations since evaluation is done once
- Suitable for complex transformations that need access to all keys simultaneously
Args:
Expr: The math expression string or parse tree.
V: Dictionary of input models (Dict Mode Input).
F: Dictionary of input floats (Dict Mode Float).
mapping: Optional dict mapping legacy alias names to V keys.
"""
if V is None:
V = {}
if F is None:
F = {}
if mapping is None:
mapping = {}
needed_vars = checkLazyNew(Expr, V, F)
needed_v_names = [name for name in V if name in needed_vars]
if "V0" in V and "V0" not in needed_v_names:
needed_v_names.append("V0")
if not needed_v_names:
needed_v_names = [name for name, value in V.items() if value is not None]
models = [value for value in V.values() if value is not None]
if not models:
return {}
all_keys = []
seen_keys = set()
for model in models:
state_dict = get_effective_state_dict(model)
if state_dict is None:
continue
for key in state_dict:
if key not in seen_keys:
seen_keys.add(key)
all_keys.append(key)
tree = parse_expr(Expr) if isinstance(Expr, str) else Expr
compute_device = comfy.model_management.get_torch_device() if use_compute_device else None
original_device = None
state_dicts = {}
for name in needed_v_names:
value = V.get(name)
if value is None:
continue
state_dict = get_effective_state_dict(value)
if state_dict is None:
continue
if original_device is None:
original_device = get_tensor_device(state_dict)
if compute_device is not None and original_device is not None and compute_device != original_device:
state_dict = move_to_device(state_dict, compute_device)
state_dicts[name] = state_dict
if not state_dicts:
return {}
all_keys = [key for key in all_keys if any(key in state_dict for state_dict in state_dicts.values())]
# Mirrors calculate_patches_autogrow's needs_deltas: a plain substring
# check on the expression text, since checkLazyNew's returned variable
# names never carry "_d" suffixes (only bare V{n}/alias names), so a
# membership test against needed_vars can never match here.
needs_deltas = not (isinstance(Expr, str) and "_d" not in Expr)
delta_names = set(needed_v_names) if needs_deltas else set()
weights = {name: {} for name in needed_v_names}
bases = {name: {} for name in delta_names}
references = {}
for key in all_keys:
for name in needed_v_names:
state_dict = state_dicts.get(name)
raw_weight = state_dict.get(key) if state_dict is not None else None
if raw_weight is None:
continue
model = V[name]
base_weight = get_base_weight(model, key)
if base_weight is None:
base_weight = raw_weight
base_weight = coerce_like(base_weight, raw_weight, device=compute_device)
patched_weight = get_patched_weight(model, key, base_weight)
if patched_weight is None:
patched_weight = raw_weight
patched_weight = coerce_like(patched_weight, raw_weight, device=compute_device)
weights[name][key] = patched_weight
if name in delta_names:
bases[name][key] = base_weight
if key not in references:
references[key] = patched_weight
if not references:
return {}
def weight_dict(name):
"""Create a ChangeTrackingDict that monitors changes to weights"""
tracking_dict = ChangeTrackingDict({key: weights[name].get(key, torch.zeros_like(references[key])) for key in all_keys})
return tracking_dict
variables = LazyVariableDict({name: value if value is not None else 0.0 for name, value in F.items()})
for alias, target in mapping.items():
if target in F:
variables[alias] = F[target] if F[target] is not None else 0.0
for name in needed_v_names:
variables[name] = weight_dict(name)
for alias, target in mapping.items():
if target in variables:
variables[alias] = variables[target]
for name in delta_names:
def make_delta_dict(name=name):
return MathDict({
key: weights[name][key] - bases[name][key] if key in weights[name] else torch.zeros_like(references[key])
for key in all_keys
})
make_delta_dict.is_lazy_var = True
variables[f"{name}_d"] = make_delta_dict
visitor_device = compute_device if compute_device is not None else next(iter(references.values())).device
# Store a copy of V0 BEFORE visiting, since the visitor may modify it
v0_dict = variables.get("V0", MathDict({key: torch.zeros_like(reference) for key, reference in references.items()}))
originals = MathDict({k: (v.clone() if isinstance(v, torch.Tensor) else v) for k, v in v0_dict.items()})
result = UnifiedMathVisitor(variables, device=visitor_device, state_storage=stack).visit(tree)
if not isinstance(result, (MathDict, dict)):
if isinstance(result, (float, int)):
result = MathDict({key: torch.full_like(reference, result) for key, reference in references.items()})
else:
raise ValueError("Dictionary mode expressions must return a dictionary")
if tuple(result) != tuple(all_keys):
raise ValueError("Dictionary mode expressions must preserve all model keys")
patches = {}
# Optimization: if result is a ChangeTrackingDict, only process changed keys
changed_keys_set = None
if isinstance(result, ChangeTrackingDict):
changed_keys_set = result.get_changed_keys()
keys_to_process = changed_keys_set
else:
# Fallback: process all keys
keys_to_process = all_keys
for key in keys_to_process:
value = result.get(key)
if value is None:
# Key was removed or doesn't exist
if pbar is not None:
pbar.update(1)
continue
if not torch.is_tensor(value):
value = torch.full_like(references[key], value)
diff = value - originals[key]
if not torch.all(diff == 0):
patches[key] = (diff,)
if pbar is not None:
pbar.update(1)
if compute_device is not None and original_device is not None and original_device != compute_device:
patches = {key: (value[0].to(device=original_device),) for key, value in patches.items()}
return patches
+188
View File
@@ -0,0 +1,188 @@
from .SigmasMathNode import SigmasMathNode
from .GuiderMathNode import GuiderMathNode
from .deprecated.NoiseMathNode import NoiseMathNodeOLD
from .deprecated.AudioMathNode import AudioMathNodeOLD
from .deprecated.ConditioningMathNode import ConditioningMathNodeOLD
from .deprecated.ImageMathNode import ImageMathNodeOLD
from .deprecated.LatentMathNode import LatentMathNodeOLD
from .deprecated.MaskMathNode import MaskMathNodeOLD
from .deprecated.ModelMathNode import ModelMathNodeOLD
from .deprecated.VaeMathNode import VAEMathNodeOLD
from .deprecated.ClipMathNode import CLIPMathNodeOLD
from .deprecated.VideoMathNode import VideoMathNodeOLD
from .deprecated.FloatMathNode import FloatMathNodeOLD
from .FloatMathNode import FloatMathNode
from .ConditioningMathNode import ConditioningMathNode
from .LatentMathNode import LatentMathNode
from .ImageMathNode import ImageMathNode
from .MaskMathNode import MaskMathNode
from .VideoMathNode import VideoMathNode
from .ModelMathNode import ModelMathNode
from .VaeMathNode import VAEMathNode
from .ClipMathNode import CLIPMathNode
from .LoraMathNode import LoraMathNode
from .SpectrogramToAudioNode import SpectrogramToAudio
from .AudioToSpectrogramNode import AudioToSpectrogram
from .NoiseMathNode import NoiseMathNode
from .AudioMathNode import AudioMathNode
from .StringMathNode import StringMathNode
from .SelectiveGuiderMathNode import SelectiveGuiderMathNode
from .StackUnBatcher import StackUnBatcher
from .ScriptTextWindow import ScriptTextInput
from .BatchLoraNode import BatchLoraNode
from .BatchLoraHooksNode import BatchLoraHooksNode
from comfy_api.latest import ComfyExtension, io
class IntToFloatNode(io.ComfyNode):
"""
Converts int to float.
"""
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_IntToFloat",
display_name="Int -> Float",
category="More math/converters",
inputs=[
io.Int.Input(id="value", default=0),
],
outputs=[
io.Float.Output(),
],
)
@classmethod
def execute(cls, value):
return (float(value),)
class FloatToIntNode(io.ComfyNode):
"""
Converts float to int.
"""
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_FloatToInt",
category="More math/converters",
display_name="Float -> Int",
inputs=[
io.Float.Input(id="value", default=0.0),
],
outputs=[
io.Int.Output(),
],
)
@classmethod
def execute(cls, value):
return (int(value),)
class BoolToIntNode(io.ComfyNode):
"""
Converts bool to int.
"""
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_BoolToInt",
display_name="Bool -> Int",
category="More math/converters",
inputs=[
io.Boolean.Input(id="value", default=0),
],
outputs=[
io.Int.Output(),
],
)
@classmethod
def execute(cls, value):
return (int(value),)
class IntToBoolNode(io.ComfyNode):
"""
Converts int to bool.
"""
@classmethod
def define_schema(cls) -> io.Schema:
""" """
return io.Schema(
node_id="mrmth_IntToBool",
category="More math/converters",
display_name="Int -> bool",
inputs=[
io.Int.Input(id="value", default=0.0),
],
outputs=[
io.Boolean.Output(),
],
)
@classmethod
def execute(cls, value):
return (bool(value),)
class MoreMathExtension(ComfyExtension):
def __init__(self):
pass
@classmethod
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
ConditioningMathNode,
ConditioningMathNodeOLD,
ModelMathNode,
ModelMathNodeOLD,
CLIPMathNode,
CLIPMathNodeOLD,
VAEMathNode,
VAEMathNodeOLD,
LoraMathNode,
LatentMathNode,
LatentMathNodeOLD,
ImageMathNode,
ImageMathNodeOLD,
MaskMathNode,
MaskMathNodeOLD,
FloatMathNode,
FloatMathNodeOLD,
NoiseMathNodeOLD,
IntToFloatNode,
FloatToIntNode,
BoolToIntNode,
IntToBoolNode,
AudioMathNodeOLD,
VideoMathNode,
VideoMathNodeOLD,
AudioToSpectrogram,
SpectrogramToAudio,
SigmasMathNode,
GuiderMathNode,
NoiseMathNode,
AudioMathNode,
StringMathNode,
ScriptTextInput,
SelectiveGuiderMathNode,
StackUnBatcher,
BatchLoraNode,
BatchLoraHooksNode
]
async def comfy_entrypoint() -> MoreMathExtension:
return MoreMathExtension()
+7 -3
View File
@@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta"
[project]
name = "more_math"
version = "0.0.2"
description = "Adds math nodes for numbers and types which do not need it."
version = "1.1.0"
tooltip = "Adds math nodes for numbers and types which do not need it."
authors = [
{name = "Daniel Martinek", email = "danda.martinek@gmail.com"}
]
readme = "README.md"
license = {text = "GNU General Public License v3"}
classifiers = []
description = "Adds math nodes for FLOAT, STRING, CONDITIONING, LATENT, IMAGE, MASK, NOISE, AUDIO, VIDEO, MODEL, CLIP, VAE, SIGMAS, and GUIDER types and allows usage of math expressions with large number of functions and variables, not limited to inputs. List of those is on github of this extension."
dependencies = [
]
@@ -37,7 +38,7 @@ Repository = "https://github.com/mcDandy/more_math.git"
[tool.comfy]
PublisherId = "mkdaniel"
DisplayName = "More Math"
Icon = ""
Icon = "https://raw.githubusercontent.com/mcDandy/more_math/60c026ecc2cd5f7600e82d87afbe795747e52890/icon.png"
[tool.setuptools.package-data]
@@ -81,3 +82,6 @@ select = [
[tool.ruff.lint.flake8-quotes]
inline-quotes = "double"
[tool.ruff.lint.per-file-ignores]
"more_math/Parser/*.py" = ["F401", "F403", "F405"]
+2
View File
@@ -0,0 +1,2 @@
[pytest]
pythonpath = . ../..
+3 -1
View File
@@ -1 +1,3 @@
antlr4-python3-runtime>=4.11.0
antlr4-python3-runtime
torch
pillow
-117
View File
@@ -1,117 +0,0 @@
from inspect import cleandoc
import torch
from antlr4 import CommonTokenStream, InputStream
from .Parser.MathExprParser import MathExprParser
from .Parser.MathExprLexer import MathExprLexer
from .Parser.TensorEvalVisitor import TensorEvalVisitor
from .helper_functions import getIndexTensorAlongDim
class AudioMathNode:
"""
This node enables the use of math expressions on AUDIO tensors.
INPUTS:
a, b, c, d:
AUDIO, bound to variables with the same name. Defaults to zero AUDIO if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Audio expression:
String, describing expression to apply to audio tensors.
OUTPUTS:
AUDIO:
Returns an AUDIO object that contains the result of the math expression applied to the input audio tensors.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
"""
"""
return {
"required": {
"a": ("AUDIO", {
"description": "Input audio tensor"
}),
"AudioExpr": ("STRING", {
"multiline": False,
"default": "a*(1-w)+b*w",
"description": "Expression to apply on input audio tensors"
}),
},
"optional": {
"b": ("AUDIO", {
"default": 0,
"description": "Second input audio tensor"
}),
"c": ("AUDIO", {
"default": 0,
"description": "Third input audio tensor"
}),
"d": ("AUDIO", {
"default": 0,
"description": "Fourth input audio tensor"
}),
"w": ("FLOAT", {
"default": 0,
"forceInput": True
}),
"x": ("FLOAT", {
"default": 0,
"forceInput": True
}),
"y": ("FLOAT", {
"default": 0,
"forceInput": True
}),
"z": ("FLOAT", {
"default": 0,
"forceInput": True
}),
}
}
RETURN_TYPES = ("AUDIO",)
DESCRIPTION = cleandoc(__doc__)
FUNCTION = "audioMathNode"
CATEGORY = "More math"
def audioMathNode(self, a, AudioExpr, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0):
bv = b if b else {'waveform':torch.zeros_like(a['waveform']),'sample_rate':a['sample_rate']}
cv = c if c else {'waveform':torch.zeros_like(a['waveform']),'sample_rate':a['sample_rate']}
dv = d if d else {'waveform':torch.zeros_like(a['waveform']),'sample_rate':a['sample_rate']}
B = getIndexTensorAlongDim(a['waveform'], 0)
C = getIndexTensorAlongDim(a['waveform'], 1)
S = getIndexTensorAlongDim(a['waveform'], 2)
R = torch.full_like(S, a['sample_rate'], dtype=torch.float32)
T = torch.full_like(S, a['waveform'].shape[2], dtype=torch.float32)
variables = {
'a': a['waveform'], 'b': bv['waveform'], 'c': cv['waveform'], 'd': dv['waveform'],
'w': w, 'x': x, 'y': y, 'z': z,
'B': B, 'C': C, 'S': S,'R': R, 'T' : T
}
input_stream = InputStream(AudioExpr)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
tree = parser.expr()
visitor = TensorEvalVisitor(variables, a['waveform'].shape)
result_tensor = visitor.visit(tree)
print("Audio Tree\n" + tree.toStringTree(recog=parser))
print("Result Tensor: ", result_tensor)
print("Result Tensor Shape: ", result_tensor.shape)
# Create output dictionary with the same sample rate
output = {
'waveform': result_tensor,
'sample_rate': a['sample_rate']
}
return (output,)
-149
View File
@@ -1,149 +0,0 @@
from inspect import cleandoc
from antlr4 import CommonTokenStream, InputStream
import torch
from .Parser.MathExprParser import MathExprParser
from .Parser.MathExprLexer import MathExprLexer
from .Parser.TensorEvalVisitor import TensorEvalVisitor
class ConditioningMathNode:
"""
This node enables the use of math on conditionings. It is recommended to keep the Tensor expression the same as the pooled_output expression.
INPUTS:
a, b, c, d:
Conditioning, bound to variables with the same name. Defaults to zero conditioning if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Tensor expression:
String, describing expression to mix tensor part of conditioning. Tensor probably describes the composition of the image as it has the say what is on the image. Valid functions are sin, cos, tan, abs, sqrt, min, max, norm. Valid operators are +, -, *, /, ^, %. Usable constants are e and pi.
Pooled output expression:
String, describing expression to mix pooled_output part of conditioning. pooled_output does not have that much say in the image but can change some details of the image or completly change the image in some cases, probably also used for positional and temporal conditioning. Expression uses same syntax as Tensor expression.
OUTPUTS:
CONDITIONING:
Returns a CONDITIONING object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
"""
"""
return {
"required": {
"a": ("CONDITIONING", {
}),
"Tensor": ("STRING", {
"multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
"default": "a*(1-w)+b*w",
"description": "Describes composition of the image. Valid functions are sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, asinh, acosh, atanh, abs, sqrt, ln, log, exp, pow, min, max, norm, floor, ceil, round, gamma. Valid operators are +, -, *, /, %, ^,!˛&,|. Usable constants are e and pi."
}),
"pooled_output": ("STRING", {
"multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
"default": "a*(1-w)+b*w",
"description": "Can change some details of the image, probably also used for positional and temporal conditioning."
}),
},
"optional": {
"b": ("CONDITIONING", {
"default": 0,
}),
"c": ("CONDITIONING", {
"default": 0,
}),
"d": ("CONDITIONING", {
"default": 0,
}),
"w": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"x": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"y": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"z": ("FLOAT", {
"default": 0,
"forceInput":True
}),
# "int_field": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
# "float_field": ("FLOAT", {"default": 0.5, "min": -10.0, "max": 10.0, "step": 0.001}),
}
}
RETURN_TYPES = ("CONDITIONING",)
#RETURN_NAMES = ("image_output_name",)
DESCRIPTION = cleandoc(__doc__)
FUNCTION = "condMathNode"
#OUTPUT_NODE = False
#OUTPUT_TOOLTIPS = ("",) # Tooltips for the output node
CATEGORY = "More math"
def condMathNode(self, Tensor,pooled_output, a, b=None, c=None, d=None,w=0.0,x=0.0,y=0.0,z=0.0):
if b is None:
b = [[torch.zeros_like(a[0][0]), {"pooled_output": torch.zeros_like(a[0][1]["pooled_output"])}]]
if c is None:
c = [[torch.zeros_like(a[0][0]), {"pooled_output": torch.zeros_like(a[0][1]["pooled_output"])}]]
if d is None:
d = [[torch.zeros_like(a[0][0]), {"pooled_output": torch.zeros_like(a[0][1]["pooled_output"])}]]
ta = a[0][0].clone()
tb = b[0][0].clone()
pa = a[0][1]["pooled_output"].clone()
pb = b[0][1]["pooled_output"].clone()
tc = c[0][0].clone()
td = d[0][0].clone()
pc = c[0][1]["pooled_output"].clone()
pd = d[0][1]["pooled_output"].clone()
variables = {'a': ta, 'b': tb, 'c': tc, 'd': td, 'w': w, 'x': x, 'y': y, 'z': z}
input_stream = InputStream(Tensor)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
tree = parser.expr()
print("Tensor\n"+tree.toStringTree(recog=parser))
visitor = TensorEvalVisitor(variables,ta.shape)
result1 = visitor.visit(tree)
variables = {'a': pa, 'b': pb, 'c': pc, 'd': pd, 'w': w, 'x': x, 'y': y, 'z': z}
input_stream = InputStream(Tensor)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
tree = parser.expr()
print("pooled_output\n"+tree.toStringTree(recog=parser))
visitor = TensorEvalVisitor(variables,pa.shape)
result2 = visitor.visit(tree)
result = [[result1, {"pooled_output": result2}]]
return (result,)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
#@classmethod
#def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
-120
View File
@@ -1,120 +0,0 @@
from inspect import cleandoc
from math import e
from antlr4 import CommonTokenStream, InputStream
from .helper_functions import ThrowingErrorListener
from .Parser.MathExprParser import MathExprParser
from .Parser.MathExprLexer import MathExprLexer
from .Parser.FloatEvalVisitor import FloatEvalVisitor
class FloatMathNode:
"""
This node enables the use of math expressions on Latents.
INPUTS:
a, b, c, d:
Floats, bound to variables with the same name. Defaults to 0.0 if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Latent expression:
String, describing expression to mix latents. Valid functions are sin, cos, tan, abs, sqrt, min, max, norm. Valid operators are +, -, *, /, ^, %. Usable constants are e and pi.
OUTPUTS:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
"""
"""
return {
"required": {
"a": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"FloatFunc": ("STRING", {
"multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
"default": "a*(1-w)+b*w",
"description": "Expression to use on inputs"
}),
},
"optional": {
"b": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"c": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"d": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"w": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"x": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"y": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"z": ("FLOAT", {
"default": 0,
"forceInput":True
}),
# "int_field": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
# "float_field": ("FLOAT", {"default": 0.5, "min": -10.0, "max": 10.0, "step": 0.001}),
}
}
RETURN_TYPES = ("FLOAT",)
#RETURN_NAMES = ("image_output_name",)
DESCRIPTION = cleandoc(__doc__)
FUNCTION = "fltMathNode"
#OUTPUT_NODE = False
#OUTPUT_TOOLTIPS = ("",) # Tooltips for the output node
CATEGORY = "More math"
def fltMathNode(self, FloatFunc, a, b=0.0, c=0.0, d=0.0, w=0.0, x=0.0, y=0.0, z=0.0):
variables = {'a': a, 'b': b, 'c': c, 'd': d, 'w': w, 'x': x, 'y': y, 'z': z}
input_stream = InputStream(FloatFunc)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
parser.addErrorListener(ThrowingErrorListener())
tree = parser.expr()
print("Tree\n"+tree.toStringTree(recog=parser))
visitor = FloatEvalVisitor(variables)
result = visitor.visit(tree)
return (result,)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
#@classmethod
#def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
-132
View File
@@ -1,132 +0,0 @@
from inspect import cleandoc
from antlr4 import CommonTokenStream
from antlr4.atn.LexerActionExecutor import InputStream
import torch
from .helper_functions import ThrowingErrorListener, getIndexTensorAlongDim
from .Parser.MathExprParser import MathExprParser
from .Parser.MathExprLexer import MathExprLexer
from .Parser.TensorEvalVisitor import TensorEvalVisitor
class ImageMathNode:
"""
This node enables the use of math expressions on Latents.
INPUTS:
a, b, c, d:
Latent, bound to variables with the same name. Defaults to zero latent if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Image expression:
String, describing expression to mix images. Valid functions are sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, asinh, acosh, atanh, abs, sqrt, ln, log, exp, pow, min, max, norm, floor, ceil, round, gamma. Valid operators are +, -, *, /, %, ^,!˛&,|. Usable constants are e and pi.
OUTPUTS:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
shape = []
B = None
C = None
X = None
Y = None
@classmethod
def INPUT_TYPES(s):
"""
"""
return {
"required": {
"a": ("IMAGE", {
}),
"Image": ("STRING", {
"multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
"default": "a*(1-w)+b*w",
"description": "Describes expression to apply to the image."
}),
},
"optional": {
"b": ("IMAGE", {
"default": 0,
}),
"c": ("IMAGE", {
"default": 0,
}),
"d": ("IMAGE", {
"default": 0,
}),
"w": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"x": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"y": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"z": ("FLOAT", {
"default": 0,
"forceInput":True
}),
# "int_field": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
# "float_field": ("FLOAT", {"default": 0.5, "min": -10.0, "max": 10.0, "step": 0.001}),
}
}
RETURN_TYPES = ("IMAGE",)
#RETURN_NAMES = ("image_output_name",)
DESCRIPTION = cleandoc(__doc__)
FUNCTION = "imgMathNode"
#OUTPUT_NODE = False
#OUTPUT_TOOLTIPS = ("",) # Tooltips for the output node
CATEGORY = "More math"
def imgMathNode(self, Image, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0):
b = torch.zeros_like(a) if b is None else b
c = torch.zeros_like(a) if c is None else c
d = torch.zeros_like(a) if d is None else d
batch = range(0,a.shape[0],1)
width = range(0,a.shape[2],1)
height = range(0,a.shape[1],1)
color = range(0,a.shape[3],1)
B = getIndexTensorAlongDim(a, 0)
W = getIndexTensorAlongDim(a, 2)
H = getIndexTensorAlongDim(a, 1)
C = getIndexTensorAlongDim(a, 3)
variables = {'a': a, 'b': b, 'c': c, 'd': d, 'w': w, 'x': x, 'y': y, 'z': z,'B':B,'X':W,'Y':H,'C':C,'W':a.shape[1],'H':a.shape[2] }
input_stream = InputStream(Image)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
parser.addErrorListener(ThrowingErrorListener())
tree = parser.expr()
print("Tree\n"+tree.toStringTree(recog=parser))
visitor = TensorEvalVisitor(variables,a.shape)
result = visitor.visit(tree)
return (result,)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
#@classmethod
#def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
-125
View File
@@ -1,125 +0,0 @@
from inspect import cleandoc
from math import e
from antlr4 import CommonTokenStream, InputStream
import torch
from .helper_functions import ThrowingErrorListener, getIndexTensorAlongDim
from .Parser.MathExprParser import MathExprParser
from .Parser.MathExprLexer import MathExprLexer
from .Parser.TensorEvalVisitor import TensorEvalVisitor
class LatentMathNode:
"""
This node enables the use of math expressions on Latents.
INPUTS:
a, b, c, d:
Latent, bound to variables with the same name. Defaults to zero latent if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Latent expression:
String, describing expression to aply to latents.
OUTPUTS:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
"""
"""
return {
"required": {
"a": ("LATENT", {
}),
"Latent": ("STRING", {
"multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
"default": "a*(1-w)+b*w",
"description": "Expression to apply on input latents"
}),
},
"optional": {
"b": ("LATENT", {
"default": 0,
}),
"c": ("LATENT", {
"default": 0,
}),
"d": ("LATENT", {
"default": 0,
}),
"w": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"x": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"y": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"z": ("FLOAT", {
"default": 0,
"forceInput":True
}),
# "int_field": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
# "float_field": ("FLOAT", {"default": 0.5, "min": -10.0, "max": 10.0, "step": 0.001}),
}
}
RETURN_TYPES = ("LATENT",)
#RETURN_NAMES = ("image_output_name",)
DESCRIPTION = cleandoc(__doc__)
FUNCTION = "latMathNode"
#OUTPUT_NODE = False
#OUTPUT_TOOLTIPS = ("",) # Tooltips for the output node
CATEGORY = "More math"
def latMathNode(self, Latent, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0):
a = a["samples"]
b = torch.zeros_like(a) if b is None else b["samples"]
c = torch.zeros_like(a) if c is None else c["samples"]
d = torch.zeros_like(a) if d is None else d["samples"]
B = getIndexTensorAlongDim(a, 0)
W = getIndexTensorAlongDim(a, 3)
H = getIndexTensorAlongDim(a, 2)
C = getIndexTensorAlongDim(a, 1)
variables = {'a': a, 'b': b, 'c': c, 'd': d, 'w': w, 'x': x, 'y': y, 'z': z,'B':B,'X':W,'Y':H,'C':C,'W':a.shape[3],'H':a.shape[2]}
input_stream = InputStream(Latent)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
parser.addErrorListener(ThrowingErrorListener())
tree = parser.expr()
print("Tree\n"+tree.toStringTree(recog=parser))
visitor = TensorEvalVisitor(variables,a.shape)
result1 = visitor.visit(tree)
result = {"samples": result1}
return (result,)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
#@classmethod
#def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
-153
View File
@@ -1,153 +0,0 @@
from inspect import cleandoc
from math import e
from antlr4 import CommonTokenStream, InputStream
import torch
from .helper_functions import ThrowingErrorListener, getIndexTensorAlongDim
from .Parser.MathExprParser import MathExprParser
from .Parser.MathExprLexer import MathExprLexer
from .Parser.TensorEvalVisitor import TensorEvalVisitor
class NoiseMathNode:
"""
This node enables the use of math expressions on Latents.
INPUTS:
a, b, c, d:
Noise generators, bound to variables with the same name. Defaults to zero latent if not provided.
w, x, y, z:
Floats, bound to variables of the expression. Defaults to 0.0 if not provided.
Latent expression:
String, describing expression to mix noise.
OUTPUTS:
LATENT:
Returns a LATENT object that contains the result of the math expression applied to the input conditionings.
"""
def __init__(self):
va= None
vb= None
vc= None
vd= None
vw= 0.0
vx= 0.0
vy= 0.0
vz= 0.0
expr="";
@classmethod
def INPUT_TYPES(s):
"""
"""
return {
"required": {
"a": ("NOISE", {
}),
"Noise": ("STRING", {
"multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
"default": "a*(1-w)+b*w",
"description": "Expression describing manipulation of noise."
}),
},
"optional": {
"b": ("NOISE", {
"default": 0,
}),
"c": ("NOISE", {
"default": 0,
}),
"d": ("NOISE", {
"default": 0,
}),
"w": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"x": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"y": ("FLOAT", {
"default": 0,
"forceInput":True
}),
"z": ("FLOAT", {
"default": 0,
"forceInput":True
}),
# "int_field": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
# "float_field": ("FLOAT", {"default": 0.5, "min": -10.0, "max": 10.0, "step": 0.001}),
}
}
RETURN_TYPES = ("NOISE",)
#RETURN_NAMES = ("image_output_name",)
DESCRIPTION = cleandoc(__doc__)
FUNCTION = "noiMathNode"
seed = 0
#OUTPUT_NODE = False
#OUTPUT_TOOLTIPS = ("",) # Tooltips for the output node
CATEGORY = "More math"
def noiMathNode(self, Noise, a, b=None, c=None, d=None, w=0.0, x=0.0, y=0.0, z=0.0):
self.va = a
self.vb = b
self.vc = c
self.vd = d
self.vw = w
self.vx = x
self.vy = y
self.vz = z
self.expr = Noise
return (self,)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
#@classmethod
#def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
def generate_noise(self, input_latent:torch.Tensor) -> torch.Tensor:
if self.vb is None:
self.vb = torch.zeros_like(input_latent["samples"])
else:
self.vb = self.vb.generate_noise(input_latent)
if self.vc is None:
self.vc = torch.zeros_like(input_latent["samples"])
else:
self.vc = self.vc.generate_noise(input_latent)
if self.vd is None:
self.vd = torch.zeros_like(input_latent["samples"])
else:
self.vd = self.vd.generate_noise(input_latent)
B = getIndexTensorAlongDim(input_latent["samples"], 0)
W = getIndexTensorAlongDim(input_latent["samples"], 3)
H = getIndexTensorAlongDim(input_latent["samples"], 2)
C = getIndexTensorAlongDim(input_latent["samples"], 1)
variables = {'a': self.va.generate_noise(input_latent), 'b': self.vb, 'c': self.vc, 'd': self.vd, 'w': self.vw, 'x': self.vx, 'y': self.vy, 'z': self.vz,'B':B,'X':W,'Y':H,'C':C,'W':input_latent["samples"].shape[3],'H':input_latent["samples"].shape[2],'I':input_latent["samples"]}
input_stream = InputStream(self.expr)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = MathExprParser(stream)
parser.addErrorListener(ThrowingErrorListener())
tree = parser.expr()
print("Tree\n"+tree.toStringTree(recog=parser))
visitor = TensorEvalVisitor(variables,variables['a'].shape)
result = visitor.visit(tree)
return result
-150
View File
@@ -1,150 +0,0 @@
import math
from tkinter import SE
from sympy import true
import torch
from .MathExprVisitor import MathExprVisitor
class FloatEvalVisitor(MathExprVisitor):
def __init__(self, variables):
self.variables = variables
def visitNumberExp(self, ctx):
print("Visiting number expression:", ctx.getText())
return float(ctx.getText())
def visitConstantExp(self, ctx):
name = ctx.getText().lower()
if name == "pi":
return 3.141592653589793
if name == "e":
return 2.718281828459045
raise ValueError(f"Unknown constant: {name}")
def visitVariableExp(self, ctx):
name = ctx.getText()
if name not in self.variables:
raise ValueError(f"Variable '{name}' not found")
return self.variables[name]
def visitParenExp(self, ctx):
return self.visit(ctx.expr())
def visitUnaryPlus(self, ctx):
return +self.visit(ctx.unaryExpr())
def visitUnaryMinus(self, ctx):
return -self.visit(ctx.unaryExpr())
def visitAddExp(self, ctx):
return self.visit(ctx.addExpr()) + self.visit(ctx.mulExpr())
def visitSubExp(self, ctx):
return self.visit(ctx.addExpr()) - self.visit(ctx.mulExpr())
def visitMulExp(self, ctx):
print("Visiting multiplication expression:", ctx.getText())
if ctx.mulExpr() is None or ctx.powExpr() is None:
raise ValueError("Invalid multiplication expression")
return self.visit(ctx.mulExpr()) * self.visit(ctx.powExpr())
def visitDivExp(self, ctx):
return self.visit(ctx.mulExpr()) / self.visit(ctx.powExpr())
def visitModExp(self, ctx):
return self.visit(ctx.mulExpr()) % self.visit(ctx.powExpr())
def visitPowExp(self, ctx):
return math.pow(self.visit(ctx.unaryExpr()), self.visit(ctx.powExpr()))
def visitNeExp(self, ctx):
return float(self.visit(ctx.compExpr()) != self.visit(ctx.addExpr()))
def visitEqExp(self, ctx):
return float(self.visit(ctx.compExpr()) == self.visit(ctx.addExpr()))
def visitGtExp(self, ctx):
return float(self.visit(ctx.compExpr()) > self.visit(ctx.addExpr()))
def visitLtExp(self, ctx):
return float(self.visit(ctx.compExpr()) < self.visit(ctx.addExpr()))
def visitGeExp(self, ctx):
return float(self.visit(ctx.compExpr()) >= self.visit(ctx.addExpr()))
def visitLeExp(self, ctx):
return float(self.visit(ctx.compExpr()) <= self.visit(ctx.addExpr()))
def visitToUnary(self, ctx):
return self.visit(ctx.unaryExpr())
def visitToPow(self, ctx):
return self.visit(ctx.powExpr())
def visitToMul(self, ctx):
return self.visit(ctx.mulExpr())
def visitToAdd(self, ctx):
return self.visit(ctx.addExpr())
def visitToGt(self, ctx):
return self.visit(ctx.gtExpr())
def visitToLt(self, ctx):
return self.visit(ctx.ltExpr())
def visitToEq(self, ctx):
return self.visit(ctx.eqExpr())
def visitToNeq(self, ctx):
return self.visit(ctx.neqExpr())
def visitToGe(self, ctx):
return self.visit(ctx.gteExpr())
def visitToLe(self, ctx):
return self.visit(ctx.lteExpr())
# Single-argument functions
def visitSinFunc(self, ctx): return math.sin(self.visit(ctx.expr()))
def visitCosFunc(self, ctx): return math.cos(self.visit(ctx.expr()))
def visitTanFunc(self, ctx): return math.tan(self.visit(ctx.expr()))
def visitAsinFunc(self, ctx): return math.asin(self.visit(ctx.expr()))
def visitAcosFunc(self, ctx): return math.acos(self.visit(ctx.expr()))
def visitAtanFunc(self, ctx): return math.atan(self.visit(ctx.expr()))
def visitSinhFunc(self, ctx): return math.sinh(self.visit(ctx.expr()))
def visitCoshFunc(self, ctx): return math.cosh(self.visit(ctx.expr()))
def visitTanhFunc(self, ctx): return math.tanh(self.visit(ctx.expr()))
def visitAsinhFunc(self, ctx): return math.asinh(self.visit(ctx.expr()))
def visitAcoshFunc(self, ctx): return math.acosh(self.visit(ctx.expr()))
def visitAtanhFunc(self, ctx): return math.atanh(self.visit(ctx.expr()))
def visitAbsFunc(self, ctx): return math.abs(self.visit(ctx.expr()))
def visitSqrtFunc(self, ctx): return math.sqrt(self.visit(ctx.expr()))
def visitLnFunc(self, ctx): return math.log(self.visit(ctx.expr()))
def visitLogFunc(self, ctx): return math.log10(self.visit(ctx.expr()))
def visitExpFunc(self, ctx): return math.exp(self.visit(ctx.expr()))
def visitNormFunc(self, ctx): return math.sqrt(math.avg(x**2 for x in self.visit(ctx.expr())))
def visitFloorFunc(self, ctx): return math.floor(self.visit(ctx.expr()))
def visitSigmoidFunc(self, ctx): return 1/(1+math.exp(-self.visit(ctx.expr())))
def visitCeilFunc(self, ctx): return math.ceil(self.visit(ctx.expr()))
def visitRoundFunc(self, ctx): return math.round(self.visit(ctx.expr()))
def visitGammaFunc(self, ctx): return math.gamma(self.visit(ctx.expr())).exp()
# Two-argument functions
def visitPowFunc(self, ctx):
return math.pow(self.visit(ctx.expr(0)), self.visit(ctx.expr(1)))
def visitAtan2Func(self, ctx):
return math.atan2(self.visit(ctx.expr(0)), self.visit(ctx.expr(1)))
# N-argument functions
def visitSMinFunc(self, ctx):
args = [self.visit(e) for e in ctx.expr()]
return math.min(args)
def visitSMaxFunc(self, ctx):
args = [self.visit(e) for e in ctx.expr()]
return math.max(args)
def visitFunc1Exp(self, ctx):
return self.visitChildren(ctx)
def visitFunc2Exp(self, ctx):
return self.visitChildren(ctx)
def visitFuncNExp(self, ctx):
return self.visitChildren(ctx)
def visitAtomExp(self, ctx):
return self.visitChildren(ctx)
def visitFunc2Expr(self, ctx):
return self.visit(ctx.getChild(0)) # forward to Atan2Func, PowFunc, etc.
def visitExpr(self, ctx):
print("Visiting expression:", ctx.getText())
return self.visitChildren(ctx)
-157
View File
@@ -1,157 +0,0 @@
grammar MathExpr;
// Top-level expression, with operator precedence (lowest to highest)
expr
: atom | compExpr
;
compExpr
: compExpr GT addExpr # GtExp
| compExpr GE addExpr # GeExp
| compExpr LT addExpr # LtExp
| compExpr LE addExpr # LeExp
| compExpr EQ addExpr # EqExp
| compExpr NE addExpr # NeExp
| addExpr # ToAdd
;
addExpr
: addExpr PLUS mulExpr # AddExp
| addExpr MINUS mulExpr # SubExp
| mulExpr # ToMul
;
mulExpr
: mulExpr MULT powExpr # MulExp
| mulExpr DIV powExpr # DivExp
| mulExpr MOD powExpr # ModExp
| powExpr # ToPow
;
powExpr
: unaryExpr POW powExpr # PowExp
| unaryExpr # ToUnary
;
unaryExpr
: PLUS unaryExpr # UnaryPlus
| MINUS unaryExpr # UnaryMinus
| atom # ToAtom
;
// Atoms: function calls, variable, number, constant, or parenthesized expression
atom
: func1 # Func1Exp
| func2 # Func2Exp
| func3 # Func3Exp
| funcN # FuncNExp
| VARIABLE # VariableExp
| NUMBER # NumberExp
| CONSTANT # ConstantExp
| '(' expr ')' # ParenExp
;
// Single-argument functions
func1
: SIN '(' expr ')' # SinFunc
| COS '(' expr ')' # CosFunc
| TAN '(' expr ')' # TanFunc
| ASIN '(' expr ')' # AsinFunc
| ACOS '(' expr ')' # AcosFunc
| ATAN '(' expr ')' # AtanFunc
| SINH '(' expr ')' # SinhFunc
| COSH '(' expr ')' # CoshFunc
| TANH '(' expr ')' # TanhFunc
| ASINH '(' expr ')' # AsinhFunc
| ACOSH '(' expr ')' # AcoshFunc
| ATANH '(' expr ')' # AtanhFunc
| ABS '(' expr ')' # AbsFunc
| SQRT '(' expr ')' # SqrtFunc
| LN '(' expr ')' # LnFunc
| LOG '(' expr ')' # LogFunc
| EXP '(' expr ')' # ExpFunc
| TNORM '(' expr ')' # TNormFunc
| SNORM '(' expr ')' # SNormFunc
| FLOOR '(' expr ')' # FloorFunc
| CEIL '(' expr ')' # CeilFunc
| ROUND '(' expr ')' # RoundFunc
| GAMMA '(' expr ')' # GammaFunc
| SIGM '(' expr ')' # sigmoidFunc
| SFFT '(' expr ')' # sfftFunc
| SIFFT '(' expr ')' # sifftFunc
| ANGL '(' expr ')' # anglFunc
;
// Two-argument functions
func2
: POWE '(' expr ',' expr ')' # PowFunc
| ATAN2 '(' expr ',' expr ')' # Atan2Func
| TMIN '(' expr ',' expr ')' # TMinFunc
| TMAX '(' expr ',' expr ')' # TMaxFunc
;
func3
: CLAMP '(' expr ',' expr ',' expr ')' # ClampFunc
;
// N-argument functions (at least 2 arguments)
funcN
: SMIN '(' expr (',' expr)+ ')' # SMinFunc
| SMAX '(' expr (',' expr)+ ')' # SMaxFunc
;
// LEXER RULES
SIN : 'sin';
COS : 'cos';
TAN : 'tan';
ASIN : 'asin';
ACOS : 'acos';
ATAN : 'atan';
ATAN2 : 'atan2';
SINH : 'sinh';
COSH : 'cosh';
TANH : 'tanh';
ASINH : 'asinh';
ACOSH : 'acosh';
ATANH : 'atanh';
ABS : 'abs';
SQRT : 'sqrt';
LN : 'ln';
LOG : 'log';
EXP : 'exp';
SMIN : 'smin';
SMAX : 'smax';
TMIN : 'tmin';
TMAX : 'tmax';
TNORM : 'tnorm';
SNORM : 'snorm';
FLOOR : 'floor';
CEIL : 'ceil';
ROUND : 'round';
GAMMA : 'gamma';
POWE : 'pow';
SIGM : 'sigm';
CLAMP : 'clamp';
SFFT : 'fft';
SIFFT : 'ifft';
ANGL : 'angle';
PLUS : '+';
MINUS : '-';
MULT : '*';
DIV : '/';
MOD : '%';
POW : '^';
GE : '>=';
GT : '>';
LE : '<=';
LT : '<';
EQ : '==';
NE : '!=';
CONSTANT : ('pi'|'PI'|'e'|'E');
NUMBER : [0-9]+ ('.' [0-9]+)?;
VARIABLE : [a-zA-Z_] [a-zA-Z_0-9]*;
WS : [ \t\r\n]+ -> skip;
File diff suppressed because one or more lines are too long
-102
View File
@@ -1,102 +0,0 @@
T__0=1
T__1=2
T__2=3
SIN=4
COS=5
TAN=6
ASIN=7
ACOS=8
ATAN=9
ATAN2=10
SINH=11
COSH=12
TANH=13
ASINH=14
ACOSH=15
ATANH=16
ABS=17
SQRT=18
LN=19
LOG=20
EXP=21
SMIN=22
SMAX=23
TMIN=24
TMAX=25
TNORM=26
SNORM=27
FLOOR=28
CEIL=29
ROUND=30
GAMMA=31
POWE=32
SIGM=33
CLAMP=34
SFFT=35
SIFFT=36
ANGL=37
PLUS=38
MINUS=39
MULT=40
DIV=41
MOD=42
POW=43
GE=44
GT=45
LE=46
LT=47
EQ=48
NE=49
CONSTANT=50
NUMBER=51
VARIABLE=52
WS=53
'('=1
')'=2
','=3
'sin'=4
'cos'=5
'tan'=6
'asin'=7
'acos'=8
'atan'=9
'atan2'=10
'sinh'=11
'cosh'=12
'tanh'=13
'asinh'=14
'acosh'=15
'atanh'=16
'abs'=17
'sqrt'=18
'ln'=19
'log'=20
'exp'=21
'smin'=22
'smax'=23
'tmin'=24
'tmax'=25
'tnorm'=26
'snorm'=27
'floor'=28
'ceil'=29
'round'=30
'gamma'=31
'pow'=32
'sigm'=33
'clamp'=34
'fft'=35
'ifft'=36
'angle'=37
'+'=38
'-'=39
'*'=40
'/'=41
'%'=42
'^'=43
'>='=44
'>'=45
'<='=46
'<'=47
'=='=48
'!='=49
File diff suppressed because one or more lines are too long
-236
View File
@@ -1,236 +0,0 @@
# Generated from MathExpr.g4 by ANTLR 4.13.2
from antlr4 import *
from io import StringIO
import sys
if sys.version_info[1] > 5:
from typing import TextIO
else:
from typing.io import TextIO
def serializedATN():
return [
4,0,53,346,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,
2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,
13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,
19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,
26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,31,7,31,2,32,7,
32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,2,38,7,38,2,
39,7,39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7,
45,2,46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2,
52,7,52,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,4,
1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,8,1,8,
1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,1,11,
1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,
1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,
1,15,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,
1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,21,1,21,
1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,
1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,
1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,29,
1,29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1,31,1,31,
1,31,1,31,1,32,1,32,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,
1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,
1,36,1,36,1,37,1,37,1,38,1,38,1,39,1,39,1,40,1,40,1,41,1,41,1,42,
1,42,1,43,1,43,1,43,1,44,1,44,1,45,1,45,1,45,1,46,1,46,1,47,1,47,
1,47,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,49,3,49,318,8,49,1,50,
4,50,321,8,50,11,50,12,50,322,1,50,1,50,4,50,327,8,50,11,50,12,50,
328,3,50,331,8,50,1,51,1,51,5,51,335,8,51,10,51,12,51,338,9,51,1,
52,4,52,341,8,52,11,52,12,52,342,1,52,1,52,0,0,53,1,1,3,2,5,3,7,
4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,
16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,
27,55,28,57,29,59,30,61,31,63,32,65,33,67,34,69,35,71,36,73,37,75,
38,77,39,79,40,81,41,83,42,85,43,87,44,89,45,91,46,93,47,95,48,97,
49,99,50,101,51,103,52,105,53,1,0,5,2,0,69,69,101,101,1,0,48,57,
3,0,65,90,95,95,97,122,4,0,48,57,65,90,95,95,97,122,3,0,9,10,13,
13,32,32,352,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,
1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,
1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,
1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,
1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,
1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,59,
1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69,
1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,0,77,1,0,0,0,0,79,
1,0,0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,87,1,0,0,0,0,89,
1,0,0,0,0,91,1,0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,1,0,0,0,0,99,
1,0,0,0,0,101,1,0,0,0,0,103,1,0,0,0,0,105,1,0,0,0,1,107,1,0,0,0,
3,109,1,0,0,0,5,111,1,0,0,0,7,113,1,0,0,0,9,117,1,0,0,0,11,121,1,
0,0,0,13,125,1,0,0,0,15,130,1,0,0,0,17,135,1,0,0,0,19,140,1,0,0,
0,21,146,1,0,0,0,23,151,1,0,0,0,25,156,1,0,0,0,27,161,1,0,0,0,29,
167,1,0,0,0,31,173,1,0,0,0,33,179,1,0,0,0,35,183,1,0,0,0,37,188,
1,0,0,0,39,191,1,0,0,0,41,195,1,0,0,0,43,199,1,0,0,0,45,204,1,0,
0,0,47,209,1,0,0,0,49,214,1,0,0,0,51,219,1,0,0,0,53,225,1,0,0,0,
55,231,1,0,0,0,57,237,1,0,0,0,59,242,1,0,0,0,61,248,1,0,0,0,63,254,
1,0,0,0,65,258,1,0,0,0,67,263,1,0,0,0,69,269,1,0,0,0,71,273,1,0,
0,0,73,278,1,0,0,0,75,284,1,0,0,0,77,286,1,0,0,0,79,288,1,0,0,0,
81,290,1,0,0,0,83,292,1,0,0,0,85,294,1,0,0,0,87,296,1,0,0,0,89,299,
1,0,0,0,91,301,1,0,0,0,93,304,1,0,0,0,95,306,1,0,0,0,97,309,1,0,
0,0,99,317,1,0,0,0,101,320,1,0,0,0,103,332,1,0,0,0,105,340,1,0,0,
0,107,108,5,40,0,0,108,2,1,0,0,0,109,110,5,41,0,0,110,4,1,0,0,0,
111,112,5,44,0,0,112,6,1,0,0,0,113,114,5,115,0,0,114,115,5,105,0,
0,115,116,5,110,0,0,116,8,1,0,0,0,117,118,5,99,0,0,118,119,5,111,
0,0,119,120,5,115,0,0,120,10,1,0,0,0,121,122,5,116,0,0,122,123,5,
97,0,0,123,124,5,110,0,0,124,12,1,0,0,0,125,126,5,97,0,0,126,127,
5,115,0,0,127,128,5,105,0,0,128,129,5,110,0,0,129,14,1,0,0,0,130,
131,5,97,0,0,131,132,5,99,0,0,132,133,5,111,0,0,133,134,5,115,0,
0,134,16,1,0,0,0,135,136,5,97,0,0,136,137,5,116,0,0,137,138,5,97,
0,0,138,139,5,110,0,0,139,18,1,0,0,0,140,141,5,97,0,0,141,142,5,
116,0,0,142,143,5,97,0,0,143,144,5,110,0,0,144,145,5,50,0,0,145,
20,1,0,0,0,146,147,5,115,0,0,147,148,5,105,0,0,148,149,5,110,0,0,
149,150,5,104,0,0,150,22,1,0,0,0,151,152,5,99,0,0,152,153,5,111,
0,0,153,154,5,115,0,0,154,155,5,104,0,0,155,24,1,0,0,0,156,157,5,
116,0,0,157,158,5,97,0,0,158,159,5,110,0,0,159,160,5,104,0,0,160,
26,1,0,0,0,161,162,5,97,0,0,162,163,5,115,0,0,163,164,5,105,0,0,
164,165,5,110,0,0,165,166,5,104,0,0,166,28,1,0,0,0,167,168,5,97,
0,0,168,169,5,99,0,0,169,170,5,111,0,0,170,171,5,115,0,0,171,172,
5,104,0,0,172,30,1,0,0,0,173,174,5,97,0,0,174,175,5,116,0,0,175,
176,5,97,0,0,176,177,5,110,0,0,177,178,5,104,0,0,178,32,1,0,0,0,
179,180,5,97,0,0,180,181,5,98,0,0,181,182,5,115,0,0,182,34,1,0,0,
0,183,184,5,115,0,0,184,185,5,113,0,0,185,186,5,114,0,0,186,187,
5,116,0,0,187,36,1,0,0,0,188,189,5,108,0,0,189,190,5,110,0,0,190,
38,1,0,0,0,191,192,5,108,0,0,192,193,5,111,0,0,193,194,5,103,0,0,
194,40,1,0,0,0,195,196,5,101,0,0,196,197,5,120,0,0,197,198,5,112,
0,0,198,42,1,0,0,0,199,200,5,115,0,0,200,201,5,109,0,0,201,202,5,
105,0,0,202,203,5,110,0,0,203,44,1,0,0,0,204,205,5,115,0,0,205,206,
5,109,0,0,206,207,5,97,0,0,207,208,5,120,0,0,208,46,1,0,0,0,209,
210,5,116,0,0,210,211,5,109,0,0,211,212,5,105,0,0,212,213,5,110,
0,0,213,48,1,0,0,0,214,215,5,116,0,0,215,216,5,109,0,0,216,217,5,
97,0,0,217,218,5,120,0,0,218,50,1,0,0,0,219,220,5,116,0,0,220,221,
5,110,0,0,221,222,5,111,0,0,222,223,5,114,0,0,223,224,5,109,0,0,
224,52,1,0,0,0,225,226,5,115,0,0,226,227,5,110,0,0,227,228,5,111,
0,0,228,229,5,114,0,0,229,230,5,109,0,0,230,54,1,0,0,0,231,232,5,
102,0,0,232,233,5,108,0,0,233,234,5,111,0,0,234,235,5,111,0,0,235,
236,5,114,0,0,236,56,1,0,0,0,237,238,5,99,0,0,238,239,5,101,0,0,
239,240,5,105,0,0,240,241,5,108,0,0,241,58,1,0,0,0,242,243,5,114,
0,0,243,244,5,111,0,0,244,245,5,117,0,0,245,246,5,110,0,0,246,247,
5,100,0,0,247,60,1,0,0,0,248,249,5,103,0,0,249,250,5,97,0,0,250,
251,5,109,0,0,251,252,5,109,0,0,252,253,5,97,0,0,253,62,1,0,0,0,
254,255,5,112,0,0,255,256,5,111,0,0,256,257,5,119,0,0,257,64,1,0,
0,0,258,259,5,115,0,0,259,260,5,105,0,0,260,261,5,103,0,0,261,262,
5,109,0,0,262,66,1,0,0,0,263,264,5,99,0,0,264,265,5,108,0,0,265,
266,5,97,0,0,266,267,5,109,0,0,267,268,5,112,0,0,268,68,1,0,0,0,
269,270,5,102,0,0,270,271,5,102,0,0,271,272,5,116,0,0,272,70,1,0,
0,0,273,274,5,105,0,0,274,275,5,102,0,0,275,276,5,102,0,0,276,277,
5,116,0,0,277,72,1,0,0,0,278,279,5,97,0,0,279,280,5,110,0,0,280,
281,5,103,0,0,281,282,5,108,0,0,282,283,5,101,0,0,283,74,1,0,0,0,
284,285,5,43,0,0,285,76,1,0,0,0,286,287,5,45,0,0,287,78,1,0,0,0,
288,289,5,42,0,0,289,80,1,0,0,0,290,291,5,47,0,0,291,82,1,0,0,0,
292,293,5,37,0,0,293,84,1,0,0,0,294,295,5,94,0,0,295,86,1,0,0,0,
296,297,5,62,0,0,297,298,5,61,0,0,298,88,1,0,0,0,299,300,5,62,0,
0,300,90,1,0,0,0,301,302,5,60,0,0,302,303,5,61,0,0,303,92,1,0,0,
0,304,305,5,60,0,0,305,94,1,0,0,0,306,307,5,61,0,0,307,308,5,61,
0,0,308,96,1,0,0,0,309,310,5,33,0,0,310,311,5,61,0,0,311,98,1,0,
0,0,312,313,5,112,0,0,313,318,5,105,0,0,314,315,5,80,0,0,315,318,
5,73,0,0,316,318,7,0,0,0,317,312,1,0,0,0,317,314,1,0,0,0,317,316,
1,0,0,0,318,100,1,0,0,0,319,321,7,1,0,0,320,319,1,0,0,0,321,322,
1,0,0,0,322,320,1,0,0,0,322,323,1,0,0,0,323,330,1,0,0,0,324,326,
5,46,0,0,325,327,7,1,0,0,326,325,1,0,0,0,327,328,1,0,0,0,328,326,
1,0,0,0,328,329,1,0,0,0,329,331,1,0,0,0,330,324,1,0,0,0,330,331,
1,0,0,0,331,102,1,0,0,0,332,336,7,2,0,0,333,335,7,3,0,0,334,333,
1,0,0,0,335,338,1,0,0,0,336,334,1,0,0,0,336,337,1,0,0,0,337,104,
1,0,0,0,338,336,1,0,0,0,339,341,7,4,0,0,340,339,1,0,0,0,341,342,
1,0,0,0,342,340,1,0,0,0,342,343,1,0,0,0,343,344,1,0,0,0,344,345,
6,52,0,0,345,106,1,0,0,0,7,0,317,322,328,330,336,342,1,6,0,0
]
class MathExprLexer(Lexer):
atn = ATNDeserializer().deserialize(serializedATN())
decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ]
T__0 = 1
T__1 = 2
T__2 = 3
SIN = 4
COS = 5
TAN = 6
ASIN = 7
ACOS = 8
ATAN = 9
ATAN2 = 10
SINH = 11
COSH = 12
TANH = 13
ASINH = 14
ACOSH = 15
ATANH = 16
ABS = 17
SQRT = 18
LN = 19
LOG = 20
EXP = 21
SMIN = 22
SMAX = 23
TMIN = 24
TMAX = 25
TNORM = 26
SNORM = 27
FLOOR = 28
CEIL = 29
ROUND = 30
GAMMA = 31
POWE = 32
SIGM = 33
CLAMP = 34
SFFT = 35
SIFFT = 36
ANGL = 37
PLUS = 38
MINUS = 39
MULT = 40
DIV = 41
MOD = 42
POW = 43
GE = 44
GT = 45
LE = 46
LT = 47
EQ = 48
NE = 49
CONSTANT = 50
NUMBER = 51
VARIABLE = 52
WS = 53
channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ]
modeNames = [ "DEFAULT_MODE" ]
literalNames = [ "<INVALID>",
"'('", "')'", "','", "'sin'", "'cos'", "'tan'", "'asin'", "'acos'",
"'atan'", "'atan2'", "'sinh'", "'cosh'", "'tanh'", "'asinh'",
"'acosh'", "'atanh'", "'abs'", "'sqrt'", "'ln'", "'log'", "'exp'",
"'smin'", "'smax'", "'tmin'", "'tmax'", "'tnorm'", "'snorm'",
"'floor'", "'ceil'", "'round'", "'gamma'", "'pow'", "'sigm'",
"'clamp'", "'fft'", "'ifft'", "'angle'", "'+'", "'-'", "'*'",
"'/'", "'%'", "'^'", "'>='", "'>'", "'<='", "'<'", "'=='", "'!='" ]
symbolicNames = [ "<INVALID>",
"SIN", "COS", "TAN", "ASIN", "ACOS", "ATAN", "ATAN2", "SINH",
"COSH", "TANH", "ASINH", "ACOSH", "ATANH", "ABS", "SQRT", "LN",
"LOG", "EXP", "SMIN", "SMAX", "TMIN", "TMAX", "TNORM", "SNORM",
"FLOOR", "CEIL", "ROUND", "GAMMA", "POWE", "SIGM", "CLAMP",
"SFFT", "SIFFT", "ANGL", "PLUS", "MINUS", "MULT", "DIV", "MOD",
"POW", "GE", "GT", "LE", "LT", "EQ", "NE", "CONSTANT", "NUMBER",
"VARIABLE", "WS" ]
ruleNames = [ "T__0", "T__1", "T__2", "SIN", "COS", "TAN", "ASIN", "ACOS",
"ATAN", "ATAN2", "SINH", "COSH", "TANH", "ASINH", "ACOSH",
"ATANH", "ABS", "SQRT", "LN", "LOG", "EXP", "SMIN", "SMAX",
"TMIN", "TMAX", "TNORM", "SNORM", "FLOOR", "CEIL", "ROUND",
"GAMMA", "POWE", "SIGM", "CLAMP", "SFFT", "SIFFT", "ANGL",
"PLUS", "MINUS", "MULT", "DIV", "MOD", "POW", "GE", "GT",
"LE", "LT", "EQ", "NE", "CONSTANT", "NUMBER", "VARIABLE",
"WS" ]
grammarFileName = "MathExpr.g4"
def __init__(self, input=None, output:TextIO = sys.stdout):
super().__init__(input, output)
self.checkVersion("4.13.2")
self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache())
self._actions = None
self._predicates = None
-102
View File
@@ -1,102 +0,0 @@
T__0=1
T__1=2
T__2=3
SIN=4
COS=5
TAN=6
ASIN=7
ACOS=8
ATAN=9
ATAN2=10
SINH=11
COSH=12
TANH=13
ASINH=14
ACOSH=15
ATANH=16
ABS=17
SQRT=18
LN=19
LOG=20
EXP=21
SMIN=22
SMAX=23
TMIN=24
TMAX=25
TNORM=26
SNORM=27
FLOOR=28
CEIL=29
ROUND=30
GAMMA=31
POWE=32
SIGM=33
CLAMP=34
SFFT=35
SIFFT=36
ANGL=37
PLUS=38
MINUS=39
MULT=40
DIV=41
MOD=42
POW=43
GE=44
GT=45
LE=46
LT=47
EQ=48
NE=49
CONSTANT=50
NUMBER=51
VARIABLE=52
WS=53
'('=1
')'=2
','=3
'sin'=4
'cos'=5
'tan'=6
'asin'=7
'acos'=8
'atan'=9
'atan2'=10
'sinh'=11
'cosh'=12
'tanh'=13
'asinh'=14
'acosh'=15
'atanh'=16
'abs'=17
'sqrt'=18
'ln'=19
'log'=20
'exp'=21
'smin'=22
'smax'=23
'tmin'=24
'tmax'=25
'tnorm'=26
'snorm'=27
'floor'=28
'ceil'=29
'round'=30
'gamma'=31
'pow'=32
'sigm'=33
'clamp'=34
'fft'=35
'ifft'=36
'angle'=37
'+'=38
'-'=39
'*'=40
'/'=41
'%'=42
'^'=43
'>='=44
'>'=45
'<='=46
'<'=47
'=='=48
'!='=49
-570
View File
@@ -1,570 +0,0 @@
# Generated from MathExpr.g4 by ANTLR 4.13.2
from antlr4 import *
if "." in __name__:
from .MathExprParser import MathExprParser
else:
from MathExprParser import MathExprParser
# This class defines a complete listener for a parse tree produced by MathExprParser.
class MathExprListener(ParseTreeListener):
# Enter a parse tree produced by MathExprParser#expr.
def enterExpr(self, ctx:MathExprParser.ExprContext):
pass
# Exit a parse tree produced by MathExprParser#expr.
def exitExpr(self, ctx:MathExprParser.ExprContext):
pass
# Enter a parse tree produced by MathExprParser#LtExp.
def enterLtExp(self, ctx:MathExprParser.LtExpContext):
pass
# Exit a parse tree produced by MathExprParser#LtExp.
def exitLtExp(self, ctx:MathExprParser.LtExpContext):
pass
# Enter a parse tree produced by MathExprParser#EqExp.
def enterEqExp(self, ctx:MathExprParser.EqExpContext):
pass
# Exit a parse tree produced by MathExprParser#EqExp.
def exitEqExp(self, ctx:MathExprParser.EqExpContext):
pass
# Enter a parse tree produced by MathExprParser#ToAdd.
def enterToAdd(self, ctx:MathExprParser.ToAddContext):
pass
# Exit a parse tree produced by MathExprParser#ToAdd.
def exitToAdd(self, ctx:MathExprParser.ToAddContext):
pass
# Enter a parse tree produced by MathExprParser#GeExp.
def enterGeExp(self, ctx:MathExprParser.GeExpContext):
pass
# Exit a parse tree produced by MathExprParser#GeExp.
def exitGeExp(self, ctx:MathExprParser.GeExpContext):
pass
# Enter a parse tree produced by MathExprParser#LeExp.
def enterLeExp(self, ctx:MathExprParser.LeExpContext):
pass
# Exit a parse tree produced by MathExprParser#LeExp.
def exitLeExp(self, ctx:MathExprParser.LeExpContext):
pass
# Enter a parse tree produced by MathExprParser#NeExp.
def enterNeExp(self, ctx:MathExprParser.NeExpContext):
pass
# Exit a parse tree produced by MathExprParser#NeExp.
def exitNeExp(self, ctx:MathExprParser.NeExpContext):
pass
# Enter a parse tree produced by MathExprParser#GtExp.
def enterGtExp(self, ctx:MathExprParser.GtExpContext):
pass
# Exit a parse tree produced by MathExprParser#GtExp.
def exitGtExp(self, ctx:MathExprParser.GtExpContext):
pass
# Enter a parse tree produced by MathExprParser#AddExp.
def enterAddExp(self, ctx:MathExprParser.AddExpContext):
pass
# Exit a parse tree produced by MathExprParser#AddExp.
def exitAddExp(self, ctx:MathExprParser.AddExpContext):
pass
# Enter a parse tree produced by MathExprParser#ToMul.
def enterToMul(self, ctx:MathExprParser.ToMulContext):
pass
# Exit a parse tree produced by MathExprParser#ToMul.
def exitToMul(self, ctx:MathExprParser.ToMulContext):
pass
# Enter a parse tree produced by MathExprParser#SubExp.
def enterSubExp(self, ctx:MathExprParser.SubExpContext):
pass
# Exit a parse tree produced by MathExprParser#SubExp.
def exitSubExp(self, ctx:MathExprParser.SubExpContext):
pass
# Enter a parse tree produced by MathExprParser#MulExp.
def enterMulExp(self, ctx:MathExprParser.MulExpContext):
pass
# Exit a parse tree produced by MathExprParser#MulExp.
def exitMulExp(self, ctx:MathExprParser.MulExpContext):
pass
# Enter a parse tree produced by MathExprParser#ModExp.
def enterModExp(self, ctx:MathExprParser.ModExpContext):
pass
# Exit a parse tree produced by MathExprParser#ModExp.
def exitModExp(self, ctx:MathExprParser.ModExpContext):
pass
# Enter a parse tree produced by MathExprParser#DivExp.
def enterDivExp(self, ctx:MathExprParser.DivExpContext):
pass
# Exit a parse tree produced by MathExprParser#DivExp.
def exitDivExp(self, ctx:MathExprParser.DivExpContext):
pass
# Enter a parse tree produced by MathExprParser#ToPow.
def enterToPow(self, ctx:MathExprParser.ToPowContext):
pass
# Exit a parse tree produced by MathExprParser#ToPow.
def exitToPow(self, ctx:MathExprParser.ToPowContext):
pass
# Enter a parse tree produced by MathExprParser#PowExp.
def enterPowExp(self, ctx:MathExprParser.PowExpContext):
pass
# Exit a parse tree produced by MathExprParser#PowExp.
def exitPowExp(self, ctx:MathExprParser.PowExpContext):
pass
# Enter a parse tree produced by MathExprParser#ToUnary.
def enterToUnary(self, ctx:MathExprParser.ToUnaryContext):
pass
# Exit a parse tree produced by MathExprParser#ToUnary.
def exitToUnary(self, ctx:MathExprParser.ToUnaryContext):
pass
# Enter a parse tree produced by MathExprParser#UnaryPlus.
def enterUnaryPlus(self, ctx:MathExprParser.UnaryPlusContext):
pass
# Exit a parse tree produced by MathExprParser#UnaryPlus.
def exitUnaryPlus(self, ctx:MathExprParser.UnaryPlusContext):
pass
# Enter a parse tree produced by MathExprParser#UnaryMinus.
def enterUnaryMinus(self, ctx:MathExprParser.UnaryMinusContext):
pass
# Exit a parse tree produced by MathExprParser#UnaryMinus.
def exitUnaryMinus(self, ctx:MathExprParser.UnaryMinusContext):
pass
# Enter a parse tree produced by MathExprParser#ToAtom.
def enterToAtom(self, ctx:MathExprParser.ToAtomContext):
pass
# Exit a parse tree produced by MathExprParser#ToAtom.
def exitToAtom(self, ctx:MathExprParser.ToAtomContext):
pass
# Enter a parse tree produced by MathExprParser#Func1Exp.
def enterFunc1Exp(self, ctx:MathExprParser.Func1ExpContext):
pass
# Exit a parse tree produced by MathExprParser#Func1Exp.
def exitFunc1Exp(self, ctx:MathExprParser.Func1ExpContext):
pass
# Enter a parse tree produced by MathExprParser#Func2Exp.
def enterFunc2Exp(self, ctx:MathExprParser.Func2ExpContext):
pass
# Exit a parse tree produced by MathExprParser#Func2Exp.
def exitFunc2Exp(self, ctx:MathExprParser.Func2ExpContext):
pass
# Enter a parse tree produced by MathExprParser#Func3Exp.
def enterFunc3Exp(self, ctx:MathExprParser.Func3ExpContext):
pass
# Exit a parse tree produced by MathExprParser#Func3Exp.
def exitFunc3Exp(self, ctx:MathExprParser.Func3ExpContext):
pass
# Enter a parse tree produced by MathExprParser#FuncNExp.
def enterFuncNExp(self, ctx:MathExprParser.FuncNExpContext):
pass
# Exit a parse tree produced by MathExprParser#FuncNExp.
def exitFuncNExp(self, ctx:MathExprParser.FuncNExpContext):
pass
# Enter a parse tree produced by MathExprParser#VariableExp.
def enterVariableExp(self, ctx:MathExprParser.VariableExpContext):
pass
# Exit a parse tree produced by MathExprParser#VariableExp.
def exitVariableExp(self, ctx:MathExprParser.VariableExpContext):
pass
# Enter a parse tree produced by MathExprParser#NumberExp.
def enterNumberExp(self, ctx:MathExprParser.NumberExpContext):
pass
# Exit a parse tree produced by MathExprParser#NumberExp.
def exitNumberExp(self, ctx:MathExprParser.NumberExpContext):
pass
# Enter a parse tree produced by MathExprParser#ConstantExp.
def enterConstantExp(self, ctx:MathExprParser.ConstantExpContext):
pass
# Exit a parse tree produced by MathExprParser#ConstantExp.
def exitConstantExp(self, ctx:MathExprParser.ConstantExpContext):
pass
# Enter a parse tree produced by MathExprParser#ParenExp.
def enterParenExp(self, ctx:MathExprParser.ParenExpContext):
pass
# Exit a parse tree produced by MathExprParser#ParenExp.
def exitParenExp(self, ctx:MathExprParser.ParenExpContext):
pass
# Enter a parse tree produced by MathExprParser#SinFunc.
def enterSinFunc(self, ctx:MathExprParser.SinFuncContext):
pass
# Exit a parse tree produced by MathExprParser#SinFunc.
def exitSinFunc(self, ctx:MathExprParser.SinFuncContext):
pass
# Enter a parse tree produced by MathExprParser#CosFunc.
def enterCosFunc(self, ctx:MathExprParser.CosFuncContext):
pass
# Exit a parse tree produced by MathExprParser#CosFunc.
def exitCosFunc(self, ctx:MathExprParser.CosFuncContext):
pass
# Enter a parse tree produced by MathExprParser#TanFunc.
def enterTanFunc(self, ctx:MathExprParser.TanFuncContext):
pass
# Exit a parse tree produced by MathExprParser#TanFunc.
def exitTanFunc(self, ctx:MathExprParser.TanFuncContext):
pass
# Enter a parse tree produced by MathExprParser#AsinFunc.
def enterAsinFunc(self, ctx:MathExprParser.AsinFuncContext):
pass
# Exit a parse tree produced by MathExprParser#AsinFunc.
def exitAsinFunc(self, ctx:MathExprParser.AsinFuncContext):
pass
# Enter a parse tree produced by MathExprParser#AcosFunc.
def enterAcosFunc(self, ctx:MathExprParser.AcosFuncContext):
pass
# Exit a parse tree produced by MathExprParser#AcosFunc.
def exitAcosFunc(self, ctx:MathExprParser.AcosFuncContext):
pass
# Enter a parse tree produced by MathExprParser#AtanFunc.
def enterAtanFunc(self, ctx:MathExprParser.AtanFuncContext):
pass
# Exit a parse tree produced by MathExprParser#AtanFunc.
def exitAtanFunc(self, ctx:MathExprParser.AtanFuncContext):
pass
# Enter a parse tree produced by MathExprParser#SinhFunc.
def enterSinhFunc(self, ctx:MathExprParser.SinhFuncContext):
pass
# Exit a parse tree produced by MathExprParser#SinhFunc.
def exitSinhFunc(self, ctx:MathExprParser.SinhFuncContext):
pass
# Enter a parse tree produced by MathExprParser#CoshFunc.
def enterCoshFunc(self, ctx:MathExprParser.CoshFuncContext):
pass
# Exit a parse tree produced by MathExprParser#CoshFunc.
def exitCoshFunc(self, ctx:MathExprParser.CoshFuncContext):
pass
# Enter a parse tree produced by MathExprParser#TanhFunc.
def enterTanhFunc(self, ctx:MathExprParser.TanhFuncContext):
pass
# Exit a parse tree produced by MathExprParser#TanhFunc.
def exitTanhFunc(self, ctx:MathExprParser.TanhFuncContext):
pass
# Enter a parse tree produced by MathExprParser#AsinhFunc.
def enterAsinhFunc(self, ctx:MathExprParser.AsinhFuncContext):
pass
# Exit a parse tree produced by MathExprParser#AsinhFunc.
def exitAsinhFunc(self, ctx:MathExprParser.AsinhFuncContext):
pass
# Enter a parse tree produced by MathExprParser#AcoshFunc.
def enterAcoshFunc(self, ctx:MathExprParser.AcoshFuncContext):
pass
# Exit a parse tree produced by MathExprParser#AcoshFunc.
def exitAcoshFunc(self, ctx:MathExprParser.AcoshFuncContext):
pass
# Enter a parse tree produced by MathExprParser#AtanhFunc.
def enterAtanhFunc(self, ctx:MathExprParser.AtanhFuncContext):
pass
# Exit a parse tree produced by MathExprParser#AtanhFunc.
def exitAtanhFunc(self, ctx:MathExprParser.AtanhFuncContext):
pass
# Enter a parse tree produced by MathExprParser#AbsFunc.
def enterAbsFunc(self, ctx:MathExprParser.AbsFuncContext):
pass
# Exit a parse tree produced by MathExprParser#AbsFunc.
def exitAbsFunc(self, ctx:MathExprParser.AbsFuncContext):
pass
# Enter a parse tree produced by MathExprParser#SqrtFunc.
def enterSqrtFunc(self, ctx:MathExprParser.SqrtFuncContext):
pass
# Exit a parse tree produced by MathExprParser#SqrtFunc.
def exitSqrtFunc(self, ctx:MathExprParser.SqrtFuncContext):
pass
# Enter a parse tree produced by MathExprParser#LnFunc.
def enterLnFunc(self, ctx:MathExprParser.LnFuncContext):
pass
# Exit a parse tree produced by MathExprParser#LnFunc.
def exitLnFunc(self, ctx:MathExprParser.LnFuncContext):
pass
# Enter a parse tree produced by MathExprParser#LogFunc.
def enterLogFunc(self, ctx:MathExprParser.LogFuncContext):
pass
# Exit a parse tree produced by MathExprParser#LogFunc.
def exitLogFunc(self, ctx:MathExprParser.LogFuncContext):
pass
# Enter a parse tree produced by MathExprParser#ExpFunc.
def enterExpFunc(self, ctx:MathExprParser.ExpFuncContext):
pass
# Exit a parse tree produced by MathExprParser#ExpFunc.
def exitExpFunc(self, ctx:MathExprParser.ExpFuncContext):
pass
# Enter a parse tree produced by MathExprParser#TNormFunc.
def enterTNormFunc(self, ctx:MathExprParser.TNormFuncContext):
pass
# Exit a parse tree produced by MathExprParser#TNormFunc.
def exitTNormFunc(self, ctx:MathExprParser.TNormFuncContext):
pass
# Enter a parse tree produced by MathExprParser#SNormFunc.
def enterSNormFunc(self, ctx:MathExprParser.SNormFuncContext):
pass
# Exit a parse tree produced by MathExprParser#SNormFunc.
def exitSNormFunc(self, ctx:MathExprParser.SNormFuncContext):
pass
# Enter a parse tree produced by MathExprParser#FloorFunc.
def enterFloorFunc(self, ctx:MathExprParser.FloorFuncContext):
pass
# Exit a parse tree produced by MathExprParser#FloorFunc.
def exitFloorFunc(self, ctx:MathExprParser.FloorFuncContext):
pass
# Enter a parse tree produced by MathExprParser#CeilFunc.
def enterCeilFunc(self, ctx:MathExprParser.CeilFuncContext):
pass
# Exit a parse tree produced by MathExprParser#CeilFunc.
def exitCeilFunc(self, ctx:MathExprParser.CeilFuncContext):
pass
# Enter a parse tree produced by MathExprParser#RoundFunc.
def enterRoundFunc(self, ctx:MathExprParser.RoundFuncContext):
pass
# Exit a parse tree produced by MathExprParser#RoundFunc.
def exitRoundFunc(self, ctx:MathExprParser.RoundFuncContext):
pass
# Enter a parse tree produced by MathExprParser#GammaFunc.
def enterGammaFunc(self, ctx:MathExprParser.GammaFuncContext):
pass
# Exit a parse tree produced by MathExprParser#GammaFunc.
def exitGammaFunc(self, ctx:MathExprParser.GammaFuncContext):
pass
# Enter a parse tree produced by MathExprParser#sigmoidFunc.
def enterSigmoidFunc(self, ctx:MathExprParser.SigmoidFuncContext):
pass
# Exit a parse tree produced by MathExprParser#sigmoidFunc.
def exitSigmoidFunc(self, ctx:MathExprParser.SigmoidFuncContext):
pass
# Enter a parse tree produced by MathExprParser#sfftFunc.
def enterSfftFunc(self, ctx:MathExprParser.SfftFuncContext):
pass
# Exit a parse tree produced by MathExprParser#sfftFunc.
def exitSfftFunc(self, ctx:MathExprParser.SfftFuncContext):
pass
# Enter a parse tree produced by MathExprParser#sifftFunc.
def enterSifftFunc(self, ctx:MathExprParser.SifftFuncContext):
pass
# Exit a parse tree produced by MathExprParser#sifftFunc.
def exitSifftFunc(self, ctx:MathExprParser.SifftFuncContext):
pass
# Enter a parse tree produced by MathExprParser#anglFunc.
def enterAnglFunc(self, ctx:MathExprParser.AnglFuncContext):
pass
# Exit a parse tree produced by MathExprParser#anglFunc.
def exitAnglFunc(self, ctx:MathExprParser.AnglFuncContext):
pass
# Enter a parse tree produced by MathExprParser#PowFunc.
def enterPowFunc(self, ctx:MathExprParser.PowFuncContext):
pass
# Exit a parse tree produced by MathExprParser#PowFunc.
def exitPowFunc(self, ctx:MathExprParser.PowFuncContext):
pass
# Enter a parse tree produced by MathExprParser#Atan2Func.
def enterAtan2Func(self, ctx:MathExprParser.Atan2FuncContext):
pass
# Exit a parse tree produced by MathExprParser#Atan2Func.
def exitAtan2Func(self, ctx:MathExprParser.Atan2FuncContext):
pass
# Enter a parse tree produced by MathExprParser#TMinFunc.
def enterTMinFunc(self, ctx:MathExprParser.TMinFuncContext):
pass
# Exit a parse tree produced by MathExprParser#TMinFunc.
def exitTMinFunc(self, ctx:MathExprParser.TMinFuncContext):
pass
# Enter a parse tree produced by MathExprParser#TMaxFunc.
def enterTMaxFunc(self, ctx:MathExprParser.TMaxFuncContext):
pass
# Exit a parse tree produced by MathExprParser#TMaxFunc.
def exitTMaxFunc(self, ctx:MathExprParser.TMaxFuncContext):
pass
# Enter a parse tree produced by MathExprParser#ClampFunc.
def enterClampFunc(self, ctx:MathExprParser.ClampFuncContext):
pass
# Exit a parse tree produced by MathExprParser#ClampFunc.
def exitClampFunc(self, ctx:MathExprParser.ClampFuncContext):
pass
# Enter a parse tree produced by MathExprParser#SMinFunc.
def enterSMinFunc(self, ctx:MathExprParser.SMinFuncContext):
pass
# Exit a parse tree produced by MathExprParser#SMinFunc.
def exitSMinFunc(self, ctx:MathExprParser.SMinFuncContext):
pass
# Enter a parse tree produced by MathExprParser#SMaxFunc.
def enterSMaxFunc(self, ctx:MathExprParser.SMaxFuncContext):
pass
# Exit a parse tree produced by MathExprParser#SMaxFunc.
def exitSMaxFunc(self, ctx:MathExprParser.SMaxFuncContext):
pass
del MathExprParser
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More