m-strat 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. m_strat-0.0.2/LICENSE +21 -0
  2. m_strat-0.0.2/PKG-INFO +137 -0
  3. m_strat-0.0.2/m_strat/__init__.py +1 -0
  4. m_strat-0.0.2/m_strat/api/__init__.py +1 -0
  5. m_strat-0.0.2/m_strat/api/dag/__init__.py +4 -0
  6. m_strat-0.0.2/m_strat/api/dag/blueprint_register.py +197 -0
  7. m_strat-0.0.2/m_strat/api/dag/executor.py +34 -0
  8. m_strat-0.0.2/m_strat/api/dag/graph_manager.py +232 -0
  9. m_strat-0.0.2/m_strat/api/dag/node.py +56 -0
  10. m_strat-0.0.2/m_strat/api/ecs/__init__.py +3 -0
  11. m_strat-0.0.2/m_strat/api/ecs/component_dispatcher.py +208 -0
  12. m_strat-0.0.2/m_strat/api/ecs/ecs_register.py +375 -0
  13. m_strat-0.0.2/m_strat/api/ecs/entity.py +23 -0
  14. m_strat-0.0.2/m_strat/api/logger/__init__.py +5 -0
  15. m_strat-0.0.2/m_strat/api/logger/log_manager.py +88 -0
  16. m_strat-0.0.2/m_strat/api/project_management/__init__.py +10 -0
  17. m_strat-0.0.2/m_strat/api/project_management/parameters_manager.py +335 -0
  18. m_strat-0.0.2/m_strat/api/project_management/project_manager.py +563 -0
  19. m_strat-0.0.2/m_strat/api/project_management/wrappers.py +18 -0
  20. m_strat-0.0.2/m_strat/app.py +16 -0
  21. m_strat-0.0.2/m_strat/cli.py +88 -0
  22. m_strat-0.0.2/m_strat/gui/__init__.py +1 -0
  23. m_strat-0.0.2/m_strat/gui/application_manager.py +187 -0
  24. m_strat-0.0.2/m_strat/gui/dag/__init__.py +3 -0
  25. m_strat-0.0.2/m_strat/gui/dag/bp_window.py +264 -0
  26. m_strat-0.0.2/m_strat/gui/dag/exec_graph.py +288 -0
  27. m_strat-0.0.2/m_strat/gui/dag/node_inspector.py +328 -0
  28. m_strat-0.0.2/m_strat/gui/dag/pm_triggered_window.py +25 -0
  29. m_strat-0.0.2/m_strat/gui/gui_helpers.py +128 -0
  30. m_strat-0.0.2/m_strat/gui/project_window.py +483 -0
  31. m_strat-0.0.2/m_strat.egg-info/PKG-INFO +137 -0
  32. m_strat-0.0.2/m_strat.egg-info/SOURCES.txt +38 -0
  33. m_strat-0.0.2/m_strat.egg-info/dependency_links.txt +1 -0
  34. m_strat-0.0.2/m_strat.egg-info/entry_points.txt +2 -0
  35. m_strat-0.0.2/m_strat.egg-info/requires.txt +9 -0
  36. m_strat-0.0.2/m_strat.egg-info/top_level.txt +1 -0
  37. m_strat-0.0.2/pyproject.toml +32 -0
  38. m_strat-0.0.2/readme.md +116 -0
  39. m_strat-0.0.2/setup.cfg +4 -0
  40. m_strat-0.0.2/setup.py +8 -0
