├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .readthedocs.yaml ├── README.md ├── docs ├── Makefile ├── figures.ipynb ├── figures │ ├── closedloop_example.gif │ ├── handoff_graphs.ai │ ├── node_deletion.ai │ └── role_in_system.ai ├── make.bat └── source │ ├── conf.py │ ├── entering_data.rst │ ├── img │ ├── action_allowed_edges.png │ ├── analysis_allowed_edges.png │ ├── example_handoff.png │ ├── example_sample_graph.png │ ├── handoff_graphs.png │ ├── logo │ │ ├── labgraph_dark mode.png │ │ └── labgraph_light mode.png │ ├── material_allowed_edges.png │ └── measurement_allowed_edges.png │ ├── index.rst │ ├── labgraph.data.actors.rst │ ├── labgraph.data.nodes.rst │ ├── labgraph.data.rst │ ├── labgraph.data.sample.rst │ ├── labgraph.rst │ ├── labgraph.utils.config.config.rst │ ├── labgraph.utils.config.rst │ ├── labgraph.utils.data_objects.rst │ ├── labgraph.utils.db_lock.rst │ ├── labgraph.utils.dev.rst │ ├── labgraph.utils.graph.rst │ ├── labgraph.utils.rst │ ├── labgraph.views.actors.rst │ ├── labgraph.views.base.rst │ ├── labgraph.views.graph_integrity.rst │ ├── labgraph.views.nodes.rst │ ├── labgraph.views.rst │ ├── labgraph.views.sample.rst │ ├── modules.rst │ ├── node_types.rst │ ├── retrieving_data.rst │ ├── schema.rst │ ├── setup.rst │ └── tutorial.rst ├── examples ├── basic_usage.ipynb └── graph_navigation.ipynb ├── labgraph ├── __init__.py ├── dashboard │ ├── .gitignore │ ├── __init__.py │ ├── lab_views.py │ ├── routes │ │ ├── __init__.py │ │ ├── basic_route.py │ │ ├── graph.py │ │ ├── nodes.py │ │ ├── sample.py │ │ └── utils.py │ └── ui │ │ ├── asset-manifest.json │ │ ├── favicon.svg │ │ ├── index.html │ │ ├── robots.txt │ │ ├── sample.png │ │ └── static │ │ ├── css │ │ ├── main.6894ff8f.css │ │ └── main.6894ff8f.css.map │ │ └── js │ │ ├── 787.28cb0dcd.chunk.js │ │ ├── 787.28cb0dcd.chunk.js.map │ │ ├── main.37fab58c.js │ │ ├── main.37fab58c.js.LICENSE.txt │ │ └── main.37fab58c.js.map ├── data │ ├── __init__.py │ ├── actors.py │ ├── nodes.py │ └── sample.py ├── scripts │ ├── __init__.py │ ├── cli.py │ └── launch_client.py ├── utils │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ └── config.py │ ├── data_objects.py │ ├── db_lock.py │ ├── dev.py │ ├── graph.py │ └── plot.py └── views │ ├── __init__.py │ ├── actors.py │ ├── base.py │ ├── graph_integrity.py │ ├── nodes.py │ └── sample.py ├── my-app ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── Images │ │ ├── lime-bicycle-riding.png │ │ ├── lime-canoeing.png │ │ ├── lime-ecology.png │ │ ├── lime-message-sent.png │ │ ├── lime-surfing.png │ │ ├── lime-travel.png │ │ └── lime-welcome.png │ ├── Styles │ │ ├── About.scss │ │ ├── Content.scss │ │ ├── Footer.scss │ │ ├── Header.scss │ │ ├── SectionFive.scss │ │ ├── SectionFour.scss │ │ ├── SectionOne.scss │ │ ├── SectionThree.scss │ │ └── SectionTwo.scss │ ├── __mock__ │ │ ├── actions.js │ │ ├── actors.js │ │ ├── analyses.js │ │ ├── analysis_methods.js │ │ ├── materials.js │ │ ├── measurements.js │ │ └── samples.tsx │ ├── api │ │ └── api.tsx │ ├── components │ │ ├── Footer.tsx │ │ ├── GraphView.tsx │ │ ├── Header.tsx │ │ ├── Tables.tsx │ │ └── Tables │ │ │ ├── Actions.tsx │ │ │ ├── Actors.tsx │ │ │ ├── Analyses.tsx │ │ │ ├── AnalysisMethods.tsx │ │ │ ├── Materials copy.tsx.data │ │ │ ├── Materials.tsx │ │ │ ├── Measurements.tsx │ │ │ ├── Samples.tsx │ │ │ └── utils.tsx │ ├── index.scss │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── views │ │ └── Content.tsx ├── tsconfig.json └── yarn.lock ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── fixtures ├── __init__.py ├── example_data.py └── example_system.py ├── test_actors.py ├── test_nodes.py └── test_samples.py /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | py3test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [ "3.8", "3.9", "3.10" ] 11 | services: 12 | mongodb: 13 | image: "mongo:5.0" 14 | ports: 15 | - 27017:27017 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Set up node 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: "14" 23 | - name: Set up python 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | cache: 'pip' 28 | cache-dependency-path: | 29 | **/requirements-dev.txt 30 | **/requirements.txt 31 | - name: Set up environment 32 | run: | 33 | pip install --quiet -r ./requirements.txt -r ./requirements-dev.txt 34 | pip install --quiet . 35 | - name: Generate coverage report 36 | run: | 37 | pip install pytest pytest-cov 38 | pytest --cov=./ --cov-report=xml 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v3 41 | with: 42 | # # token: ${{ secrets.CODECOV_TOKEN }} 43 | # directory: ./coverage/reports/ 44 | env_vars: OS,PYTHON 45 | fail_ci_if_error: true 46 | # files: ./coverage1.xml,./coverage2.xml 47 | # flags: unittests 48 | # name: codecov-umbrella 49 | # path_to_write_report: ./coverage/codecov_report.txt 50 | # verbose: true 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.toml 2 | 3 | .vscode 4 | .DS_Store 5 | */.DS_Store 6 | 7 | playground.ipynb 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | *.pyc 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | alab_data.egg-info 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | cover/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | .pybuilder/ 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | # For a library or package, you might want to ignore these files since the code is 95 | # intended to run in multiple environments; otherwise, check them in: 96 | # .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # poetry 106 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 107 | # This is especially recommended for binary packages to ensure reproducibility, and is more 108 | # commonly ignored for libraries. 109 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 110 | #poetry.lock 111 | 112 | # pdm 113 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 114 | #pdm.lock 115 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 116 | # in version control. 117 | # https://pdm.fming.dev/#use-with-ide 118 | .pdm.toml 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.8" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: requirements.txt 30 | - requirements: requirements-dev.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation Status](https://readthedocs.org/projects/labgraph/badge/?version=latest)](https://labgraph.readthedocs.io/en/latest/?badge=latest) 2 | [![codecov](https://codecov.io/gh/rekumar/labgraph/branch/master/graph/badge.svg?token=TUCYBZI2P4)](https://codecov.io/gh/rekumar/labgraph) 3 | 4 | `pip install labgraph-db` 5 | 6 | 7 | 8 | 9 | LabGraph: a graph-based schema for storing experimental data for chemistry and materials science. 10 | 11 | 12 | > **Warning** 13 | > This project is still under development! 14 | 15 | 16 | 17 | This library defines a graph-based schema for storing materials science data. 18 | 19 | You can read the (evolving) documentation [here](https://labgraph.readthedocs.io/en/latest/). 20 | 21 | I gave a talk on Labgraph at the 2023 Spring meeting for the Materials Research Society. You can view the slides [here](https://www.slideshare.net/secret/pevc4VHK5ThSr6), though the animations don't work. They key point is that we use labgraph as a central database to coordinate our automated lab like so: 22 | 23 |

24 | 25 |