m_strat-0.0.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pharez Vitalis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
m_strat-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: m_strat
3
+ Version: 0.0.2
4
+ Summary: A Directed Acyclic Graph Automated Machine Learning Tool using Entity-Component Systems concepts.
5
+ Author-email: Pharez Vitalis <orcid.conceded130@passinbox.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: datasets
12
+ Requires-Dist: dearpygui
13
+ Requires-Dist: loguru
14
+ Requires-Dist: numpy
15
+ Requires-Dist: omegaconf
16
+ Requires-Dist: pandas
17
+ Requires-Dist: rapidfuzz
18
+ Requires-Dist: scikit-learn
19
+ Requires-Dist: torch
20
+ Dynamic: license-file
21
+
22
+ # m-strat
23
+
24
+ This project was done by Pharez Vitalis for a Master's of AI at Queen Mary's University of London.
25
+
26
+ Please [Give me some feedback!](https://forms.gle/sw6MmfX8j3cNiwi9A), the docs are available [here]( https://m-strat-e0776b.gitlab.io/m_strat.html)
27
+
28
+ # Project Description
29
+ The project represents the implementation of an AutoML GUI system with DAGs fusing with the Entity-Component System framework implemented in python. The project inspirations were drawn from the idea of "What would a game engine to game developer look like for an ML developer?". A system where a developer can extend and alter components in order to produce more complex ML algorithms but also, able to lean into existing systems to fill in gaps in their knowledge.
30
+
31
+ # Benefits and Architechture
32
+ The project is in pre-alpha concept of what is being dubbed a Machine Intelligence Learning Environment (MILE). The system is designed for rapid ML prototyping and works under the followoing core ethos':
33
+ 1. **State agnostice processing**: There is a main data pipeline called "context" which keeps the current running variables, and node_params which only show the parameterised running of the current node. By using this operational system the nodes remains independent and very reuasable across different projects.
34
+ 2. **Fully Moddable**: The project is designed under a tenant of glassboxing, so that you the user can create your own Nodes within the AutoML graph simply and easily. And because you are using open source code with open source blueprints, there is a glass sandbox of alterations that can be done to your project, you could even go into the code and write your own execution system and your own custom templates!
35
+ 3. **Rapid Prototyping**: Create Models quickly and on the Fly using Pipeline features from both PyTorch and SkLearn's libraries using yaml files only! Plug in a url and a local project location and download datasets from HF and anywhere easily. Don't like the way something works? Copy it and change it your way!
36
+
37
+ #### Note: A full manual is supplied in **user_manual.md**
38
+
39
+ ## Setup and Installation
40
+ 1. Download and extract the project if it is in a zip folder.
41
+ 2. Open a command window in the root project folder (in `m_strat` **NOT** 'm_strat/m_strat'!)
42
+ 3. `pip install .` does a standard installation. `pip install -e .` Is recommended for developers
43
+ The `-e` flag means editable, this allows the project to customised at source without needing to reinstall the package
44
+ 4. to open the gui now, type in a command window anywhere `m_strat gui` your GUI should now open!
45
+ 5. you can also do `m_strat init` or any standard command using the command line interface yourself
46
+
47
+ # Usage
48
+ Once the project is installed using `pip install .`, you can launch the app in command terminal by simply typing `m_strat` anywhere.
49
+
50
+
51
+
52
+ ## Libraries
53
+ - **Loguru**: Was used because it is a simple, well-designed logging system. You can use the observer class to help you make simple attachers in api/logger, but you don't have to and you can attach your own stuff.Additionally, when using blueprints it is recommended that you use loguru, which will log to the observers created by m-strat
54
+ - **DearPyGui**: Rescued my project when I lost faith in coding GUIs in python! The OOTB Node Space really helped me somewhat land this thing, their use of GPU based rending using C++ bridging is most well implemented.
55
+ - **OmegaConf**: A comprehensive but simple to use library that does indeed elegantly do yaml.
56
+ - **RapidFuzz**: Had a query text problem, remembered a library I used and, to be honest very straightforward.
57
+ - **PyTorch, Pandas, NumPy & Scikit-learn**: This is ML, of course they are here!
58
+
59
+ ### Package Citations
60
+ ```bibtex
61
+ @Misc{Yadan2019Hydra,
62
+ author = {Omry Yadan},
63
+ title = {Hydra - A framework for elegantly configuring complex applications},
64
+ howpublished = {Github},
65
+ year = {2019},
66
+ url = {https://github.com/facebookresearch/hydra}
67
+ }
68
+
69
+ @misc{DearPyGui,
70
+ author = {Hoffstadt, Jonathan and Cothren, Preston},
71
+ title = {Dear PyGui: A fast and powerful Graphical User Interface Toolkit for Python},
72
+ year = {2026},
73
+ publisher = {GitHub},
74
+ journal = {GitHub repository},
75
+ howpublished = {\url{https://github.com/hoffstadt/DearPyGui}},
76
+ }
77
+
78
+ @inproceedings{paszke2019pytorch,
79
+ title={PyTorch: An Imperative Style, High-Performance Deep Learning Library},
80
+ author={Paszke, Adam and Gross, Sam and Massa, Francisco and Lerer, Adam and Bradbury, James and Chanan, Gregory and Killeen, Trevor and Lin, Zeming and Gimelshein, Natalia and Antiga, Luca and Desmaison, Alban and K{\"o}pf, Andreas and Yang, Edward and DeVito, Zachary and Raison, Martin and Tejani, Alykhan and Chilamkurthy, Sasank and Steiner, Benoit and Fang, Lu and Bai, Junjie and Chintala, Soumith},
81
+ booktitle={Advances in Neural Information Processing Systems (NeurIPS)},
82
+ pages={8024--8035},
83
+ year={2019}
84
+ }
85
+
86
+ @article{scikit-learn,
87
+ title={Scikit-learn: Machine Learning in {P}ython},
88
+ author={Pedregosa, F. and Varoquaux, G. and Gramfort, A. and Michel, V. and Thirion, B. and Grisel, O. and Blondel, M. and Prettenhofer, P. and Weiss, R. and Dubourg, V. and Vanderplas, J. and Passos, A. and Cournapeau, D. and Brucher, M. and Perrot, M. and Duchesnay, E.},
89
+ journal={Journal of Machine Learning Research},
90
+ volume={12},
91
+ pages={2825--2830},
92
+ year={2011}
93
+ }
94
+
95
+ @inproceedings{lhoest-etal-2021-datasets,
96
+ title = {Datasets: A Community Library for Natural Language Processing},
97
+ author = {Lhoest, Quentin and Villanova del Moral, Albert and Jernite, Yacine and Thakur, Abhishek and von Platen, Patrick and Patil, Suraj and Chaumond, Julien and Moro, Mariama and Sutawika, Julien},
98
+ year = {2021},
99
+ publisher = {Association for Computational Linguistics},
100
+ booktitle = {Proceedings of the 2021 Conference on Empirical Methods in Natural Language Processing: System Demonstrations},
101
+ pages = {175--184},
102
+ url = {https://aclanthology.org/2021.emnlp-demo.21}
103
+ }
104
+
105
+ @software{max_bachmann_rapidfuzz,
106
+ author = {Max Bachmann},
107
+ title = {rapidfuzz/RapidFuzz},
108
+ publisher = {Zenodo},
109
+ doi = {10.5281/zenodo.15133267},
110
+ url = {https://doi.org/10.5281/zenodo.15133267}
111
+ }
112
+
113
+ @Article{harris2020array,
114
+ title = {Array programming with {NumPy}},
115
+ author = {Charles R. Harris and K. Jarrod Millman and St{\'{e}}fan J. van der Walt and Ralf Gommers and Pauli Virtanen and David Cournapeau and Eric Wieser and Julian Taylor and Sebastian Berg and Nathaniel J. Smith and Robert Kern and Matti Picus and Stephan Hoyer and Marten H. van Kerkwijk and Matthew Brett and Allan Haldane and Jaime Fern{\'{a}}ndez del R{\'{i}}o and Mark Wiebe and Pearu Peterson and Pierre G{\'{e}}rard-Marchant and Kevin Sheppard and Tyler Reddy and Warren Weckesser and Hameer Abbasi and Christoph Gohlke and Travis E. Oliphant},
116
+ year = {2020},
117
+ month = sep,
118
+ journal = {Nature},
119
+ volume = {585},
120
+ number = {7825},
121
+ pages = {357--362},
122
+ doi = {10.1038/s41586-020-2649-2},
123
+ publisher = {Springer Science and Business Media {LLC}},
124
+ url = {https://doi.org/10.1038/s41586-020-2649-2}
125
+ }
126
+
127
+ @software{reback2020pandas,
128
+ author = {The pandas development team},
129
+ title = {pandas-dev/pandas: Pandas},
130
+ month = feb,
131
+ year = 2020,
132
+ publisher = {Zenodo},
133
+ version = {latest},
134
+ doi = {10.5281/zenodo.3509134},
135
+ url = {https://doi.org/10.5281/zenodo.3509134}
136
+ }
137
+ ```
@@ -0,0 +1 @@
1
+ # Init
@@ -0,0 +1 @@
1
+ # init
@@ -0,0 +1,4 @@
1
+ from .blueprint_register import *
2
+ from .executor import *
3
+ from .graph_manager import *
4
+ from .node import *
@@ -0,0 +1,197 @@
1
+ import importlib.util
2
+ import inspect
3
+ import json
4
+ import os
5
+
6
+ from m_strat.api.dag.node import BlueprintNode
7
+ from m_strat.api.logger import m_log
8
+
9
+
10
+ class BlueprintRegister:
11
+ """Manages Blueprint Nodes in a project"""
12
+
13
+ def __init__(self, meta_file_path: str = "configs/bp_files_meta.jsonl"):
14
+ self.blueprints = {}
15
+ self.schemas = {}
16
+ self.files_found = []
17
+
18
+ if meta_file_path:
19
+ self.load_from_meta_file(meta_file_path)
20
+ self.meta_file_path = meta_file_path
21
+
22
+ def register_node(
23
+ self, name: str, func: callable, schema: dict, overwrite: bool = True
24
+ ):
25
+ """Adds a method to the project registry
26
+ Args:
27
+ name (str): The new name of the blueprint node being created (can be given via BlueprintNode decorator)
28
+ func (callable): The function that will be used to execute the node logic
29
+ schema (dict[str, type]): Used to guide the UI and create an automated form in the application, not enforced
30
+ overwrite (bool, optional): Allows a node to be overwritten if the name already exists. Defaults to True.
31
+ """
32
+ assert overwrite or self.blueprints.get(name, None) is None, (
33
+ f"blueprint of '{name}' already exists, overwrite not allowed"
34
+ )
35
+ self.blueprints[name] = func
36
+ self.schemas[name] = schema
37
+
38
+ def unregister_node(self, name: str):
39
+ """Remove a node from the registry by name
40
+ Returns:
41
+ tuple[dict,dict] : blueprint callable and schema respectively
42
+ """
43
+ return (self.blueprints.pop(name, None), self.schemas.pop(name, None))
44
+
45
+ def get_blueprint(self, name: str):
46
+ """Get the callable of a blueprint by name"""
47
+ return self.blueprints.get(name, None)
48
+
49
+ def refresh_blueprints(self):
50
+ self.blueprints = {}
51
+ self.schemas = {}
52
+ old_files_found = list(self.files_found)
53
+ self.files_found = []
54
+ for f in old_files_found:
55
+ self.add_from_file(f)
56
+
57
+ def save_meta_file(self, meta_file_path="configs/bp_files_meta.jsonl"):
58
+ """Generates a jsonl file of where all the blueprints were retrieved from, so they can be loaded easily
59
+ Args:
60
+ meta_file_path (str, optional): relative path of wheree this generated file will save. Defaults to "configs/bp_files_meta.jsonl".
61
+ """
62
+ if not self.files_found:
63
+ m_log.info("Attempted to save blueprints, but no blueprints registered")
64
+ return
65
+ with open(meta_file_path, "w", encoding="utf-8") as meta_f:
66
+ meta_f.writelines(
67
+ json.dumps(bp_file_path) + "\n"
68
+ for bp_file_path in set(self.files_found)
69
+ )
70
+ m_log.info(f"Blueprint data saved {meta_file_path}")
71
+
72
+ def load_from_meta_file(self, meta_file_path: str | None = None):
73
+ """Loads and use each line as a path to retrieve saved blueprint reference from py files .
74
+ Args:
75
+ meta_file_path (str, optional): The path where the meta file resides. Defaults to "configs/bp_files_meta.jsonl".
76
+ Returns:
77
+ list[str] : Returns a list of all the names of the blueprints that were added
78
+ """
79
+ m_log.info("Loading Blueprint Configuration Data...")
80
+ if meta_file_path is None:
81
+ meta_file_path = self.meta_file_path
82
+ if not os.path.exists(meta_file_path):
83
+ m_log.warning(f"bp_reg: path does not exist:: {meta_file_path}")
84
+ return []
85
+ added = []
86
+ with open(meta_file_path, "r", encoding="utf8") as meta_f:
87
+ for i, line in enumerate(meta_f):
88
+ if not line.strip():
89
+ continue
90
+ try:
91
+ next_path = json.loads(line.strip())
92
+ added.extend(self.add_from_file(next_path))
93
+ except (OSError, ValueError, json.JSONDecodeError) as e:
94
+ m_log.warning(
95
+ f"Line at {i} in {meta_file_path} failed to read: {e}"
96
+ )
97
+ return added
98
+
99
+ def get_schema(self, name: str):
100
+ """Gets the (dict[str,type]) schema given a name of a Blueprint Node"""
101
+ return self.schemas.get(name, None)
102
+
103
+ def add_from_file(self, file_path: str):
104
+ """uses importlib to check for any Blueprint Nodes and attempts to add them to the system
105
+ Args:
106
+ file_path (str): relative path of the .py blueprint file resides
107
+ Returns:
108
+ list[str] : Returns a list of all the names of the blueprints that were added
109
+ """
110
+ if not os.path.exists(file_path):
111
+ m_log.warning(f"bp_reg: path does not exist:: {file_path}")
112
+ return []
113
+
114
+ module_name = os.path.splitext(os.path.basename(file_path))[0]
115
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
116
+ added = []
117
+ assert (
118
+ spec is not None and spec.loader
119
+ ), f"""Provided file does not have a spec or spec loader: ||{file_path} ||
120
+ This can happen because:
121
+ 1. If the file exstension is wrong (not .py)
122
+ 2. File permission issues such as file locked (in use elsewhere e.g )
123
+ 3. Unfriendly path characters: path contains unusual unicode characters or file name is not python friendly
124
+ 4. Some issue which I simply have not thought of in this moment.
125
+ """
126
+ module = importlib.util.module_from_spec(spec)
127
+ module.BlueprintNode = BlueprintNode
128
+ try:
129
+ spec.loader.exec_module(module)
130
+ except Exception as e:
131
+ # flagrantly assume the error is because the module should not be run like this!
132
+ m_log.debug(
133
+ f"Spec Loader did not like something when bp register used exec_module (can ignore typically): {e}"
134
+ )
135
+
136
+ for _, obj in inspect.getmembers(module, inspect.isfunction):
137
+ if getattr(obj, "__is_m_strat_node__", False):
138
+ self.register_node(
139
+ obj.__m_strat_node_name__, obj, obj.__m_strat_node_schema__
140
+ )
141
+ added.append(obj.__m_strat_node_name__)
142
+
143
+ if len(added) > 0:
144
+ self.files_found.append(file_path)
145
+ return added
146
+
147
+ def add_from_folder(self, folder_path, add_subfolders=True):
148
+ """
149
+ From a directory, adds all Blueprint Nodes to the register
150
+ Args:
151
+ folder_path (str): The relative directory path
152
+ add_subfolders (bool, optional): Whether to add Blueprint Nodes from subdirectories (of any depth) . Defaults to True.
153
+ Returns:
154
+ list[str] : Returns a list of all the names of the blueprints that were added
155
+ """
156
+ if not os.path.isdir(folder_path):
157
+ m_log.warning(f"bp_reg: path does not exist:: {folder_path}")
158
+ return []
159
+ added_nodes = []
160
+ m_log.info(f"Adding folder blueprint folder: {folder_path}")
161
+ if add_subfolders:
162
+ for root, _, files in os.walk(folder_path):
163
+ for f_name in files:
164
+ if f_name.endswith(".py") and not f_name.startswith("__"):
165
+ full_path = os.path.join(root, f_name)
166
+ next_added = []
167
+ try:
168
+ next_added = self.add_from_file(full_path)
169
+ except Exception as e:
170
+ m_log.error(
171
+ f"Blueprint tried to add Blueprints from |{f_name}| in directory |{root}| but failed: \n {e} \n{'*' * 12}\n ...Skipping!"
172
+ )
173
+ continue
174
+
175
+ added_nodes.extend(next_added)
176
+ else:
177
+ for f_name in os.listdir(folder_path):
178
+ if f_name.endswith(".py") and not f_name.startswith("__"):
179
+ full_path = os.path.join(folder_path, f_name)
180
+ if os.path.isfile(full_path):
181
+ added_nodes.extend(self.add_from_file(full_path))
182
+ m_log.info(
183
+ f"Finished adding blueprint director, {len(added_nodes)} blueprint(s) found"
184
+ )
185
+ return added_nodes
186
+
187
+ def get_all_blueprints(self):
188
+ """Copies the blueprint callable dictionary, returning all blueprints
189
+ NB: Blueprints are given by reference, modifications will alter them!
190
+ """
191
+ return dict(self.blueprints)
192
+
193
+ def get_all_schemas(self):
194
+ """Copies the schema dictionary, returning all blueprint schemas
195
+ NB: Blueprints are given by reference, modifications will alter them!
196
+ """
197
+ return dict(self.schemas)
@@ -0,0 +1,34 @@
1
+ from m_strat.api.logger import m_log
2
+
3
+ _EXECUTORS = {}
4
+
5
+
6
+ # Defines an exectuion method for Node system. Must have (targetnodes, init_context) as arguments. Must return context
7
+ # name muse be unique
8
+ def Executor(name: str):
9
+ def decorator(func):
10
+ assert _EXECUTORS.get(name, None) is None, f"Executor {name} already exists"
11
+ _EXECUTORS[name] = func
12
+ return func
13
+
14
+ return decorator
15
+
16
+
17
+ # Standard linear-non parallelised running, executes in same thread without modification
18
+ @Executor(name="Sequential")
19
+ def execute_sequential(target_nodes: list[tuple], init_context: dict):
20
+ context = init_context
21
+ m_log.info("Started Sequential running")
22
+ for func, params, node_info in target_nodes:
23
+ m_log.debug(f"Running Node {node_info.id}")
24
+ try:
25
+ context = func(context, params)
26
+ except Exception as e:
27
+ m_log.error(
28
+ f"Pipeline execution failed at '{node_info.id}' using blueprint '{node_info.blueprint_name}':\n {e}"
29
+ )
30
+ raise # NB you should always raiserrors when making your own Executor!!
31
+ m_log.debug(f"Node{node_info.id} Completed")
32
+
33
+ m_log.info(f"Finished Executing {len(target_nodes)} node(s)")
34
+ return context
@@ -0,0 +1,232 @@
1
+ import json
2
+ import os
3
+ from collections import defaultdict, deque
4
+ from dataclasses import asdict
5
+
6
+ from m_strat.api.project_management.wrappers import Serializable
7
+
8
+ from .node import ExecutionNode
9
+
10
+
11
+ class GraphManager(Serializable):
12
+ """Manages a single DAG which is used to build a ML pipeline from Blueprint Nodes"""
13
+
14
+ def __init__(
15
+ self,
16
+ name: str,
17
+ load_path: str | None = None,
18
+ load: bool = False,
19
+ dir: str = "exec_graphs",
20
+ ):
21
+ """Instansiates a Graphmanager object used to manage a DAG
22
+ Args:
23
+ name (str): The designated label of this DAG
24
+ load_path (str, optional): Path to json config . Defaults to None.
25
+ load (bool, optional): If true attemps to load the graph by name or load path. Defaults to False.
26
+ dir (str, optional): Path to where this graph is stored. Defaults to "exec_graphs".
27
+
28
+ Raises:
29
+ ValueError: if no name or load_path is supplied and load is true as no data can be loaded.
30
+ """
31
+ self.nodes = {"start": ExecutionNode("start", "", "")}
32
+ self.edges = defaultdict(list)
33
+ self.name = name
34
+ self.dir = dir
35
+ if load and (name or load_path):
36
+ self.load(name=name, load_path=load_path)
37
+ elif load:
38
+ raise ValueError("GraphManager cannot load without a valid graph name")
39
+
40
+ @staticmethod
41
+ def get_file_path(name: str, dir: str):
42
+ """From the name and directory, returns what the graph data file should be"""
43
+ return os.path.join(dir, f"gm_{name}.json")
44
+
45
+ @staticmethod
46
+ def get_name_from_filename(path: str):
47
+ """if given a standardised graph data file, extracts the name of the graph"""
48
+ base_name: str = os.path.splitext(os.path.basename(path))[0]
49
+ return base_name[3:]
50
+
51
+ def create_add_new_node(self, node_id: str, blueprint_name: str, param_id: str):
52
+ """From data supplied, creates an ExecutionNode and adds it to the graph
53
+ Args:
54
+ node_id (str): human readable name of node
55
+ blueprint_name (str): name of the blueprint in blueprint Register
56
+ param_id (str): name of node params from paramater manager
57
+ """
58
+ new_node = ExecutionNode(
59
+ id=node_id, blueprint_name=blueprint_name, param_id=param_id
60
+ )
61
+ self.add_node(new_node)
62
+
63
+ def add_node(self, new_node: ExecutionNode):
64
+ """Adds a node to the graph"""
65
+ assert self.nodes.get(new_node.id, None) is None, "Node Already exists"
66
+ self.nodes[new_node.id] = new_node
67
+
68
+ def change_node_param_template(self, node_id: str, new_param_id: str):
69
+ """Changes a node parameter template (i.e. the "params" parsed into the ExecutionNode at runtime)
70
+ Args:
71
+ node_id (str): instance name of the node within the DAG
72
+ new_param_id (str): the new parameter id for the node to use
73
+ """
74
+ assert isinstance(new_param_id, str) and new_param_id, (
75
+ "New Parameter id Invalid"
76
+ )
77
+ node = self.nodes.get(node_id, None)
78
+ assert node is not None, "Node not found"
79
+ node.param_id = new_param_id
80
+
81
+ def remove_node(self, node_id: str):
82
+ """
83
+ Removes a node from the DAG (and its edges)
84
+ Args:
85
+ node_id(str): the target node to be removed (DAG name not blueprint name!)
86
+ """
87
+ assert node_id != "start", (
88
+ "Cannot Remove or Alter Start Node, alter it's edges, or parse different start point to build_execution instead"
89
+ )
90
+ self.edges.pop(node_id, [])
91
+ self.nodes.pop(node_id)
92
+ for e_v_lst in self.edges.values():
93
+ if node_id in e_v_lst:
94
+ e_v_lst.remove(node_id)
95
+
96
+ def add_edge(self, node_from: str, node_to: str):
97
+ """Connects 2 nodes together, representing execution order
98
+ Args:
99
+ node_from (str): Node from which the connection is to be made
100
+ node_to (str): Node the connection ends at.
101
+ """
102
+ assert node_to != "start", "All edges on start node must be outgoing"
103
+ assert node_from != node_to, "A node cannot have an edge connecting to itself"
104
+ assert self.nodes.get(node_from, None) is not None, (
105
+ f"Add Edge From Node {node_from} not present in manager"
106
+ )
107
+ assert self.nodes.get(node_to, None) is not None, (
108
+ f"Add Edge To Node {node_to} not present in manager"
109
+ )
110
+ neighbours = self.edges[node_from]
111
+ if node_to not in neighbours:
112
+ neighbours.append(node_to)
113
+
114
+ def remove_edge(self, node_from: str, node_to: str, allow_inverse: bool = False):
115
+ """Removes 2 connecting edges of a node, allow inverse checks with the nodes in the opposite direction"""
116
+ self.edges[node_from].remove(node_to)
117
+ if allow_inverse:
118
+ inverse_edge = self.edges.get(node_to, None)
119
+ if not inverse_edge:
120
+ return
121
+ if node_from in inverse_edge:
122
+ inverse_edge.remove(node_from)
123
+
124
+ def get_edges(self):
125
+ """Gets all edges as a set of tuples (of -> from, to) and returns them
126
+ Returns:
127
+ set: tuple of (node_from, node_to)
128
+ """
129
+ edge_tuples = set()
130
+ for node_from, neighbours in self.edges.items():
131
+ for node_to in neighbours:
132
+ edge_tuples.add((node_from, node_to))
133
+ return edge_tuples
134
+
135
+ def get_nodes(self):
136
+ """Returns a shallow copy of all nodes"""
137
+ return dict(self.nodes)
138
+
139
+ def get_node(self, node_id: str):
140
+ """Gets a node via node_id from this dag, returns None if not present"""
141
+ return self.nodes.get(node_id, None)
142
+
143
+ def build_execution(self, start_node_id="start"):
144
+ """Generates a linear execution of nodes from star_node_id until there are ends
145
+ Args:
146
+ start_node_id (str, optional): String of first node_id. Defaults to "start".
147
+ Returns:
148
+ list[ExecutionNode]: returns the order of nodes by execution from start->finish
149
+ NB: Currently does not detect loops, parallelisation is flattened currently.
150
+ """
151
+ in_degree = {node_id: 0 for node_id in self.nodes}
152
+
153
+ for neighbors in self.edges.values():
154
+ for n in neighbors:
155
+ in_degree[n] += 1
156
+
157
+ queue = deque([start_node_id])
158
+ exec_order = []
159
+
160
+ while queue:
161
+ current = queue.popleft()
162
+
163
+ exec_order.append(self.nodes.get(current))
164
+
165
+ for neighbor in self.edges.get(current, []):
166
+ in_degree[neighbor] -= 1
167
+ if in_degree[neighbor] == 0:
168
+ queue.append(neighbor)
169
+
170
+ return exec_order
171
+
172
+ def load(self, name: str | None = None, load_path: str | None = None):
173
+ """using the load path primarily or the name if not supplied. load selection order: load_path->name->self.name
174
+ Args:
175
+ name (str, optional): Name of graph manager. Defaults to None.
176
+ load_path (str, optional): Path to graph configuration file. Priortised. Defaults to None.
177
+ """
178
+ assert load_path is not None or name is not None or self.name is not None, (
179
+ "At least one identifier required to find the graph manager"
180
+ )
181
+ if load_path is None:
182
+ load_path = self.get_file_path(name, self.dir)
183
+
184
+ with open(load_path, "r", encoding="utf-8") as load_file:
185
+ load_data = json.load(load_file)
186
+ self.edges = defaultdict(list, load_data["edges"])
187
+ self.name = load_data["name"] if name is None else name
188
+ node_data = {}
189
+ for node_id, exec_dict in load_data["nodes"].items():
190
+ node_data[node_id] = ExecutionNode(
191
+ id=node_id,
192
+ blueprint_name=exec_dict["blueprint_name"],
193
+ param_id=exec_dict["param_id"],
194
+ pos=exec_dict.get("pos", None),
195
+ )
196
+ self.nodes = node_data
197
+
198
+ def save(self):
199
+ """using the name of the current graph, generates a name and saves the configuration as json"""
200
+ save_path = self.get_file_path(self.name, self.dir)
201
+ save_data = {"edges": self.edges, "name": self.name}
202
+ save_data["nodes"] = {
203
+ node_id: asdict(exec_node) for node_id, exec_node in self.nodes.items()
204
+ }
205
+ with open(save_path, "w", encoding="utf-8") as save_file:
206
+ json.dump(save_data, save_file)
207
+
208
+ def duplicate(self, new_name=""):
209
+ """Duplicates the current graph manager into another"""
210
+ if not new_name:
211
+ new_name = f"{self.name}_copy"
212
+ dupe = GraphManager(name=new_name, load=False, dir=self.dir)
213
+ for n in self.nodes.values():
214
+ if n.id == "start":
215
+ if n.pos:
216
+ dupe.nodes["start"].pos = list(n.pos)
217
+ continue
218
+ n_copy = ExecutionNode(
219
+ id=n.id,
220
+ blueprint_name=n.blueprint_name,
221
+ param_id=n.param_id,
222
+ pos=list(n.pos) if n.pos else None,
223
+ )
224
+ dupe.add_node(n_copy)
225
+ for node_name, neighbours in self.edges.items():
226
+ for neigh in neighbours:
227
+ dupe.add_edge(node_name, neigh)
228
+ return dupe
229
+
230
+ @classmethod
231
+ def file_type(cls):
232
+ return "json"