26 | 27 | 28 | 29 | ## Additional Dependencies 30 | 31 | - A `Sample` graph can be plotted within Python using `Sample.plot()`. The default `networkx` plotting layouts can be pretty confusing to interpret. If you install [graphviz](https://www.graphviz.org), `labgraph` will instead use graphviz to design the graph layout in a hierarchical fashion. This only affects plotting, but if you are relying on this functionality it can make a big difference! 32 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/figures/closedloop_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/figures/closedloop_example.gif -------------------------------------------------------------------------------- /docs/figures/handoff_graphs.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/figures/handoff_graphs.ai -------------------------------------------------------------------------------- /docs/figures/node_deletion.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/figures/node_deletion.ai -------------------------------------------------------------------------------- /docs/figures/role_in_system.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/figures/role_in_system.ai -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | from pathlib import Path 7 | 8 | THIS_DIR = Path(__file__).parent 9 | 10 | 11 | def get_version(filepath: Path) -> str: 12 | with open(filepath, encoding="utf-8") as fd: 13 | for line in fd.readlines(): 14 | if line.startswith("__version__"): 15 | delim = '"' if '"' in line else "'" 16 | return line.split(delim)[1] 17 | raise RuntimeError("Unable to find version string.") 18 | 19 | 20 | version = get_version(THIS_DIR.parent.parent / "labgraph" / "__init__.py") 21 | 22 | # -- Project information ----------------------------------------------------- 23 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 24 | 25 | project = "LabGraph" 26 | copyright = "2022, Rishi E Kumar" 27 | author = "Rishi E Kumar" 28 | release = version 29 | 30 | # -- General configuration --------------------------------------------------- 31 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 32 | 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.autosectionlabel", 36 | "sphinx.ext.doctest", 37 | "sphinx.ext.intersphinx", 38 | "sphinx.ext.todo", 39 | "sphinx.ext.coverage", 40 | "sphinx.ext.mathjax", 41 | "sphinx.ext.ifconfig", 42 | "sphinx.ext.napoleon", 43 | "sphinx.ext.viewcode", 44 | "sphinx.ext.githubpages", 45 | "recommonmark", 46 | "sphinx_autodoc_typehints", 47 | ] 48 | 49 | add_module_names = False 50 | typehints_fully_qualified = False 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ["_templates"] 54 | 55 | # List of patterns, relative to source directory, that match files and 56 | # directories to ignore when looking for source files. 57 | # This pattern also affects html_static_path and html_extra_path. 58 | exclude_patterns = [] 59 | 60 | 61 | # -- Options for HTML output ------------------------------------------------- 62 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 63 | 64 | html_theme = "sphinx_book_theme" 65 | 66 | html_theme_options = { 67 | "repository_url": "https://github.com/rekumar/labgraph", 68 | "use_repository_button": True, 69 | "home_page_in_toc": True, 70 | "show_navbar_depth": 0, 71 | } 72 | 73 | # html_logo = (Path(__file__).parent / "_static" / "logo.png").as_posix() 74 | html_title = "LabGraph" 75 | 76 | 77 | def run_apidoc(_): 78 | from pathlib import Path 79 | 80 | ignore_paths = [] 81 | 82 | ignore_paths = [ 83 | (Path(__file__).parent.parent.parent / p).absolute().as_posix() 84 | for p in ignore_paths 85 | ] 86 | 87 | argv = [ 88 | "-f", 89 | "-e", 90 | "-o", 91 | Path(__file__).parent.as_posix(), 92 | (Path(__file__).parent.parent.parent / "labgraph").absolute().as_posix(), 93 | ] + ignore_paths 94 | 95 | try: 96 | # Sphinx 1.7+ 97 | from sphinx.ext import apidoc 98 | 99 | apidoc.main(argv) 100 | except ImportError: 101 | # Sphinx 1.6 (and earlier) 102 | from sphinx import apidoc 103 | 104 | argv.insert(0, apidoc.__file__) 105 | apidoc.main(argv) 106 | 107 | 108 | def setup(app): 109 | app.connect("builder-inited", run_apidoc) 110 | -------------------------------------------------------------------------------- /docs/source/img/action_allowed_edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/action_allowed_edges.png -------------------------------------------------------------------------------- /docs/source/img/analysis_allowed_edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/analysis_allowed_edges.png -------------------------------------------------------------------------------- /docs/source/img/example_handoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/example_handoff.png -------------------------------------------------------------------------------- /docs/source/img/example_sample_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/example_sample_graph.png -------------------------------------------------------------------------------- /docs/source/img/handoff_graphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/handoff_graphs.png -------------------------------------------------------------------------------- /docs/source/img/logo/labgraph_dark mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/logo/labgraph_dark mode.png -------------------------------------------------------------------------------- /docs/source/img/logo/labgraph_light mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/logo/labgraph_light mode.png -------------------------------------------------------------------------------- /docs/source/img/material_allowed_edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/material_allowed_edges.png -------------------------------------------------------------------------------- /docs/source/img/measurement_allowed_edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/docs/source/img/measurement_allowed_edges.png -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. LabGraph documentation master file, created by 2 | sphinx-quickstart on Tue Nov 22 09:49:05 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. note:: 7 | This project is under active development. 8 | 9 | 10 | Welcome to LabGraph's documentation! 11 | ===================================== 12 | **LabGraph** is a Python library for storing and retrieving materials science data stored in MongoDB. This library enforces a data model tailored for experimental materials data. 13 | 14 | At a high-level, data is stored as a directed graph of four node types: :py:class:`Material `, :py:class:`Action `, :py:class:`Measurement `, and :py:class:`Analysis `. The content of these nodes is up to you -- we just make sure that any data you enter results in a valid graph. 15 | 16 | 17 | .. figure:: img/example_sample_graph.png 18 | :scale: 100 % 19 | :alt: An example graph for a single Sample 20 | 21 | This is a graph for a single Sample. Learn more in the :doc:`schema` section. 22 | 23 | 24 | Node Types 25 | """"""""""" 26 | Here is a brief overview of the four node types and their roles in the data model. Further details on allowed node relationships can be found in the :doc:`schema` section. 27 | 28 | - :py:class:`Material ` nodes are the fundamental building blocks of the data model. These represent a material in a given state. 29 | 30 | - :py:class:`Action ` nodes are operations that generate :py:class:`Material `s. :py:class:`Action ` nodes have incoming edges from any input :py:class:`Material `(s) and outgoing edges to generated :py:class:`Material `(s). An :py:class:`Action ` can generate :py:class:`Material `(s) without consuming any input :py:class:`Material `(s), as may be the case when procuring a :py:class:`Material ` from a vendor or receiving a :py:class:`Material ` from a collaborator. 31 | 32 | - :py:class:`Measurement ` nodes act upon a :py:class:`Material ` node to yield some form of raw data. 33 | 34 | - :py:class:`Analysis ` nodes act upon :py:class:`Measurement ` and/or :py:class:`Analysis ` nodes to yield some form of processed data. 35 | 36 | 37 | Samples = Graphs 38 | """"""""""""""""" 39 | As materials scientists, we execute sets of :py:class:`Action `s, :py:class:`Measurement `s, and :py:class:`Analysis ` to synthesize and study :py:class:`Material `s. In **LabGraph**, one such set of nodes is referred to as a :py:class:`Sample `. A :py:class:`Sample ` is simply a graph of nodes that captures the steps performed in an experiment. In typical usage, we will enter nodes into the database as part of a :py:class:`Sample `. This achieves a few things: 40 | 41 | - We can ensure that the graph we are entering is valid (i.e. it is a directed acyclic graph (DAG) with no isolated nodes). 42 | 43 | - Given a node, we can easily retrieve the most related nodes that belong to the same :py:class:`Sample `. 44 | 45 | - We can record any :py:class:`Sample `-level metadata (e.g. sample name, sample description, sample author, etc.). 46 | 47 | 48 | Database Backend 49 | """""""""""""""""""""" 50 | **LabGraph** uses `MongoDB `_ as our backend database. We communicate with the database 51 | using the `pymongo `_ package. 52 | 53 | 54 | 55 | Quickstart 56 | """"""""""" 57 | .. toctree:: 58 | :maxdepth: -1 59 | 60 | Getting Started 61 | Quickstart 62 | Schema 63 | Entering Data 64 | Retrieving Data 65 | .. Node Types 66 | 67 | 68 | 69 | .. Indices and tables 70 | .. ================== 71 | 72 | .. * :ref:`genindex` 73 | .. * :ref:`modindex` 74 | .. * :ref:`search` 75 | -------------------------------------------------------------------------------- /docs/source/labgraph.data.actors.rst: -------------------------------------------------------------------------------- 1 | labgraph.data.actors module 2 | =========================== 3 | 4 | .. automodule:: labgraph.data.actors 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.data.nodes.rst: -------------------------------------------------------------------------------- 1 | labgraph.data.nodes module 2 | ========================== 3 | 4 | .. automodule:: labgraph.data.nodes 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.data.rst: -------------------------------------------------------------------------------- 1 | labgraph.data package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | labgraph.data.actors 11 | labgraph.data.nodes 12 | labgraph.data.sample 13 | 14 | Module contents 15 | --------------- 16 | 17 | .. automodule:: labgraph.data 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | -------------------------------------------------------------------------------- /docs/source/labgraph.data.sample.rst: -------------------------------------------------------------------------------- 1 | labgraph.data.sample module 2 | =========================== 3 | 4 | .. automodule:: labgraph.data.sample 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.rst: -------------------------------------------------------------------------------- 1 | labgraph package 2 | ================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | labgraph.data 11 | labgraph.utils 12 | labgraph.views 13 | 14 | Module contents 15 | --------------- 16 | 17 | .. automodule:: labgraph 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | -------------------------------------------------------------------------------- /docs/source/labgraph.utils.config.config.rst: -------------------------------------------------------------------------------- 1 | labgraph.utils.config.config module 2 | =================================== 3 | 4 | .. automodule:: labgraph.utils.config.config 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.utils.config.rst: -------------------------------------------------------------------------------- 1 | labgraph.utils.config package 2 | ============================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | labgraph.utils.config.config 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: labgraph.utils.config 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/labgraph.utils.data_objects.rst: -------------------------------------------------------------------------------- 1 | labgraph.utils.data\_objects module 2 | =================================== 3 | 4 | .. automodule:: labgraph.utils.data_objects 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.utils.db_lock.rst: -------------------------------------------------------------------------------- 1 | labgraph.utils.db\_lock module 2 | ============================== 3 | 4 | .. automodule:: labgraph.utils.db_lock 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.utils.dev.rst: -------------------------------------------------------------------------------- 1 | labgraph.utils.dev module 2 | ========================= 3 | 4 | .. automodule:: labgraph.utils.dev 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.utils.graph.rst: -------------------------------------------------------------------------------- 1 | labgraph.utils.graph module 2 | =========================== 3 | 4 | .. automodule:: labgraph.utils.graph 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.utils.rst: -------------------------------------------------------------------------------- 1 | labgraph.utils package 2 | ====================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | labgraph.utils.config 11 | 12 | Submodules 13 | ---------- 14 | 15 | .. toctree:: 16 | :maxdepth: 4 17 | 18 | labgraph.utils.data_objects 19 | labgraph.utils.db_lock 20 | labgraph.utils.dev 21 | labgraph.utils.graph 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: labgraph.utils 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/source/labgraph.views.actors.rst: -------------------------------------------------------------------------------- 1 | labgraph.views.actors module 2 | ============================ 3 | 4 | .. automodule:: labgraph.views.actors 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.views.base.rst: -------------------------------------------------------------------------------- 1 | labgraph.views.base module 2 | ========================== 3 | 4 | .. automodule:: labgraph.views.base 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.views.graph_integrity.rst: -------------------------------------------------------------------------------- 1 | labgraph.views.graph\_integrity module 2 | ====================================== 3 | 4 | .. automodule:: labgraph.views.graph_integrity 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.views.nodes.rst: -------------------------------------------------------------------------------- 1 | labgraph.views.nodes module 2 | =========================== 3 | 4 | .. automodule:: labgraph.views.nodes 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/labgraph.views.rst: -------------------------------------------------------------------------------- 1 | labgraph.views package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | labgraph.views.actors 11 | labgraph.views.base 12 | labgraph.views.graph_integrity 13 | labgraph.views.nodes 14 | labgraph.views.sample 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: labgraph.views 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/labgraph.views.sample.rst: -------------------------------------------------------------------------------- 1 | labgraph.views.sample module 2 | ============================ 3 | 4 | .. automodule:: labgraph.views.sample 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | labgraph 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | labgraph 8 | -------------------------------------------------------------------------------- /docs/source/node_types.rst: -------------------------------------------------------------------------------- 1 | .. _node-types: 2 | 3 | Node Types 4 | =========== 5 | 6 | Material 7 | """""""" 8 | 9 | - :py:class:`Material ` nodes are the fundamental building blocks of the data model. These represent a material in a given state. 10 | 11 | 12 | .. autoclass:: labgraph.data.nodes.Material 13 | 14 | .. automethod:: __init__ 15 | 16 | 17 | 18 | 19 | Action 20 | """""""" 21 | 22 | - :py:class:`Action ` nodes are operations that generate new :py:class:`Material `s. :py:class:`Action ` nodes have incoming edges from any input :py:class:`Material `(s) and outgoing edges to generated :py:class:`Material `(s). An :py:class:`Action ` can generate :py:class:`Material `(s) without consuming any input :py:class:`Material `(s), as may be the case when procuring a :py:class:`Material ` from a vendor or receiving a :py:class:`Material ` from a collaborator. 23 | 24 | Measurement 25 | """""""""""" 26 | - :py:class:`Measurement ` nodes act upon a :py:class:`Material ` node to yield some form of raw data. 27 | 28 | 29 | Analysis 30 | """""""""" 31 | - :py:class:`Analysis ` nodes act upon a :py:class:`Measurement ` node to yield some form of processed data. 32 | -------------------------------------------------------------------------------- /docs/source/retrieving_data.rst: -------------------------------------------------------------------------------- 1 | .. warning:: 2 | This page is a work in progress that is mostly incomplete! But you could probably tell already :p 3 | 4 | Retrieving Data 5 | ================= 6 | 7 | -------------------------------------------------------------------------------- /docs/source/schema.rst: -------------------------------------------------------------------------------- 1 | Schema Overview 2 | ================ 3 | 4 | All data is stored as a `directed acyclic graph (DAG) `_. The "direction" of edges encodes the order that nodes (ie experimental steps) were performed in. Put another way, edges always point ahead in time. The "acyclic" constraint ensures that nodes cannot connect upstream to older nodes, which would be travelling back in time! 5 | 6 | The four :ref:`node types ` are designed to cover capture the generation of materials, the measurement of these materials, and analysis of these measurements. 7 | 8 | .. figure:: img/example_sample_graph.png 9 | :scale: 100 % 10 | :alt: An example graph for a single Sample 11 | 12 | This is a graph for a single Sample. The four node types are shown in different colors. The edges point forward in time, and the nodes are arranged in a topological order. The graph is acyclic, so there are no loops. The graph can branch to show multiple downstream processes (in this case, an :py:class:`Action ` and :py:class:`Measurement `) acting upon a single :py:class:`Material `. 13 | 14 | Allowed Node Relationships (Edges) 15 | =================================== 16 | Each :ref:`node type ` can only be connected to certain other node types. The allowed edges/relationships are described below. 17 | 18 | ############### 19 | Material Nodes 20 | ############### 21 | .. figure:: img/material_allowed_edges.png 22 | :scale: 100 % 23 | :alt: Allowed edges for Material nodes. 24 | 25 | 26 | :py:class:`Material ` nodes represent a material in a given state. Every :py:class:`Material ` node follows an :py:class:`Action ` node describing how the :py:class:`Material ` was generated (whether this is an experimental :py:class:`Action ` or simply procurement of a reagent from a supplier). A :py:class:`Material ` node can be followed by either an :py:class:`Action ` (e.g. where the :py:class:`Material ` is an input to an experimental step) or a :py:class:`Measurement ` (e.g. the :py:class:`Material ` is the subject of some test or characterization). 27 | 28 | ############### 29 | Action Nodes 30 | ############### 31 | .. figure:: img/action_allowed_edges.png 32 | :scale: 100 % 33 | :alt: Allowed edges for Action nodes. 34 | 35 | :py:class:`Action ` nodes bridge :py:class:`Material ` nodes. An :py:class:`Action ` will always generate at least one :py:class:`Material `. The :py:class:`Action ` may also take incoming edges from :py:class:`Material `(s), indicating that the upstream :py:class:`Material `(s) were required to perform the :py:class:`Action `. For example, a "mixing" :py:class:`Action ` might use upstream "solvent" and "reagent" :py:class:`Material `s to generate a "mixture" :py:class:`Material `. An :py:class:`Action ` can generate more than one :py:class:`Material `, as might be the case in a "separation" :py:class:`Action `. 36 | 37 | .. note:: 38 | In real life, we usually perform a series of actions to make our final "material". In **LabGraph**, sequential :py:class:`Action ` nodes must be bridged by intermediate :py:class:`Material ` nodes. **LabGraph** has helper functions to create these intermediates automatically. Just be aware that your graphs may have more :py:class:`Material ` nodes than you would expect just to support the graph semantics. 39 | 40 | ################## 41 | Measurement Nodes 42 | ################## 43 | .. figure:: img/measurement_allowed_edges.png 44 | :scale: 100 % 45 | :alt: Allowed edges for Measurement nodes. 46 | 47 | 48 | :py:class:`Measurement ` nodes are used to represent measurements of :py:class:`Material `s that generate raw data (e.g. a "powder diffraction" :py:class:`Measurement `). A :py:class:`Measurement ` node can only be connected to a single upstream :py:class:`Material ` node, which is the :py:class:`Material ` under test. A :py:class:`Measurement ` node can be connected to any number of downstream :py:class:`Analysis ` nodes. 49 | 50 | ############### 51 | Analysis Nodes 52 | ############### 53 | .. figure:: img/analysis_allowed_edges.png 54 | :scale: 100 % 55 | :alt: Allowed edges for Analysis nodes. 56 | 57 | :py:class:`Analysis ` nodes are used to represent the analysis of :py:class:`Measurement ` data to yield features. :py:class:`Analysis ` nodes can have any number of upstream :py:class:`Measurement `s or Analyses -- whatever raw data or analyzed features are required to perform the :py:class:`Analysis `. On the downstream side, an :py:class:`Analysis ` node can be followed by any number of other Analyses. :py:class:`Analysis ` is commonly the terminal node for a graph. 58 | 59 | Samples (Graphs) 60 | ================= 61 | A :py:class:`Sample ` is a DAG of :ref:`nodes ` that represent the materials, actions, measurements, and analyses that were performed on a single sample. Nodes are added to the database as part of a :py:class:`Sample `. Along with the nodes, the :py:class:`Sample ` can be given tags or additional fields to make it easy to retrieve the :py:class:`Sample ` at a later time. 62 | 63 | Additionally, hits from a node search can be expanded to the complete :py:class:`Sample ` that contains the nodes. For example, one could search for :py:class:`Analysis ` nodes named "Phase Identification" that identified some amount of a target phase. Then, by retrieving the :py:class:`Sample ` containing each of these nodes, we can compare the starting :py:class:`Material ` s and :py:class:`Action ` sequences that led to the target phase. 64 | 65 | Actors and AnalysisMethods 66 | ========================== 67 | 68 | When we look at Actions, Measurements, and Analyses, we'd like to track tool/method was used to perform these steps. This is important when: 69 | 70 | - you have a few different tools that can perform the same task (e.g. multiple furnaces) 71 | - you have a few different tasks that use the same tool (e.g. a liquid handler can do dilutions, mixtures, and dispenses). 72 | - you modify an instrument or analysis script over time, and you'd like to track which version was used. 73 | 74 | This tracking is formalized and enforced through the :py:class:`Actor ` and :py:class:`AnalysisMethod ` classes. Every :py:class:`Action ` and :py:class:`Measurement ` must be associated with an :py:class:`Actor `, and every :py:class:`Analysis ` must be associated with an :py:class:`AnalysisMethod `. -------------------------------------------------------------------------------- /docs/source/setup.rst: -------------------------------------------------------------------------------- 1 | Setting up Labgraph 2 | ==================== 3 | 4 | Labgraph is a Python package that can be installed using pip. It is designed to work with MongoDB, a NoSQL database. When you interact with Labgraph, it is communicating with MongoDB to store and retrieve data. 5 | 6 | Installing Labgraph using pip 7 | ------------------------------ 8 | 9 | You can install Labgraph using pip: 10 | 11 | .. code-block:: bash 12 | 13 | pip install labgraph 14 | 15 | .. warning:: 16 | 17 | Labgraph was written using Python 3.8, and is tested on Python 3.8, 3.9, 3.10. 18 | 19 | 20 | Installing MongoDB 21 | ------------------- 22 | `MongoDB `_ is a `NoSQL `_ database that is used to store and retrieve data. Labgraph uses MongoDB to store and retrieve data. MongoDB can be `installed on your local machine `_, or you can use a cloud service such as `MongoDB Atlas `_. 23 | 24 | Wherever you run MongoDB, you will need to know the host and port number. If you are running MongoDB locally, by default the host will be ``localhost`` and the port will be ``27017``. If you are using MongoDB Atlas, you will need to know the host and port number provided by MongoDB Atlas. These values are used to connect to MongoDB. 25 | 26 | .. note:: 27 | 28 | **A Quick MongoDB primer** 29 | 30 | Your MongoDB instance can hold multiple `databases`. Each `database` can hold multiple `collections`, and each `collection` can hold multiple `documents`. A `document` is a single data entry, which is basically a JSON object. Labgraph will create its own `database` (by default named "Labgraph") in your MongoDB instance, in which it will create `collections` for each type of Labgraph data (nodes, actors, samples, etc). Each node will be a `document` in its respective `collection`. 31 | 32 | 33 | The Labgraph Config File 34 | ------------------------- 35 | Labgraph needs to know how to find your MongoDB instance. This is done using a config file. You will only need to set this up once unless your MongoDB information changes. 36 | 37 | The config file is a TOML file that contains the information needed to connect to MongoDB. An example is shown below. The "username" and "password" fields are optional, depending on whether your MongoDB instance requires authentication. 38 | 39 | .. code:: toml 40 | 41 | [mongodb] 42 | host = "localhost" 43 | port = 27017 44 | db_name = "Labgraph" 45 | username = "my_username" 46 | password = "my_password" 47 | 48 | Labgraph provides a helper function `labgraph.utils.make_config` to make this file. Running this will walk you through the process of creating the config file. This function provides default values which are set for a local MongoDB instance. 49 | 50 | 51 | .. code-block:: python 52 | 53 | from labgraph.utils import make_config 54 | make_config() 55 | 56 | By default this config file will be created inside the Labgraph package directory. You can put this config file wherever you want -- if it is not in the default location, however, you need to set an environment variable `LABGRAPH_CONFIG` to point to the location of the config file. 57 | 58 | -------------------------------------------------------------------------------- /labgraph/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.1" 2 | 3 | from labgraph.data import ( 4 | Material, 5 | Action, 6 | Measurement, 7 | Analysis, 8 | Ingredient, 9 | WholeIngredient, 10 | Actor, 11 | Sample, 12 | ) 13 | 14 | from labgraph.views import SampleView, ActorView 15 | -------------------------------------------------------------------------------- /labgraph/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | !build/ -------------------------------------------------------------------------------- /labgraph/dashboard/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The UI and API module. 3 | """ 4 | 5 | from pathlib import Path 6 | 7 | from flask import Flask 8 | from flask_cors import CORS 9 | from .routes import init_app as init_app_route 10 | 11 | 12 | def create_app(cors=False): 13 | """ 14 | Create app, which is a factory function to be called when serving the app 15 | """ 16 | app = Flask(__name__, static_folder=(Path(__file__).parent / "ui").as_posix()) 17 | if cors: 18 | CORS(app) 19 | init_app_route(app) 20 | return app 21 | -------------------------------------------------------------------------------- /labgraph/dashboard/lab_views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/labgraph/dashboard/lab_views.py -------------------------------------------------------------------------------- /labgraph/dashboard/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .basic_route import modules 2 | from .sample import sample_bp 3 | from .nodes import material_bp, measurement_bp, analysis_bp, action_bp 4 | from .graph import graph_bp 5 | 6 | 7 | def init_app(app): 8 | """ 9 | Add routes to the app 10 | """ 11 | app.register_blueprint(modules) 12 | app.register_blueprint(sample_bp) 13 | app.register_blueprint(material_bp) 14 | app.register_blueprint(measurement_bp) 15 | app.register_blueprint(analysis_bp) 16 | app.register_blueprint(action_bp) 17 | app.register_blueprint(graph_bp) 18 | -------------------------------------------------------------------------------- /labgraph/dashboard/routes/basic_route.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import cast 3 | from flask import Blueprint, send_from_directory 4 | 5 | modules = Blueprint( 6 | "basic_route", 7 | __name__, 8 | static_folder=(Path(__file__).parent.parent / "ui").as_posix(), 9 | ) 10 | 11 | 12 | @modules.route("/", defaults={"path": ""}) 13 | @modules.route("/") 14 | def serve(path): 15 | if path != "" and (Path(modules.static_folder) / path).exists(): # type: ignore 16 | return send_from_directory(cast(str, modules.static_folder), path) 17 | return send_from_directory(cast(str, modules.static_folder), "index.html") 18 | -------------------------------------------------------------------------------- /labgraph/dashboard/routes/graph.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from bson import ObjectId 3 | from flask import Blueprint, request 4 | from labgraph.views import ( 5 | SampleView, 6 | MeasurementView, 7 | MaterialView, 8 | ActionView, 9 | AnalysisView, 10 | ) 11 | from dataclasses import dataclass, asdict 12 | import networkx as nx 13 | from networkx.drawing.nx_agraph import graphviz_layout 14 | from .utils import MongoEncoder 15 | import json 16 | 17 | sample_view: SampleView = SampleView() 18 | node_views = { 19 | "measurement": MeasurementView(), 20 | "material": MaterialView(), 21 | "action": ActionView(), 22 | "analysis": AnalysisView(), 23 | } 24 | 25 | 26 | ### Dataclasses 27 | @dataclass 28 | class NodeEntry: 29 | _id: str 30 | x: float 31 | y: float 32 | label: str 33 | size: float 34 | contents: dict 35 | 36 | 37 | @dataclass 38 | class EdgeEntry: 39 | source: str 40 | target: str 41 | contents: dict 42 | 43 | 44 | @dataclass 45 | class Graph: 46 | nodes: List[NodeEntry] 47 | edges: List[EdgeEntry] 48 | 49 | 50 | ### API 51 | graph_bp = Blueprint("/graph", __name__, url_prefix="/api/graph") 52 | 53 | 54 | @graph_bp.route("/complete", methods=["GET"]) 55 | def get_complete_graph() -> Graph: 56 | """ 57 | Get the summary of all samples 58 | """ 59 | 60 | g = nx.DiGraph() 61 | 62 | for node_type, view in node_views.items(): 63 | i = 0 64 | for node in view._collection.find(): 65 | upstream = node.pop("upstream") 66 | downstream = node.pop("downstream") 67 | g.add_node(node["_id"], type=node_type, **node) 68 | for u in upstream: 69 | g.add_edge(u["node_id"], node["_id"]) 70 | for d in downstream: 71 | g.add_edge(node["_id"], d["node_id"]) 72 | 73 | i += 1 74 | if i > 100: 75 | break 76 | 77 | layout = graphviz_layout(g, prog="dot") 78 | 79 | nodes = [] 80 | edges = [] 81 | 82 | for node_id, node in g.nodes(data=True): 83 | nodes.append( 84 | NodeEntry( 85 | _id=str(node_id), 86 | x=layout[node_id][0], 87 | y=layout[node_id][1], 88 | label=node.get("name", "oops"), 89 | size=10, 90 | contents=node, 91 | ) 92 | ) 93 | for source, target, data in g.edges(data=True): 94 | edges.append( 95 | EdgeEntry( 96 | source=str(source), 97 | target=str(target), 98 | contents=data, 99 | ) 100 | ) 101 | gdict = asdict(Graph(nodes, edges)) 102 | return json.loads(MongoEncoder().encode(gdict)) 103 | 104 | 105 | @graph_bp.route("/samples", methods=["POST"]) 106 | def get_samples_graph() -> Graph: 107 | content = request.get_json(force=True) 108 | print(content) 109 | sample_ids = content["sample_ids"] 110 | 111 | g = nx.DiGraph() 112 | 113 | for sample_id in sample_ids: 114 | sample = sample_view.get(ObjectId(sample_id)) 115 | g = nx.compose(g, sample.graph) 116 | 117 | layout = graphviz_layout(g, prog="dot") 118 | 119 | nodes = [] 120 | edges = [] 121 | 122 | for node_id, node in g.nodes(data=True): 123 | nodes.append( 124 | NodeEntry( 125 | _id=str(node_id), 126 | x=layout[node_id][0], 127 | y=layout[node_id][1], 128 | label=node["name"], 129 | size=10, 130 | contents=node, 131 | ) 132 | ) 133 | for source, target, data in g.edges(data=True): 134 | edges.append( 135 | EdgeEntry( 136 | source=str(source), 137 | target=str(target), 138 | contents=data, 139 | ) 140 | ) 141 | gdict = asdict(Graph(nodes, edges)) 142 | return json.loads(MongoEncoder().encode(gdict)) 143 | -------------------------------------------------------------------------------- /labgraph/dashboard/routes/nodes.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | from bson import ObjectId 3 | from flask import Blueprint 4 | from labgraph.views import MeasurementView, MaterialView, ActionView, AnalysisView 5 | from labgraph.views.base import BaseView 6 | 7 | 8 | node_views = { 9 | "measurement": MeasurementView(), 10 | "material": MaterialView(), 11 | "action": ActionView(), 12 | "analysis": AnalysisView(), 13 | } 14 | 15 | 16 | measurement_bp = Blueprint("/measurement", __name__, url_prefix="/api/measurement") 17 | material_bp = Blueprint("/material", __name__, url_prefix="/api/material") 18 | action_bp = Blueprint("/action", __name__, url_prefix="/api/action") 19 | analysis_bp = Blueprint("/analysis", __name__, url_prefix="/api/analysis") 20 | 21 | 22 | def get_node( 23 | node_type: Literal["measurement", "material", "analysis", "action"], node_id: str 24 | ): 25 | """ 26 | Get the status of a node 27 | """ 28 | try: 29 | if node_type not in node_views: 30 | raise ValueError(f"Invalid node type {node_type}") 31 | view: BaseView = node_views[node_type] 32 | node_entry = view._collection.find_one( 33 | {"_id": ObjectId(node_id)}, 34 | ) 35 | if node_entry is None: 36 | raise ValueError(f"No {node_type} node found with id {node_id}") 37 | except ValueError as exception: 38 | return {"status": "error", "errors": exception.args[0]}, 400 39 | 40 | node_entry["_id"] = str(node_entry["_id"]) 41 | node_entry["upstream"] = [ 42 | { 43 | "node_type": i["node_type"], 44 | "node_id": str(i["node_id"]), 45 | } 46 | for i in node_entry["upstream"] 47 | ] 48 | node_entry["downstream"] = [ 49 | { 50 | "node_type": i["node_type"], 51 | "node_id": str(i["node_id"]), 52 | } 53 | for i in node_entry["downstream"] 54 | ] 55 | 56 | return node_entry 57 | 58 | 59 | @measurement_bp.route("/", methods=["GET"]) 60 | def get_measurement(measurement_id): 61 | return get_node("measurement", measurement_id) 62 | 63 | 64 | @material_bp.route("/", methods=["GET"]) 65 | def get_material(material_id): 66 | return get_node("material", material_id) 67 | 68 | 69 | @action_bp.route("/", methods=["GET"]) 70 | def get_action(action_id): 71 | return get_node("action", action_id) 72 | 73 | 74 | @analysis_bp.route("/", methods=["GET"]) 75 | def get_analysis(analysis_id): 76 | return get_node("analysis", analysis_id) 77 | -------------------------------------------------------------------------------- /labgraph/dashboard/routes/sample.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | from flask import Blueprint 3 | from labgraph.views import SampleView 4 | import pymongo 5 | 6 | sample_view: SampleView = SampleView() 7 | sample_bp = Blueprint("/sample", __name__, url_prefix="/api/sample") 8 | 9 | 10 | @sample_bp.route("/summary/", methods=["GET"]) 11 | def get_sample_summary(count: int): 12 | """ 13 | Get the summary of the "count" most recent samples. if count==0, returns all samples. samples are returned in reverse chronological order. 14 | """ 15 | samples = [] 16 | count = int(count) 17 | 18 | kws = dict( 19 | projection={ 20 | "_id": 1, 21 | "name": 1, 22 | "description": 1, 23 | "nodes": 1, 24 | "tags": 1, 25 | "created_at": 1, 26 | # "updated_at": 1, 27 | }, 28 | sort=[("created_at", pymongo.DESCENDING)], 29 | ) 30 | 31 | count = int(count) 32 | if count != 0: 33 | kws["limit"] = count 34 | 35 | for entry in sample_view._collection.find( 36 | {}, 37 | **kws, 38 | ): 39 | samples.append( 40 | { 41 | "_id": str(entry["_id"]), 42 | "name": entry["name"], 43 | "description": entry["description"], 44 | "nodes": { 45 | k: [str(oid) for oid in v] for k, v in entry["nodes"].items() 46 | }, 47 | "tags": entry["tags"], 48 | "created_at": entry["created_at"], 49 | # "updated_at": entry["updated_at"], 50 | } 51 | ) 52 | 53 | return samples 54 | 55 | 56 | # @sample_bp.route("/submit", methods=["POST"]) 57 | # def submit_new_sample(): 58 | # """ 59 | # Submit a new sample to the system 60 | # """ 61 | # data = request.get_json(force=True) # type: ignore 62 | # try: 63 | # sample = SampleInputFormat(**data) # type: ignore 64 | # sample_id = sample_view.add_sample(Sample.from_dict(data)) 65 | # except ValidationError as exception: 66 | # return {"status": "error", "errors": exception.errors()}, 400 67 | # except ValueError as exception: 68 | # return {"status": "error", "errors": exception.args[0]}, 400 69 | 70 | # return {"status": "success", "data": {"sample_id": sample_id}} 71 | 72 | 73 | @sample_bp.route("/", methods=["GET"]) 74 | def get_sample(sample_id): 75 | """ 76 | Get the status of a sample 77 | """ 78 | try: 79 | sample_entry = sample_view._collection.find_one( 80 | {"_id": ObjectId(sample_id)}, 81 | ) 82 | if sample_entry is None: 83 | raise ValueError(f"No sample found with id {sample_id}") 84 | 85 | except ValueError as exception: 86 | return {"status": "error", "errors": exception.args[0]}, 400 87 | 88 | sample_entry["_id"] = str(sample_entry["_id"]) 89 | sample_entry["nodes"] = { 90 | k: [str(oid) for oid in v] for k, v in sample_entry["nodes"].items() 91 | } 92 | return sample_entry 93 | -------------------------------------------------------------------------------- /labgraph/dashboard/routes/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | from bson import ObjectId 4 | 5 | 6 | class MongoEncoder(json.JSONEncoder): 7 | def default(self, obj): 8 | if isinstance(obj, ObjectId): 9 | return str(obj) 10 | if isinstance(obj, datetime.datetime): 11 | return obj.isoformat() 12 | return json.JSONEncoder.default(self, obj) 13 | -------------------------------------------------------------------------------- /labgraph/dashboard/ui/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.6894ff8f.css", 4 | "main.js": "/static/js/main.37fab58c.js", 5 | "static/js/787.28cb0dcd.chunk.js": "/static/js/787.28cb0dcd.chunk.js", 6 | "index.html": "/index.html", 7 | "main.6894ff8f.css.map": "/static/css/main.6894ff8f.css.map", 8 | "main.37fab58c.js.map": "/static/js/main.37fab58c.js.map", 9 | "787.28cb0dcd.chunk.js.map": "/static/js/787.28cb0dcd.chunk.js.map" 10 | }, 11 | "entrypoints": [ 12 | "static/css/main.6894ff8f.css", 13 | "static/js/main.37fab58c.js" 14 | ] 15 | } -------------------------------------------------------------------------------- /labgraph/dashboard/ui/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /labgraph/dashboard/ui/index.html: -------------------------------------------------------------------------------- 1 | Simple Mantine Template
-------------------------------------------------------------------------------- /labgraph/dashboard/ui/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /labgraph/dashboard/ui/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/labgraph/dashboard/ui/sample.png -------------------------------------------------------------------------------- /labgraph/dashboard/ui/static/css/main.6894ff8f.css: -------------------------------------------------------------------------------- 1 | #about{align-items:center;display:flex;height:100vh;min-height:100vh;padding-top:0}#about .about-content{background-color:rgba(255,244,213,.6);border-radius:25px;padding:160px 120px;text-align:center}#about .about-content .title{color:#2d2d2d;font-family:Montserrat,sans-serif;font-size:7vmin;font-weight:0;letter-spacing:2px;margin-top:0}#about .about-content .buttons{display:flex;flex-direction:row;gap:10px;justify-content:center}#about .about-content a{color:#fd7e14}@media only screen and (max-width:1086px){#about .about-content{padding:110px 120px}}@media only screen and (max-width:768px){#about .about-content{padding:120px 40px}}@media only screen and (max-width:350px){#about .about-content{padding:80px 20px}}@media only screen and (max-width:425px){#about .mantine-Container-root{padding:0}}header{position:absolute;z-index:99}header .navbar{align-items:center;display:flex;flex-direction:row;gap:25px}header .navbar .navbar-item{cursor:pointer;font-weight:500}header .navbar .navbar-item a{color:#2d2d2d;transition:all .1s ease}header .navbar .navbar-item a:hover{color:#0598fa;text-decoration:none}header .content-desktop{align-items:center;display:flex;justify-content:space-between}header .content-mobile .burger-button{float:right}@media only screen and (min-width:769px){header{left:60px;right:60px;top:60px}header .content-desktop{display:flex}header .content-mobile{display:none}}@media only screen and (max-width:768px){header{left:20px;right:20px;top:30px}header .content-desktop{display:none}header .content-mobile{display:block}}.menu{display:flex!important;flex-direction:column;gap:30px;justify-content:space-between!important}.menu .menu-items{display:flex;flex-direction:column;gap:10px}footer{padding-bottom:3rem;padding-top:3rem}@media only screen and (max-width:768px){footer{text-align:center}}#section-one h2,#section-one h3{margin:0;padding:0}@media only screen and (max-width:769px){#section-one .mantine-Carousel-root{padding-left:0;padding-right:0}}body{background-color:#f8f6f6;margin:0;padding:0}footer,section{padding:4rem 60px}@media only screen and (max-width:768px){footer,section{padding-left:1rem;padding-right:1rem}footer .mantine-Container-root,section .mantine-Container-root{padding:0}} 2 | /*# sourceMappingURL=main.6894ff8f.css.map*/ -------------------------------------------------------------------------------- /labgraph/dashboard/ui/static/css/main.6894ff8f.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.6894ff8f.css","mappings":"AAAA,OAII,kBAAmB,CADnB,YAAa,CADb,YAAa,CADb,gBAAiB,CAIjB,aAAgB,CALpB,sBASQ,qCAA8B,CAE9B,mBADA,mBAAoB,CAFpB,iBAGmB,CAX3B,6BAeY,aAAsB,CADtB,iCAAqC,CAErC,eAAgB,CAEhB,aAAe,CADf,kBAAmB,CAEnB,YAAe,CAnB3B,+BAuBY,YAAa,CACb,kBAAmB,CAEnB,SADA,sBACS,CA1BrB,wBA6BY,aAAc,CAAI,0CA7B9B,sBAgCY,mBAAoB,CAU3B,CAPG,yCAnCR,sBAoCY,kBAAmB,CAM1B,CAHG,yCAvCR,sBAwCY,iBAAkB,CAEzB,CAED,yCA5CJ,+BA8CY,SAAY,CACf,CC/CT,OACI,iBAAkB,CAClB,UAAW,CAFf,eAQQ,mBAHA,YAAa,CACb,kBAAmB,CACnB,QACmB,CAR3B,4BAWY,cAAe,CACf,eAAgB,CAZ5B,8BAegB,aAAc,CACd,uBAAyB,CAhBzC,oCAoBgB,aAAc,CACd,oBAAqB,CArBrC,wBA6BQ,mBADA,YAAa,CADb,6BAEmB,CA7B3B,sCAkCY,WAAY,CACf,yCAnCT,OAiDQ,SAAU,CACV,WAFA,QAEW,CAlDnB,wBAyCY,YAAa,CAzCzB,uBA6CY,YAAa,CAChB,CAOL,yCArDJ,OA+DQ,SAAU,CACV,WAFA,QAEW,CAhEnB,wBAuDY,YAAa,CAvDzB,uBA2DY,aAAc,CACjB,CAQT,MACI,sBAAwB,CACxB,qBAAsB,CAEtB,SADA,uCACS,CAJb,kBAOQ,YAAa,CACb,qBAAsB,CACtB,QAAS,CAGZ,OC9ED,oBADA,gBACoB,CAEpB,yCAJJ,OAKQ,iBAAkB,CAEzB,CCPD,gCACY,QAAS,CAAE,SAAW,CAAE,yCADpC,oCAKY,cAAiB,CACjB,eAAkB,CACrB,CCFT,KAGE,yBAFA,QAAS,CACT,SACoC,CACrC,eAMC,iBAAkB,CAElB,yCANF,eAOI,iBAAkB,CAClB,kBAAmB,CARvB,+DAWM,SAAY,CACb","sources":["Styles/About.scss","Styles/Header.scss","Styles/Footer.scss","Styles/SectionOne.scss","index.scss"],"sourcesContent":["#about {\n min-height: 100vh;\n height: 100vh;\n display: flex;\n align-items: center;\n padding-top: 0px;\n\n .about-content {\n text-align: center;\n background-color: rgba(#fff4d5, 0.60);\n padding: 160px 120px;\n border-radius: 25px;\n \n .title {\n font-family: 'Montserrat', sans-serif;\n color: rgb(45, 45, 45);\n font-size: 7vmin;\n letter-spacing: 2px;\n font-weight: 00;\n margin-top: 0px;\n }\n\n .buttons {\n display: flex;\n flex-direction: row;\n justify-content: center;\n gap: 10px;\n }\n \n a { color: #fd7e14; }\n\n @media only screen and (max-width: 1086px) {\n padding: 110px 120px;\n }\n\n @media only screen and (max-width: 768px) {\n padding: 120px 40px;\n }\n\n @media only screen and (max-width: 350px) {\n padding: 80px 20px;\n }\n }\n\n @media only screen and (max-width: 425px) {\n .mantine-Container-root {\n padding: 0px;\n }\n }\n}","header {\n position: absolute;\n z-index: 99;\n\n .navbar {\n display: flex;\n flex-direction: row;\n gap: 25px;\n align-items: center;\n\n .navbar-item {\n cursor: pointer;\n font-weight: 500;\n\n a {\n color: #2d2d2d;\n transition: all 0.1s ease;\n }\n\n a:hover {\n color: #0598fa;\n text-decoration: none;\n }\n }\n }\n\n .content-desktop {\n justify-content: space-between;\n display: flex;\n align-items: center;\n }\n\n .content-mobile {\n .burger-button {\n float: right;\n }\n }\n\n /* Responsive header */\n @media only screen and (min-width: 769px) {\n .content-desktop {\n display: flex;\n }\n\n .content-mobile {\n display: none;\n }\n\n top: 60px;\n left: 60px;\n right: 60px;\n }\n\n @media only screen and (max-width: 768px) {\n .content-desktop {\n display: none;\n }\n\n .content-mobile {\n display: block;\n }\n\n top: 30px;\n left: 20px;\n right: 20px;\n }\n}\n\n.menu {\n display: flex !important;\n flex-direction: column;\n justify-content: space-between !important;\n gap: 30px;\n\n .menu-items {\n display: flex;\n flex-direction: column;\n gap: 10px;\n\n .menu-item {}\n }\n}","footer {\n padding-top: 3rem;\n padding-bottom: 3rem;\n\n @media only screen and (max-width: 768px) {\n text-align: center;\n }\n}","#section-one {\n h2,h3 { margin: 0; padding: 0 }\n \n @media only screen and (max-width: 769px) {\n .mantine-Carousel-root {\n padding-left: 0px;\n padding-right: 0px;\n }\n }\n}","@import './Styles/About.scss';\n@import './Styles/Header.scss';\n@import './Styles/Footer.scss';\n@import './Styles/Content.scss';\n\nbody {\n margin: 0;\n padding: 0;\n background-color: rgb(248, 246, 246);\n}\n\nsection,\nfooter {\n padding: 4rem 0rem;\n padding-right: 60px;\n padding-left: 60px;\n\n @media only screen and (max-width: 768px) {\n padding-left: 1rem;\n padding-right: 1rem;\n\n .mantine-Container-root {\n padding: 0px;\n }\n }\n}"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /labgraph/dashboard/ui/static/js/787.28cb0dcd.chunk.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkmy_app=self.webpackChunkmy_app||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]); 2 | //# sourceMappingURL=787.28cb0dcd.chunk.js.map -------------------------------------------------------------------------------- /labgraph/dashboard/ui/static/js/main.37fab58c.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 2 | 3 | /** 4 | * @license React 5 | * react-dom.production.min.js 6 | * 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | /** 14 | * @license React 15 | * react-jsx-runtime.production.min.js 16 | * 17 | * Copyright (c) Facebook, Inc. and its affiliates. 18 | * 19 | * This source code is licensed under the MIT license found in the 20 | * LICENSE file in the root directory of this source tree. 21 | */ 22 | 23 | /** 24 | * @license React 25 | * react.production.min.js 26 | * 27 | * Copyright (c) Facebook, Inc. and its affiliates. 28 | * 29 | * This source code is licensed under the MIT license found in the 30 | * LICENSE file in the root directory of this source tree. 31 | */ 32 | 33 | /** 34 | * @license React 35 | * scheduler.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | 43 | /** @license React v16.13.1 44 | * react-is.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | -------------------------------------------------------------------------------- /labgraph/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .actors import Actor 2 | from .sample import Sample 3 | from .nodes import Material, Action, Measurement, Analysis, Ingredient, WholeIngredient 4 | -------------------------------------------------------------------------------- /labgraph/data/actors.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | import datetime 3 | from typing import Any, Dict, List 4 | from bson import BSON, ObjectId 5 | 6 | 7 | class BaseActor: 8 | def __init__( 9 | self, 10 | name: str, 11 | description: str, 12 | tags: List[str] = None, 13 | **user_fields, 14 | ): 15 | self.name = name 16 | self.description = description 17 | 18 | if tags is None: 19 | self.tags = [] 20 | else: 21 | self.tags = tags 22 | self._user_fields = user_fields 23 | 24 | self._id = ObjectId() 25 | self._version_history = [ 26 | { 27 | "version": 1, 28 | "description": "Initial version.", 29 | "version_date": datetime.datetime.now().replace( 30 | microsecond=0 31 | ), # remove microseconds, they get lost in MongoDB anyways 32 | } 33 | ] 34 | 35 | @property 36 | def id(self): 37 | return self._id 38 | 39 | @property 40 | def version_history(self): 41 | return self._version_history.copy() 42 | 43 | @property 44 | def version(self): 45 | return max([version["version"] for version in self.version_history]) 46 | 47 | def to_dict(self): 48 | d = deepcopy(self.__dict__) 49 | d.pop("_version_history") 50 | d["version"] = self.version 51 | 52 | # in case we reformat version_history within the property down the line 53 | d["version_history"] = self.version_history 54 | 55 | user_fields = d.pop("_user_fields") 56 | for field_name in user_fields: 57 | if field_name in d: 58 | raise ValueError( 59 | f"Parameter name {field_name} already exists as a default key of an {self.__class__.__name__}. Please rename this parameter and try again." 60 | ) 61 | d.update(user_fields) 62 | return d 63 | 64 | @classmethod 65 | def from_dict(cls, entry: Dict[str, Any]): 66 | _id = entry.pop("_id", None) 67 | entry.pop("created_at", None) 68 | entry.pop("updated_at", None) 69 | entry.pop("version", None) 70 | version_history = entry.pop("version_history", None) 71 | 72 | obj = cls(**entry) 73 | if _id is not None: 74 | obj._id = _id 75 | obj._version_history = version_history 76 | 77 | return obj 78 | 79 | # @classmethod 80 | # @abstractmethod 81 | # def from_dict(cls, entry: Dict[str, Any]): 82 | # raise NotImplementedError() 83 | 84 | def __repr__(self): 85 | return f"<{self.__class__.__name__}: {self.name} v{self.version}>" 86 | 87 | def new_version(self, description: str, **user_fields): 88 | """Increment the version of the actor and record a description of what changed in this version. This is used to track changes (instrument service, modification, update to analysis code, etc) to an actor over time. 89 | 90 | Note that this function only changes the actor locally. You need to call .update() in the database view to record this updated version to the database. 91 | 92 | Args: 93 | description (str): Description of the changes made to the actor in this version 94 | **user_fields (dict): Any additional fields you want to add to the version update log (e.g. "instrument_service", "code diff", etc.) 95 | """ 96 | 97 | try: 98 | BSON.encode(user_fields) 99 | except: 100 | raise ValueError( 101 | f"User fields must be BSON-encodable. Something in {user_fields} is not BSON-encodable." 102 | ) 103 | 104 | self._version_history.append( 105 | { 106 | "version": self.version + 1, 107 | "description": description, 108 | "version_date": datetime.datetime.now().replace( 109 | microsecond=0 110 | ), # remove microseconds, they get lost in MongoDB anyways, 111 | **user_fields, 112 | } 113 | ) 114 | 115 | def __eq__(self, other): 116 | if not isinstance(other, self.__class__): 117 | return False 118 | if self.id != other.id: 119 | return False 120 | if self.to_dict() != other.to_dict(): 121 | raise ValueError( 122 | f"Objects have the same id but different attributes: {self.to_dict()} != {other.to_dict()}. Be careful, you have two different version of the same object!" 123 | ) 124 | return True 125 | 126 | @classmethod 127 | def __get_view(cls): 128 | from labgraph.views import ActorView 129 | 130 | VIEWS = { 131 | "Actor": ActorView, 132 | } 133 | return VIEWS[cls.__name__]() 134 | 135 | @classmethod 136 | def get_by_name(cls, name: str) -> "BaseActor": 137 | """Get an Actor by name 138 | 139 | Args: 140 | name (str): Name of the actor or analysis method 141 | 142 | Returns: 143 | BaseActor: Actor object 144 | """ 145 | 146 | view = cls.__get_view() 147 | return view.get_by_name(name)[0] 148 | 149 | @classmethod 150 | def get_by_tags(cls, tags: List[str]) -> List["BaseActor"]: 151 | """Get an Actor by tags 152 | 153 | Args: 154 | tags (List[str]): Tags of the actor or analysis method 155 | 156 | Returns: 157 | List[BaseActor]: List of Actor objects 158 | """ 159 | 160 | view = cls.__get_view() 161 | return view.get_by_tags(tags) 162 | 163 | @classmethod 164 | def filter( 165 | cls, 166 | filter_dict: dict, 167 | datetime_min: datetime = None, 168 | datetime_max: datetime = None, 169 | ) -> List["BaseActor"]: 170 | """Thin wrapper around pymongo find method, with an extra datetime filter. 171 | 172 | Args: 173 | filter_dict (Dict): standard mongodb filter dictionary. 174 | datetime_min (datetime, optional): entries from before this datetime will not be shown. Defaults to None. 175 | datetime_max (datetime, optional): entries from after this datetime will not be shown. Defaults to None. 176 | 177 | Returns: 178 | List[BaseActor]: List of Actors that match the filter 179 | """ 180 | view = cls.__get_view() 181 | return view.filter(filter_dict, datetime_min, datetime_max) 182 | 183 | @classmethod 184 | def filter_one( 185 | cls, 186 | filter_dict: dict, 187 | datetime_min: datetime = None, 188 | datetime_max: datetime = None, 189 | ) -> "BaseActor": 190 | """Thin wrapper around pymongo find_one method, with an extra datetime filter. 191 | 192 | Args: 193 | filter_dict (Dict): standard mongodb filter dictionary. 194 | datetime_min (datetime, optional): entries from before this datetime will not be shown. Defaults to None. 195 | datetime_max (datetime, optional): entries from after this datetime will not be shown. Defaults to None. 196 | 197 | Returns: 198 | BaseActor: Actor that matches the filter 199 | """ 200 | view = cls.__get_view() 201 | return view.filter_one(filter_dict, datetime_min, datetime_max) 202 | 203 | def save(self): 204 | view = self.__get_view() 205 | view.add(entry=self, if_already_in_db="update") 206 | 207 | def __getitem__(self, key: str): 208 | return self._user_fields[key] 209 | 210 | def __setitem__(self, key: str, value: Any): 211 | self._user_fields[key] = value 212 | 213 | def keys(self): 214 | return list(self._user_fields.keys()) 215 | 216 | 217 | class Actor(BaseActor): 218 | """An experimental actor (hardware, system, or lab facility) that can perform synthesis Action's or Measurement's""" 219 | 220 | def __init__( 221 | self, name: str, description: str, tags: List[str] = None, **user_fields 222 | ): 223 | super().__init__(name=name, description=description, tags=tags, **user_fields) 224 | -------------------------------------------------------------------------------- /labgraph/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI related functions 3 | """ 4 | 5 | from .cli import cli 6 | -------------------------------------------------------------------------------- /labgraph/scripts/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Useful CLI tools for the alab_management package. 3 | """ 4 | 5 | import click 6 | 7 | from .launch_client import launch_dashboard 8 | 9 | CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) 10 | 11 | 12 | @click.group("cli", context_settings=CONTEXT_SETTINGS) 13 | def cli(): 14 | """ALab Data CLI tools""" 15 | click.echo(r"""--- Labgraph ---""") 16 | 17 | 18 | # @cli.command("init", short_help="Init definition folder with default configuration") 19 | # def init_cli(): 20 | # if init_project(): 21 | # click.echo("Done") 22 | # else: 23 | # click.echo("Stopped") 24 | 25 | 26 | # @cli.command("setup", short_help="Read and write definitions to database") 27 | # def setup_lab_cli(): 28 | # if setup_lab(): 29 | # click.echo("Done") 30 | # else: 31 | # click.echo("Stopped") 32 | 33 | 34 | @cli.command("launch", short_help="Start to run the Labgraph client + API") 35 | @click.option( 36 | "--host", 37 | default="127.0.0.1", 38 | ) 39 | @click.option("-p", "--port", default="8899", type=int) 40 | @click.option("--debug", default=False, is_flag=True) 41 | def launch_client_cli(host, port, debug): 42 | click.echo(f"The client will be served on http://{host}:{port}") 43 | launch_dashboard(host, port, debug) 44 | 45 | 46 | @cli.command("mongodb_config", short_help="Configure your connection to MongoDB.") 47 | def launch_mongodb_config(): 48 | from labgraph.utils.config import make_config 49 | 50 | make_config() 51 | -------------------------------------------------------------------------------- /labgraph/scripts/launch_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | The script to launch task_view and executor, which are the core of the system. 3 | """ 4 | 5 | import multiprocessing 6 | import sys 7 | import time 8 | from threading import Thread 9 | 10 | from gevent.pywsgi import WSGIServer 11 | 12 | try: 13 | multiprocessing.set_start_method("spawn") 14 | except RuntimeError: 15 | pass 16 | 17 | 18 | def launch_dashboard(host: str, port: int, debug: bool = False): 19 | from ..dashboard import create_app 20 | 21 | if debug: 22 | print("Debug mode is on, the dashboard will be served with CORS enabled!") 23 | app = create_app(cors=debug) # if debug enabled, allow cross-origin requests to API 24 | if debug: 25 | server = WSGIServer((host, port), app) # print server's log on the console 26 | else: 27 | server = WSGIServer((host, port), app, log=None, error_log=None) 28 | server.serve_forever() 29 | 30 | 31 | def launch_client(host, port, debug): 32 | dashboard_thread = Thread(target=launch_dashboard, args=(host, port, debug)) 33 | 34 | dashboard_thread.daemon = True 35 | 36 | dashboard_thread.start() 37 | 38 | while True: 39 | time.sleep(1) 40 | if not dashboard_thread.is_alive(): 41 | sys.exit(1001) 42 | -------------------------------------------------------------------------------- /labgraph/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom useful functions and classes for ``alab_data``. 3 | """ 4 | 5 | from .config import get_config, make_config 6 | -------------------------------------------------------------------------------- /labgraph/utils/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import get_config, make_config 2 | -------------------------------------------------------------------------------- /labgraph/utils/config/config.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import toml 3 | import os 4 | from getpass import getpass 5 | from pydantic import BaseModel 6 | 7 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 8 | DEFAULT_CONFIG_PATH = os.path.join( 9 | os.path.expanduser("~"), ".labgraph", "labgraph_config.toml" 10 | ) 11 | 12 | 13 | class MongoDBConfigValidator(BaseModel): 14 | host: str 15 | port: int 16 | db_name: str 17 | username: Optional[str] 18 | password: Optional[str] 19 | 20 | 21 | class ConfigValidator(BaseModel): 22 | mongodb: dict 23 | 24 | 25 | def validate_config(config: dict): 26 | """Validates the config file. Raises an exception if the config file is invalid. 27 | 28 | Args: 29 | config (dict): The config file to validate. 30 | 31 | Raises: 32 | ValueError: The config file is invalid. 33 | """ 34 | try: 35 | ConfigValidator(**config) 36 | except Exception as e: 37 | raise ValueError( 38 | f"The config file is invalid. Please check the config file and try again. You can use `labgraph.utils.make_config()` to walk you through creating a valid config file. Error: {e}" 39 | ) 40 | 41 | 42 | def get_config(): 43 | config_path = os.getenv("LABGRAPH_CONFIG", DEFAULT_CONFIG_PATH) 44 | try: 45 | with open(config_path, "r", encoding="utf-8") as f: 46 | config = toml.load(f) 47 | except FileNotFoundError: 48 | raise ValueError( 49 | "Labgraph config file was not found. Please set your computer's environment variable 'LABGRAPH_CONFIG' to the path to the config file. If you need help with this, you can run `labgraph.utils.make_config()` to create a config file for you." 50 | ) 51 | 52 | validate_config(config) 53 | 54 | return config 55 | 56 | 57 | def make_config(): 58 | """Command line tool to help create a config file for Labgraph. The config file is a TOML file that contains the information needed to connect to a MongoDB instance hosting the Labgraph databa5. 59 | 60 | Raises: 61 | ValueError: Invalid file extension. Must be a .toml file. 62 | Exception: Unable to write config file to the specified path. 63 | """ 64 | config_path = os.getenv("LABGRAPH_CONFIG", DEFAULT_CONFIG_PATH) 65 | 66 | filepath = None 67 | warn_for_envvar = False 68 | if os.path.exists(config_path): 69 | print(f"A labgraph config file already exists at {config_path}.\n") 70 | response = input( 71 | "Do you want to:\n\t[u]pdate this config file\n\t[r]eplace this config file\n\t[e]xit\n\n[u/r/e]: " 72 | ) 73 | if response == "u": 74 | filepath = os.path.abspath(config_path) 75 | elif response == "r": 76 | os.remove(config_path) 77 | if filepath != DEFAULT_CONFIG_PATH: 78 | warn_for_envvar = True 79 | pass 80 | else: 81 | print("Nothing was changed.") 82 | return 83 | 84 | if filepath is None: 85 | warn_for_envvar = True 86 | filepath = input( 87 | f"Enter the path to the new config file. (Leave blank for default location: {DEFAULT_CONFIG_PATH}): " 88 | ) 89 | if filepath == "": 90 | warn_for_envvar = False 91 | filepath = DEFAULT_CONFIG_PATH 92 | filepath = os.path.abspath(filepath) 93 | if not os.path.basename(filepath).endswith(".toml"): 94 | raise ValueError( 95 | f"Config file must be a TOML file! The file should end with extension '.toml'. You provided {filepath}" 96 | ) 97 | 98 | print(f"We will create a config file for you at {filepath}.") 99 | if warn_for_envvar: 100 | print( 101 | f"\nWARNING: Do not forget to create/update an environment variable LABGRAPH_CONFIG to point to {filepath}!\n\nYou have chosen not to put your labgraph config in the default location, so Labgraph requires the environment variable to know where to find your config file. If this is confusing, please restart this function and leave the filepath blank to use the default location.\n" 102 | ) 103 | response = input("Do you want to continue? [y/n]: ") 104 | if response != "y": 105 | print("Nothing was changed.") 106 | return 107 | 108 | print( 109 | "Please enter the following information to point Labgraph to the correct MongoDB instance!\n" 110 | ) 111 | 112 | config_dict = {"mongodb": {}} 113 | 114 | config_dict["mongodb"]["host"] = ( 115 | input("Enter MongoDB host (leave blank for default = localhost): ") 116 | or "localhost" 117 | ) 118 | port = input("Enter MongoDB port (leave blank for default = 27017): ") or 27017 119 | config_dict["mongodb"]["port"] = int(port) 120 | 121 | config_dict["mongodb"]["db_name"] = ( 122 | input("Enter database name (leave blank for default = Labgraph): ") 123 | or "Labgraph" 124 | ) 125 | 126 | db_user = input("Enter username (leave blank if not required): ") 127 | if db_user != "": 128 | config_dict["mongodb"]["username"] = db_user 129 | config_dict["mongodb"]["password"] = getpass( 130 | "Enter password (typed characters hidden for privacy): " 131 | ) 132 | 133 | validate_config(config_dict) 134 | try: 135 | os.makedirs(os.path.dirname(filepath), exist_ok=True) 136 | with open(filepath, "w", encoding="utf-8") as f: 137 | toml.dump(config_dict, f) 138 | except Exception as e: 139 | raise Exception( 140 | "Labgraph was unable to write your config file. Error message: ", e 141 | ) 142 | 143 | print( 144 | f"\nConfig file successfully written to {filepath}. \nWARNING: If you aren't using the default location, please remember to set the environment variable LABGRAPH_CONFIG = {filepath}. \nWARNING: You should restart your Python kernel to ensure that Labgraph uses the new config file." 145 | ) 146 | -------------------------------------------------------------------------------- /labgraph/utils/data_objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | A convenient wrapper for MongoClient. We can get a database object by calling ``get_collection`` function. 3 | """ 4 | 5 | from typing import Optional 6 | 7 | import pymongo 8 | from pymongo import collection, database 9 | 10 | from .db_lock import MongoLock 11 | 12 | 13 | class _GetMongoCollection: 14 | client: Optional[pymongo.MongoClient] = None 15 | db: Optional[database.Database] = None 16 | db_lock: Optional[MongoLock] = None 17 | 18 | @classmethod 19 | def init(cls): 20 | from labgraph.utils import get_config 21 | 22 | db_config = get_config()["mongodb"] 23 | cls.client = pymongo.MongoClient( 24 | host=db_config.get("host", None), 25 | port=db_config.get("port", None), 26 | username=db_config.get("username", ""), 27 | password=db_config.get("password", ""), 28 | ) 29 | cls.db = cls.client[db_config.get("db_name")] # type: ignore # pylint: disable=unsubscriptable-object 30 | 31 | @classmethod 32 | def get_collection(cls, name: str) -> collection.Collection: 33 | """ 34 | Get collection by name 35 | """ 36 | if cls.client is None: 37 | cls.init() 38 | 39 | return cls.db[name] # type: ignore # pylint: disable=unsubscriptable-object 40 | 41 | @classmethod 42 | def get_lock(cls, name: str) -> MongoLock: 43 | if cls.db_lock is None: 44 | cls.db_lock = MongoLock(collection=cls.get_collection("_lock"), name=name) 45 | return cls.db_lock 46 | 47 | 48 | get_collection = _GetMongoCollection.get_collection 49 | get_lock = _GetMongoCollection.get_lock 50 | -------------------------------------------------------------------------------- /labgraph/utils/db_lock.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file defines the database lock class, which can block other processes to access the database. 3 | """ 4 | 5 | import time 6 | from contextlib import contextmanager 7 | from typing import Optional 8 | 9 | from pymongo.collection import Collection 10 | from pymongo.errors import DuplicateKeyError 11 | 12 | 13 | class MongoLockAcquireError(Exception): 14 | """ 15 | Raised when failing to acquire a lock 16 | """ 17 | 18 | 19 | class MongoLockReleaseError(Exception): 20 | """ 21 | Raised when failing to release a lock 22 | """ 23 | 24 | 25 | class MongoLock: 26 | """ 27 | Use a distributed lock to lock a collection or something else 28 | """ 29 | 30 | def __init__(self, name: str, collection: Collection): 31 | self._lock_collection = collection 32 | self._name = name 33 | self._locked: bool = self._lock_collection.find_one({"_id": name}) is not None 34 | 35 | @property 36 | def name(self) -> str: 37 | return self._name 38 | 39 | @contextmanager 40 | def __call__(self, timeout: Optional[float] = None): 41 | yield self.acquire(timeout=timeout) 42 | self.release() 43 | 44 | def acquire(self, timeout: Optional[float] = None): 45 | start_time = time.time() 46 | while timeout is None or time.time() - start_time <= timeout: 47 | try: 48 | self._lock_collection.insert_one({"_id": self._name}) 49 | except DuplicateKeyError: 50 | time.sleep(0.1) 51 | continue 52 | else: 53 | self._locked = True 54 | return 55 | raise MongoLockAcquireError("Acquire lock timeout") 56 | 57 | def release(self): 58 | result = self._lock_collection.delete_one({"_id": self._name}) 59 | if result.deleted_count != 1: 60 | raise MongoLockReleaseError( 61 | f"Fail to release a lock (name={self._name}). " 62 | f"Are you sure if the key is right?" 63 | ) 64 | self._locked = False 65 | -------------------------------------------------------------------------------- /labgraph/utils/dev.py: -------------------------------------------------------------------------------- 1 | def _drop_collections(): 2 | from labgraph.views import ( 3 | MaterialView, 4 | ActionView, 5 | MeasurementView, 6 | AnalysisView, 7 | ActorView, 8 | SampleView, 9 | ) 10 | 11 | for view in [ 12 | MaterialView(), 13 | ActionView(), 14 | MeasurementView(), 15 | AnalysisView(), 16 | ActorView(), 17 | SampleView(), 18 | ]: 19 | view._collection.drop() 20 | -------------------------------------------------------------------------------- /labgraph/utils/graph.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from typing import List, Dict, Tuple 3 | 4 | 5 | def get_subgraphs(graph: nx.DiGraph) -> List[nx.DiGraph]: 6 | """Return subgraphs of a DiGraph. A subgraph is a set of connected nodes that are not connected to any other nodes in the graph. 7 | 8 | Args: 9 | graph (nx.DiGraph): directed graph of an experiment 10 | 11 | Returns: 12 | List[nx.Digraph]: list of subgraphs of the directed graph. 13 | """ 14 | subgraphs = [ 15 | graph.subgraph(c) for c in nx.connected_components(graph.to_undirected()) 16 | ] 17 | return subgraphs 18 | 19 | 20 | def _walk_graph_for_positions(graph, positions=None, node=None, x0=0, y0=0, width=1): 21 | if node is None: 22 | node = list(nx.topological_sort(graph))[0] 23 | if positions is None: 24 | positions = {} 25 | positions[node] = (x0, y0) 26 | 27 | successors = list(graph.successors(node)) 28 | if len(successors) == 0: 29 | return 30 | 31 | if len(successors) == 1: 32 | dx = width 33 | x = [0] 34 | else: 35 | dx = width / (len(successors) - 1) 36 | x = [i * dx - width / 2 for i in range(len(successors))] 37 | 38 | for delta_x, successor in zip(x, successors): 39 | next_successors = list(graph.successors(successor)) 40 | if len(next_successors) != 0: 41 | width = dx * (len(next_successors) - 1) / len(next_successors) * 0.95 42 | _walk_graph_for_positions( 43 | graph, positions, successor, x0=x0 + delta_x, y0=y0 - 1, width=width 44 | ) 45 | 46 | return positions 47 | 48 | 49 | def hierarchical_layout(graph: nx.DiGraph) -> Dict[str, Tuple[float, float]]: 50 | """Create a hierarchical layout for a directed graph 51 | 52 | Args:s 53 | graph (nx.DiGraph): graph to layout 54 | 55 | Returns: 56 | dict: mapping of node ids to (x,y) coordinates 57 | """ 58 | subgraphs = get_subgraphs(graph) 59 | subgraph_positions = [] 60 | subgraph_bounds = [] 61 | for subgraph in get_subgraphs(graph): 62 | subgraph_positions.append(_walk_graph_for_positions(subgraph)) 63 | 64 | max_x = max([x for x, y in subgraph_positions[-1].values()]) 65 | min_x = min([x for x, y in subgraph_positions[-1].values()]) 66 | subgraph_bounds.append((min_x, max_x)) 67 | 68 | positions = {} 69 | reference_x = 0 70 | total_width = sum([max_x - min_x for min_x, max_x in subgraph_bounds]) 71 | TARGET_MARGIN = total_width * 0.05 72 | for subgraph, (min_x, max_x) in zip(subgraphs, subgraph_bounds): 73 | overlap = reference_x - min_x 74 | offset = TARGET_MARGIN + overlap 75 | for node, (x, y) in subgraph_positions.pop(0).items(): 76 | positions[node] = (x + offset, y) 77 | reference_x = max_x + offset 78 | 79 | return positions 80 | -------------------------------------------------------------------------------- /labgraph/utils/plot.py: -------------------------------------------------------------------------------- 1 | from typing import List, TYPE_CHECKING 2 | import matplotlib.pyplot as plt 3 | import networkx as nx 4 | from networkx.drawing.nx_agraph import graphviz_layout 5 | import warnings 6 | 7 | if TYPE_CHECKING: 8 | from labgraph.data.sample import Sample 9 | 10 | 11 | def plot_multiple_samples(samples: List["Sample"], ax: plt.axes = None): 12 | graph = nx.DiGraph() 13 | for sample in samples: 14 | graph = nx.compose(graph, sample.graph) 15 | plot_graph(graph, ax=ax) 16 | 17 | 18 | def plot_graph(graph: nx.DiGraph, with_labels: bool = True, ax: plt.axes = None): 19 | """Plots the sample graph. This is pretty chaotic with branched graphs, but can give a qualitative sense of the experimental procedure 20 | 21 | Args: 22 | with_labels (bool, optional): Whether to show the node names. Defaults to True. 23 | ax (matplotlib.pyplot.Axes, optional): Existing plot Axes to draw the graph onto. If None, a new plot figure+axes will be created. Defaults to None. 24 | """ 25 | if ax is None: 26 | fig, ax = plt.subplots() 27 | 28 | color_key = { 29 | nodetype: plt.cm.tab10(i) 30 | for i, nodetype in enumerate(["Material", "Action", "Analysis", "Measurement"]) 31 | } 32 | node_colors = [] 33 | node_labels = {} 34 | for node in graph.nodes: 35 | node_labels[node] = graph.nodes[node]["name"] 36 | color = color_key[graph.nodes[node]["type"]] 37 | if node_labels[node] == "": 38 | color = tuple( 39 | [*color[:3], 0.4] 40 | ) # low opacity for attached nodes that are not part of the sample 41 | node_colors.append(color) 42 | try: 43 | layout = graphviz_layout(graph, prog="dot") 44 | except: 45 | warnings.warn( 46 | "Could not use graphviz layout, falling back to default networkx layout. Ensure that graphviz and pygraphviz are installed to enable hierarchical graph layouts. This only affects graph visualization." 47 | ) 48 | layout = nx.spring_layout(graph) 49 | # layout = hierarchical_layout(self.graph) #TODO substitute graphviz to remove dependency. 50 | nx.draw( 51 | graph, 52 | with_labels=with_labels, 53 | node_color=node_colors, 54 | labels=node_labels, 55 | pos=layout, 56 | ax=ax, 57 | ) 58 | -------------------------------------------------------------------------------- /labgraph/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import BaseView 2 | from .sample import SampleView 3 | from .nodes import ( 4 | MaterialView, 5 | ActionView, 6 | MeasurementView, 7 | AnalysisView, 8 | ) 9 | from .actors import ActorView 10 | 11 | def get_view(node) -> BaseView: 12 | """Get the view corresponding to a given node type 13 | 14 | Args: 15 | node (Node): Node to get view for 16 | 17 | Returns: 18 | BaseNodeView: View for node 19 | """ 20 | return get_view_by_type(node.__class__.__name__) 21 | 22 | 23 | def get_view_by_type(node_type: str) -> BaseView: 24 | """Get the view corresponding to a given node type 25 | 26 | Args: 27 | type (str): Node/Actor/Sample type to get view for 28 | 29 | Returns: 30 | BaseNodeView: View for type 31 | """ 32 | VIEWS = { 33 | "Action": ActionView, 34 | "Analysis": AnalysisView, 35 | "Actor": ActorView, 36 | "Measurement": MeasurementView, 37 | "Material": MaterialView, 38 | "Sample": SampleView, 39 | } 40 | if node_type not in VIEWS: 41 | raise ValueError( 42 | f"Invalid node type: {node_type}. Must be one of {VIEWS.keys()}" 43 | ) 44 | return VIEWS[node_type]() 45 | -------------------------------------------------------------------------------- /labgraph/views/actors.py: -------------------------------------------------------------------------------- 1 | from labgraph.data.actors import Actor 2 | from labgraph.views.base import BaseActorView 3 | 4 | 5 | class ActorView(BaseActorView): 6 | def __init__(self): 7 | super().__init__("actors", Actor, allow_duplicate_names=False) 8 | -------------------------------------------------------------------------------- /labgraph/views/graph_integrity.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, TYPE_CHECKING 2 | 3 | from bson import ObjectId 4 | from labgraph.data.nodes import BaseNode, NodeList 5 | from labgraph import views 6 | 7 | if TYPE_CHECKING: 8 | from labgraph.data.sample import Sample 9 | 10 | 11 | def _get_affected_nodes( 12 | node: BaseNode, affected_nodes: Optional[Dict[str, List[str]]] = None 13 | ): 14 | """Recursively gets node ids that would be affected by a change to a given node. This assumes that all nodes downstream of a given node are dependent on it!""" 15 | VIEWS = { 16 | "Action": views.ActionView, 17 | "Analysis": views.AnalysisView, 18 | "Measurement": views.MeasurementView, 19 | "Material": views.MaterialView, 20 | } 21 | 22 | affected_nodes = affected_nodes or { 23 | "Action": [], 24 | "Analysis": [], 25 | "Measurement": [], 26 | "Material": [], 27 | } 28 | for i, downstream in enumerate(node.downstream): 29 | type_list = affected_nodes[downstream["node_type"]] 30 | if downstream["node_id"] in type_list: 31 | continue 32 | type_list.append(downstream["node_id"]) 33 | affected_nodes = _get_affected_nodes(node.downstream.get(i), affected_nodes) 34 | 35 | return affected_nodes 36 | 37 | 38 | def get_affected_nodes(node: BaseNode) -> NodeList: 39 | """Get all nodes affected by a change to a given node. This assumes that all nodes downstream of a given node are dependent on it! 40 | 41 | Args: 42 | node (Node): Node to check 43 | 44 | Returns: 45 | list: List of affected nodes 46 | """ 47 | affected_by_type = _get_affected_nodes(node) 48 | affected_nodes = NodeList() 49 | for node_type, node_ids in affected_by_type.items(): 50 | for node_id in node_ids: 51 | affected_nodes.append( 52 | { 53 | "node_type": node_type, 54 | "node_id": node_id, 55 | } 56 | ) 57 | return affected_nodes 58 | 59 | 60 | def get_affected_samples(node: BaseNode) -> List["Sample"]: 61 | """Get all samples affected by a change to a given node. This assumes that all nodes downstream of a given node are dependent on it! 62 | 63 | Args: 64 | node (Node): Node to check 65 | 66 | Returns: 67 | list: List of affected samples 68 | """ 69 | sampleview = views.SampleView() 70 | affected_nodes = get_affected_nodes(node) 71 | affected_nodes.append(node) # this node matters too! 72 | affected_samples = [] 73 | for node in affected_nodes.get(): 74 | for sample in sampleview.get_by_node(node): 75 | if sample not in affected_samples: 76 | affected_samples.append(sample) 77 | 78 | return affected_samples 79 | 80 | 81 | def _remove_references_to_node(node_type: str, node_id: ObjectId): 82 | """Removes all edges and sample references that point to the given node. 83 | This is used internally by node/sample deletion routines. Not intended for users, be careful with this -- it can render graphs invalid! 84 | 85 | Args: 86 | node_type (str): Type of node to be removed (Action, Analysis, Measurement, Material) 87 | node_id (ObjectId): ID of node to be removed 88 | """ 89 | from labgraph.views import ( 90 | ActionView, 91 | AnalysisView, 92 | MeasurementView, 93 | MaterialView, 94 | SampleView, 95 | ) 96 | 97 | for view in [ActionView(), AnalysisView(), MeasurementView(), MaterialView()]: 98 | view._collection.update_many( 99 | {"downstream": {"node_type": node_type, "node_id": node_id}}, 100 | {"$pull": {"downstream": {"node_type": node_type, "node_id": node_id}}}, 101 | ) 102 | view._collection.update_many( 103 | {"upstream": {"node_type": node_type, "node_id": node_id}}, 104 | {"$pull": {"upstream": {"node_type": node_type, "node_id": node_id}}}, 105 | ) 106 | 107 | SampleView()._collection.update_many( 108 | {f"nodes.{node_type}": node_id}, 109 | {"$pull": {f"nodes.{node_type}": node_id}}, 110 | ) 111 | -------------------------------------------------------------------------------- /labgraph/views/nodes.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from labgraph.data.nodes import ( 3 | Action, 4 | Analysis, 5 | Material, 6 | Measurement, 7 | ) 8 | from .base import BaseNodeView 9 | from .actors import Actor 10 | 11 | 12 | class MaterialView(BaseNodeView): 13 | def __init__(self): 14 | super().__init__("materials", Material) 15 | 16 | 17 | class ActionView(BaseNodeView): 18 | def __init__(self): 19 | super().__init__("actions", Action) 20 | 21 | def get_by_actor(self, actor: Actor) -> List[Action]: 22 | return self.filter({"actor_id": actor.id}) 23 | 24 | 25 | class MeasurementView(BaseNodeView): 26 | def __init__(self): 27 | super().__init__("measurements", Measurement) 28 | 29 | def get_by_actor(self, actor: Actor) -> List[Action]: 30 | return self.filter({"actor_id": actor.id}) 31 | 32 | 33 | class AnalysisView(BaseNodeView): 34 | def __init__(self): 35 | super().__init__("analyses", Analysis) 36 | 37 | def get_by_actor(self, actor: Actor) -> List[Action]: 38 | return self.filter({"actor_id": actor.id}) 39 | -------------------------------------------------------------------------------- /my-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /my-app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /my-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-sigma/core": "^3.4.1", 7 | "@tabler/icons": "1.115", 8 | "@tabler/icons-react": "^2.23.0", 9 | "@testing-library/jest-dom": "^5.14.1", 10 | "@testing-library/react": "^13.0.0", 11 | "@testing-library/user-event": "^13.2.1", 12 | "@types/jest": "^27.0.1", 13 | "@types/node": "^16.7.13", 14 | "@types/react": "^18.0.0", 15 | "@types/react-dom": "^18.0.0", 16 | "@types/react-scroll": "^1.8.7", 17 | "axios": "^1.4.0", 18 | "graphology": "^0.25.1", 19 | "graphology-layout-forceatlas2": "^0.10.1", 20 | "graphology-types": "^0.24.7", 21 | "mantine-datatable": "^2.7.1", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-scripts": "5.0.1", 25 | "react-scroll": "^1.8.9", 26 | "react-sigma-v2": "^1.3.0", 27 | "sass": "^1.63.6", 28 | "sigma": "^3.0.0-alpha3", 29 | "typescript": "^4.4.2", 30 | "web-vitals": "^2.1.0" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject" 37 | }, 38 | "eslintConfig": { 39 | "extends": [ 40 | "react-app", 41 | "react-app/jest" 42 | ] 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /my-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/my-app/public/favicon.ico -------------------------------------------------------------------------------- /my-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /my-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/my-app/public/logo192.png -------------------------------------------------------------------------------- /my-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekumar/labgraph/35cad08edfd8d3bf9c8260c524462f6be1a857f5/my-app/public/logo512.png -------------------------------------------------------------------------------- /my-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /my-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /my-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Content from './views/Content'; 2 | // import Header from './components/Header'; 3 | // import Footer from './components/Footer'; 4 | import { MantineProvider } from '@mantine/core'; 5 | import { TypographyStylesProvider } from '@mantine/core'; 6 | 7 | function App() { 8 | return ( 9 | 10 | 11 | {/*
*/} 12 | 13 | {/*