diff --git a/.gitignore b/.gitignore index bb1f3ee9..744b79c1 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,8 @@ ipython_config.py # OpenMDAO cases.sql n2.html +connections.html +openmdao_checks.out # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -156,3 +158,4 @@ envs/ api/ db/ downloads/ +raw.json diff --git a/README.md b/README.md index eadebf20..1d57a0c9 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,24 @@ Copy the URL where JupyterLab is running into your preferred browser, and you sh ## Widgets You can interact with the SysML v2 data using widgets, as illustrated below: -![Composed Widget](https://user-images.githubusercontent.com/1438114/113528145-bb494280-958d-11eb-8d9f-5b8f7d2b1dbe.gif) +![Composed Widget](https://user-images.githubusercontent.com/1438114/132993186-858063a7-1bb7-483b-b3be-0d61d3d27fca.gif) > If you can't see the animation clearly, click on it to see it in higher resolution. + +## Setup SysML Kernel + +If you want to experiment with the SysML v2 kernel, you can install it on the environment you want by running the following command: + +```bash +anaconda-project run sysml:kernel:install +``` + +## Setup SysML API Kernel + +You can setup and run a local SysML Pilot Implementation API server by running: + +```bash +anaconda-project run sysml:api:setup # to install the service +anaconda-project run sysml:api:start # to start the server +anaconda-project run sysml:api:stop # to stop the server +``` diff --git a/_scripts/__init__.py b/_scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/_scripts/_paths.py b/_scripts/_paths.py new file mode 100644 index 00000000..c1619be6 --- /dev/null +++ b/_scripts/_paths.py @@ -0,0 +1,24 @@ +import sys +from pathlib import Path +from shutil import which + +ROOT = (Path(__file__) / "../..").resolve() + +DOWNLOADS = ROOT / "downloads" + +ENV_ROOT = Path(sys.executable).parent + +DOT_LOCATION = which("dot") +if DOT_LOCATION: + DOT_LOCATION = Path(DOT_LOCATION) + +POSTGRES_USER = "postgres" +POSTGRES_PASSWORD = "mysecretpassword" +POSTGRES_DB = "sysml2" + +PSQL_DATA = ROOT / "db" +PSQL_LOGS = PSQL_DATA / "logs" +PSQL_PWFILE = ROOT / "_pwfile" + +API = ROOT / "api" +SBT_BIN = API / "sbt/bin" diff --git a/_scripts/_variables.py b/_scripts/_variables.py new file mode 100644 index 00000000..550332d2 --- /dev/null +++ b/_scripts/_variables.py @@ -0,0 +1,28 @@ +import platform +from os import environ as ENV_VARS + +OK = 0 +ERR = 1 + +# system +PLATFORM = platform.system() +WIN = PLATFORM == "Windows" +OSX = PLATFORM == "Darwin" +CI = ENV_VARS.get("JENKINS_HOME") + +# installer versions +SBT_VERSION = ENV_VARS.get("SBT_VERSION", "1.2.8") +SYSML2_API_RELEASE = ENV_VARS.get("SYSML2_API_RELEASE", "2021-06") +SYSML2_RELEASE = ENV_VARS.get("SYSML2_RELEASE", "2021-06.1") + +# API Configuration +API_PORT = ENV_VARS.get("API_PORT", 9000) +LOCAL_API_SERVER_URL = ENV_VARS.get("LOCAL_API_SERVER_URL", "http://localhost") +REMOTE_API_SERVER_URL = ENV_VARS.get("REMOTE_API_SERVER_URL", "http://sysml2.intercax.com") +SBT_GITHUB = "https://github.com/sbt/sbt" +SYSML_RELEASE_GITHUB = "https://github.com/Systems-Modeling/SysML-v2-Release" +SYSML_API_GITHUB = "https://github.com/Systems-Modeling/SysML-v2-API-Services" + +PSQL_DBNAME = ENV_VARS.get("PSQL_DBNAME", "sysml2") +PSQL_USERNAME = ENV_VARS.get("PSQL_USERNAME", "postgres") +PSQL_PASSWORD = ENV_VARS.get("PSQL_PASSWORD", "pUtY0uR$eCr3tP@$sW0rDh3R3") diff --git a/_scripts/api_server.py b/_scripts/api_server.py new file mode 100644 index 00000000..ba5c2447 --- /dev/null +++ b/_scripts/api_server.py @@ -0,0 +1,282 @@ +import re +import sys +import traceback +from argparse import ArgumentParser +from dataclasses import dataclass +from pathlib import Path +from zipfile import ZipFile + +from . import _paths as P +from . import _variables as V +from .utils import COLOR as C +from .utils import _check_output, _run, download_file + +API_ZIP_FILE = P.DOWNLOADS / "api_server.zip" +API_DOWNLOAD_URL = f"{V.SYSML_API_GITHUB}/archive/refs/tags/{V.SYSML2_API_RELEASE}.zip" + +SBT_ZIP_FILE = P.DOWNLOADS / "sbt.zip" +SBT_DOWNLOAD_URL = f"{V.SBT_GITHUB}/releases/download/v{V.SBT_VERSION}/sbt-{V.SBT_VERSION}.zip" + +# Executables +PG_CTL = "pg_ctl" +PSQL = "psql" +SBT = P.SBT_BIN / ("sbt" + (".bat" if V.WIN else "")) + +PARSER = ArgumentParser() + +PARSER.add_argument( + "--host", + default="127.0.0.1", + type=str, + help="Hostname on which to run PosgreSQL (default='127.0.0.1')", +) +PARSER.add_argument( + "--port", + default=5432, + type=int, + help="Port on which to run PosgreSQL (default=5432)", +) +PARSER.add_argument("--setup", action="store_true") +PARSER.add_argument("--silent", action="store_true") +PARSER.add_argument("--start", action="store_true") +PARSER.add_argument("--stop", action="store_true") +PARSER.add_argument("--tear-down", action="store_true") +PARSER.add_argument("--no-autostart", action="store_true") + + +# Commands: +# 1. Create User `postgres` +# 2. Create database 'sysml2' +# 3. + + +def setup_api(force=False, skip_clean=True): + """Setup the SysML v2 Pilot API and the Scala Build Tools (SBT)""" + if not P.API.exists() or not list(P.API.iterdir()): + if force or not API_ZIP_FILE.exists(): + if ( + download_file( + url=API_DOWNLOAD_URL, + filename=API_ZIP_FILE, + ) + != 0 + ): + print( + f"{C.FAIL} Failed to download the SysML v2 Pilot API!" + f" (url={API_DOWNLOAD_URL} {C.ENDC}" + ) + # unzip the contents and remove the zip file + try: + with ZipFile(API_ZIP_FILE) as zip_file: + zip_file.extractall(path=P.API) + if not skip_clean: + API_ZIP_FILE.unlink() + except: # pylint: disable=bare-except; # noqa: 722 + print(f"{C.FAIL} Could not unzip file '{API_ZIP_FILE}' to '{P.API}' {C.ENDC}") + print(f"{C.FAIL} {traceback.format_exc()} {C.ENDC}") + + if not P.SBT_BIN.exists(): + # download and extract file + if force or not SBT_ZIP_FILE.exists(): + if ( + download_file( + url=SBT_DOWNLOAD_URL, + filename=SBT_ZIP_FILE, + ) + != 0 + ): + print(f"{C.FAIL} Failed to download SBT! (url={SBT_DOWNLOAD_URL} {C.ENDC}") + # unzip the contents and remove the zip file + try: + with ZipFile(SBT_ZIP_FILE) as zip_file: + zip_file.extractall(path=P.API) + if not skip_clean: + SBT_ZIP_FILE.unlink() + except: # pylint: disable=bare-except; # noqa: 722 + print(f"{C.FAIL} Could not unzip file '{SBT_ZIP_FILE}' to '{P.API}' {C.ENDC}") + print(f"{C.FAIL} {traceback.format_exc()} {C.ENDC}") + + +def tear_down(): + try: + P.PSQL_DATA.unlink(missing_ok=True) + except: # pylint: disable=bare-except; # noqa: 722 + print(f"{C.FAIL} Failed to delete the PostgreSQL data folder {C.ENDC}") + print(f"{C.FAIL} {traceback.format_exc()} {C.ENDC}") + try: + P.API.unlink(missing_ok=True) + except: # pylint: disable=bare-except; # noqa: 722 + print(f"{C.FAIL} Failed to delete the API folder {C.ENDC}") + print(f"{C.FAIL} {traceback.format_exc()} {C.ENDC}") + + +@dataclass +class PostgreSQLProcess: # pylint: disable=too-many-instance-attributes + """ + A context manager for a psql process. + + IMPORTANT: This configuration is not intended to be used in production! + + """ + + pid_regex = re.compile(r"\(PID: ?(\d+)\)") + + host: str = "127.0.0.1" + port: int = 5432 + autostart: bool = True + silent: bool = False + + _pid: int = None + + _dbname: str = V.PSQL_DBNAME + _datafile: Path = P.PSQL_DATA.resolve().absolute() + _logsfile: Path = P.PSQL_LOGS.resolve().absolute() + _pwfile: Path = P.PSQL_PWFILE.resolve().absolute() + _username: str = V.PSQL_USERNAME + + def __post_init__(self): + if "0.0.0.0" in self.host: + print( + f"{C.WARNING} You are binding to PostgreSQL to 0.0.0.0, " + "this exposes serious risks! {C.ENDC}" + ) + + if not self._datafile.exists() or not list(self._datafile.iterdir()): + self.initialize_db() + + if not self._logsfile.exists(): + self._logsfile.parent.mkdir(parents=True, exist_ok=True) + + if self.autostart: + self.start_proc() + + def clear(self): + self.server("stop") + self._datafile.unlink() + + def write_pwfile(self): + """ + Write password to pwfile + + IMPORTANT: This is NOT secure!!! It should NOT be used in production! + """ + if self._pwfile.exists(): + self._pwfile.unlink() + self._pwfile.write_text(f"{V.PSQL_PASSWORD}\n") + + def initialize_db(self): + P.PSQL_DATA.mkdir(parents=True, exist_ok=True) + + # Initialize the database + silent_args = ["--silent"] if self.silent else [] + result_code = _run( + [ + PG_CTL, + "initdb", + f"--pgdata={self._datafile}", + f"--pwfile={self._pwfile}", + f"--username={self._username}", + ] + + silent_args, + cwd=P.ROOT, + wait=True, + ) + if result_code != 0: + print(f"{C.FAIL} Database initialization (initdb) returned a non-zero value {C.ENDC}") + + def create_db(self): + # Create the sysml database + is_running = self.is_running() + if not is_running: + self.server("start") + + result_code = _run( + [ + "createdb", + f"--owner={self._username}", + f"--host={self.host}", + f"--port={self.port}", + "--no-password", + self._dbname, + ], + cwd=self._datafile, + wait=True, + ) + + if result_code != 0: + print(f"{C.FAIL} Database creation (createdb) returned a non-zero value {C.ENDC}") + if not is_running: + self.server("stop") + + def server(self, command: str, wait=False): + func = _run if wait else _check_output + print(f"{C.OKBLUE} running '{command}' server command {C.ENDC}") + + os_dep_args = ["-U", V.PSQL_USERNAME] if V.WIN else [] + return func( + [ + PG_CTL, + f"--pgdata={self._datafile}", + f"--log={self._logsfile}", + command, + ] + + os_dep_args, + cwd=P.ROOT, + wait=wait, + ) + + @property + def server_status(self): + return self.server("status", wait=True) # .decode() + + def start_proc(self): + if self.is_running(): + print(f"{C.WARNING} PostgreSQL is already running {C.ENDC}") + return + + self.server("start") + + if not self.is_running(): + print(f"{C.FAIL} Could not start PostgreSQL {C.ENDC}") + else: + print(f"{C.OKGREEN} Serving PostgreSQL on {self.host}:{self.port} {C.ENDC}") + + def is_running(self): + pids = self.pid_regex.findall(self.server_status) + self._pid = pid = int(pids[0]) if pids else None + return pid + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + print(f"{C.OKBLUE} Shutting down psql on {self.host}:{self.port} {C.ENDC}") + self.server("stop") + print(f"{C.OKGREEN} Finished shutting down psql on {self.host}:{self.port} {C.ENDC}") + + +def setup(): + pass + + +if __name__ == "__main__": + args, extra_args = PARSER.parse_known_args(sys.argv[1:]) + + result_codes = [] + if args.tear_down: + result_codes.append(tear_down()) + + if args.setup: + result_codes.append(setup()) + + with PostgreSQLProcess( + host=args.host, + port=args.port, + autostart=not args.no_autostart, + ) as psql_proc: + if args.start: + result_codes.append( + # launch(env=env, port=args.mbee_port) + ) + + sys.exit(max(result_codes)) diff --git a/_scripts/kernel.py b/_scripts/kernel.py new file mode 100644 index 00000000..8c6f67a3 --- /dev/null +++ b/_scripts/kernel.py @@ -0,0 +1,143 @@ +import json +import sys +import traceback +from argparse import ArgumentParser +from zipfile import ZipFile + +from . import _paths as P +from . import _variables as V +from .utils import COLOR as C +from .utils import _check_output, _run, download_file + +SYSML2_RELEASE_FOLDER = P.DOWNLOADS / f"SysML-v2-Release-{V.SYSML2_RELEASE}" + +SYSML2_RELEASE_URL = f"{V.SYSML_RELEASE_GITHUB}/archive/refs/tags/{V.SYSML2_RELEASE}.zip" +SYSML2_RELEASE_ZIPFILE = P.DOWNLOADS / "sysml2_release.zip" +SYSML2_JUPYTER_FOLDER = SYSML2_RELEASE_FOLDER / "install/jupyter" +SYSML2_KERNEL_INSTALLER = SYSML2_JUPYTER_FOLDER / "jupyter-sysml-kernel/install.py" +SYSML2_LABEXTENSION = SYSML2_JUPYTER_FOLDER / "jupyterlab-sysml/package" + +PARSER = ArgumentParser() + +PARSER.add_argument( + "--force-download", + action="store_true", + help="Force downloading the release", +) +PARSER.add_argument( + "--skip-clean", + action="store_true", + help="Skip cleaning up downloaded files (e.g., .zip files)", +) +PARSER.add_argument( + "--publish-to", + default=V.REMOTE_API_SERVER_URL, + type=str, + help=f"IP address to publish SysML models to (default='{V.REMOTE_API_SERVER_URL}')", +) + + +def _get_sysml_release(force: bool = False, skip_clean: bool = False) -> list: + exceptions = [] + # download and extract file + if force or not SYSML2_RELEASE_ZIPFILE.exists(): + if ( + download_file( + url=SYSML2_RELEASE_URL, + filename=SYSML2_RELEASE_ZIPFILE, + ) + != 0 + ): + exceptions += [f"Failed to download file: {SYSML2_RELEASE_URL}"] + # unzip the contents and remove the zip file + try: + with ZipFile(SYSML2_RELEASE_ZIPFILE) as zip_file: + zip_file.extractall(path=P.DOWNLOADS) + if not skip_clean: + SYSML2_KERNEL_INSTALLER.unlink() + except: # pylint: disable=bare-except; # noqa: 722 + exceptions += [ + f"Could not unzip file '{SYSML2_RELEASE_ZIPFILE}' to '{P.DOWNLOADS}'", + traceback.format_exc(), + ] + return exceptions + + +def install( + force_download: bool = False, + publish_to: str = V.REMOTE_API_SERVER_URL, + skip_clean: bool = True, +) -> int: + """Install SysML v2 Kernel for JupyterLab.""" + exceptions = [] + if not P.DOT_LOCATION: + print(f"{C.FAIL} Need to install graphviz in '{P.ENV_ROOT.name}' environment! {C.ENDC}") + return 1 + + if force_download or not SYSML2_KERNEL_INSTALLER.exists(): + exceptions += _get_sysml_release( + force=force_download, + skip_clean=skip_clean, + ) + + # remove old kernel + try: + output = _check_output(["jupyter", "kernelspec", "list", "--json"]) + kernel_specs = json.loads(output.decode())["kernelspecs"] + found_old_kernel = "sysml" in kernel_specs + except: # pylint: disable=bare-except; # noqa: 722 + found_old_kernel = True + exceptions += [f"Failed to get kernel specifications: {traceback.format_exc()}"] + + try: + if found_old_kernel: + if _run(["jupyter", "kernelspec", "remove", "sysml", "-f"], cwd=P.ROOT) != 0: + exceptions += ["Failed to remove sysml kernel"] + except: # pylint: disable=bare-except; # noqa: 722 + exceptions += [f"Failed to remove sysml kernel: {traceback.format_exc()}"] + + # install sysmlv2 kernel + if ":" not in publish_to: + publish_to += f":{V.API_PORT}" + + kernel_install_commands = [ + "python", + SYSML2_KERNEL_INSTALLER.resolve(), + "--sys-prefix", + f"--api-base-path={publish_to}", + f"--graphviz-path={P.DOT_LOCATION.resolve().as_posix()}", + ] + if _run(kernel_install_commands, cwd=P.ROOT) != 0: + exceptions += [ + f"Failed to install kernel: {SYSML2_RELEASE_URL}", + ] + + if _run(["jupyter", "labextension", "uninstall", "jupyterlab-sysml"], cwd=P.ROOT) != 0: + exceptions += ["Failed to uninstall the JupyterLab SysML extension"] + + if ( + _run(["jupyter", "labextension", "install", SYSML2_LABEXTENSION.as_posix()], cwd=P.ROOT) + != 0 + ): + exceptions += ["Failed to install the JupyterLab SysML extension"] + + if exceptions: + print(f"{C.FAIL} Error installing SysML 2 kernel! {C.ENDC}") + for msg in exceptions: + print(f"{C.FAIL} {msg} {C.ENDC}") + else: + print(f"{C.OKBLUE} Installed the SysML v2 ({V.SYSML2_RELEASE}) JupyterLab Kernel {C.ENDC}") + + return 1 if exceptions else 0 + + +if __name__ == "__main__": + args, extra_args = PARSER.parse_known_args(sys.argv[1:]) + + sys.exit( + install( + force_download=args.force_download, + publish_to=args.publish_to, + skip_clean=args.skip_clean, + ) + ) diff --git a/_scripts/utils.py b/_scripts/utils.py new file mode 100644 index 00000000..a78fecd3 --- /dev/null +++ b/_scripts/utils.py @@ -0,0 +1,126 @@ +import subprocess +from pathlib import Path +from typing import List, Tuple, Union + +import requests + +from . import _paths as P +from . import _variables as V + +HAS_COLOR = True + +if V.WIN: + HAS_COLOR = False + try: + import colorama + + colorama.init() + HAS_COLOR = True + except ImportError: + print( + f"Please install colorama in `{P.ENV_ROOT.name}` if you'd like pretty colors", + flush=True, + ) + + +class COLOR: + """Terminal colors. Always print ENDC when done :P + + Falls back to ASCII art in absence of colors (windows, no colorama) + """ + + HEADER = "\033[95m" if HAS_COLOR else "=== " + OKBLUE = "\033[94m" if HAS_COLOR else "+++ " + OKGREEN = "\033[92m" if HAS_COLOR else "*** " + WARNING = "\033[93m" if HAS_COLOR else "!!! " + FAIL = "\033[91m" if HAS_COLOR else "XXX " + ENDC = "\033[0m" if HAS_COLOR else "" + BOLD = "\033[1m" if HAS_COLOR else "+++ " + UNDERLINE = "\033[4m" if HAS_COLOR else "___ " + + +def download_file( + url: str, + filename: Union[Path, str] = None, + allow_redirects: bool = True, + overwrite: bool = True, + stream: bool = True, +) -> int: + """Download a file from a URL.""" + + if not filename: + filename = url.split("?")[0].split("/")[-1] + if isinstance(filename, str): + if Path(filename).name == filename: + filename = P.DOWNLOADS / filename + else: + filename = Path(filename) + filename = filename.resolve() + if filename.exists(): + if overwrite: + print(f"{COLOR.WARNING} Will overwrite '{filename}' {COLOR.ENDC}") + else: + raise ValueError("{filename} already exists, and overwrite is not allowed!") + + try: + response = requests.get(url, allow_redirects=allow_redirects, stream=stream) + if response.status_code == 200: + if not filename.parent.exists(): + filename.parent.mkdir(parents=True) + if filename.exists(): + filename.unlink() + if stream: + with filename.open("w") as file_pointer: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + file_pointer.write(chunk) + else: + filename.write_bytes(response.content) + print(f"{COLOR.OKGREEN} Downloaded {filename} from {url} {COLOR.ENDC}") + return 0 + finally: + pass + print(f"{COLOR.WARNING} Failed to download: {url} {COLOR.ENDC}") + return 1 + + +def _run(args: Union[List[str], Tuple[str]], *, wait: bool = True, shorten_paths=False, **kwargs) -> int: + blue, endc = COLOR.OKBLUE, COLOR.ENDC + cwd = kwargs.get("cwd", None) + if cwd: + kwargs["cwd"] = str(cwd) + location = f" in {cwd}" + else: + location = "" + + def format_msg(msg: str) -> str: + msg = f"{blue}\n==={msg}\n===\n{endc}" + if shorten_paths: + msg = msg.replace(str(P.ROOT), ".") + return msg + + str_args = " ".join(map(str, args)) + if kwargs.get("shell"): + msg = format_msg(f"{location}\n{str_args}") + else: + msg = format_msg(f"\n{str_args}{location}") + print(msg, flush=True) + + proc = subprocess.Popen(str_args, **kwargs) + + if not wait: + return proc + + result_code = 1 + try: + result_code = proc.wait() + except KeyboardInterrupt: + proc.terminate() + result_code = proc.wait() + + return result_code + + +def _check_output(args, **kwargs): + """wrapper for subprocess.check_output that handles non-string args""" + return subprocess.check_output([*map(str, args)], **kwargs) diff --git a/anaconda-project-lock.yml b/anaconda-project-lock.yml index 6e805912..be6bde2d 100644 --- a/anaconda-project-lock.yml +++ b/anaconda-project-lock.yml @@ -15,15 +15,20 @@ locking_enabled: true # A key goes in here for each env spec. # env_specs: + _java: + locked: false + _sysml_kernel: + locked: false user: locked: true - env_spec_hash: 5ea25b12e7583f0ce70c0e3017c80e57b255733f + env_spec_hash: 6bf68ef2118da28e12e3b3e340a76a392581f88c platforms: - linux-64 - osx-64 - win-64 packages: all: + - appdirs=1.4.4=pyh9f0ad1d_0 - async-lru=1.0.2=py_0 - async_generator=1.10=py_0 - attrs=21.2.0=pyhd8ed1ab_0 @@ -34,11 +39,19 @@ env_specs: - bleach=4.0.0=pyhd8ed1ab_0 - cachetools=4.2.2=pyhd8ed1ab_0 - charset-normalizer=2.0.0=pyhd8ed1ab_0 + - colorama=0.4.4=pyh9f0ad1d_0 + - commonmark=0.9.1=py_0 - cycler=0.10.0=py_2 - dataclasses=0.8=pyhc8e2a94_3 - decorator=4.4.2=py_0 - defusedxml=0.7.1=pyhd8ed1ab_0 - - entrypoints=0.3=pyhd8ed1ab_1003 + - euporie=0.1.16=pyhd8ed1ab_0 + - font-ttf-dejavu-sans-mono=2.37=hab24e00_0 + - font-ttf-inconsolata=3.000=h77eed37_0 + - font-ttf-source-code-pro=2.038=h77eed37_0 + - font-ttf-ubuntu=0.83=hab24e00_0 + - fonts-conda-ecosystem=1=0 + - fonts-conda-forge=1=0 - frozendict=2.0.3=pyhd8ed1ab_0 - html5lib=1.1=pyh9f0ad1d_0 - idna=3.1=pyhd3deb0d_0 @@ -71,8 +84,6 @@ env_specs: - packaging=21.0=pyhd8ed1ab_0 - pandocfilters=1.4.2=py_1 - parso=0.8.2=pyhd8ed1ab_0 - - pickleshare=0.7.5=py_1003 - - pip=21.2.4=pyhd8ed1ab_0 - prometheus_client=0.11.0=pyhd8ed1ab_0 - prompt-toolkit=3.0.19=pyha770c72_0 - pycparser=2.20=pyh9f0ad1d_2 @@ -81,6 +92,7 @@ env_specs: - pyld=2.0.3=pyh9f0ad1d_0 - pyopenssl=20.0.1=pyhd8ed1ab_0 - pyparsing=2.4.7=pyh9f0ad1d_0 + - pyperclip=1.8.2=pyhd8ed1ab_2 - python-dateutil=2.8.2=pyhd8ed1ab_0 - python_abi=3.9=2_cp39 - pytz=2021.1=pyhd8ed1ab_0 @@ -97,13 +109,14 @@ env_specs: - urllib3=1.26.6=pyhd8ed1ab_0 - wcwidth=0.2.5=pyh9f0ad1d_2 - webencodings=0.5.1=py_1 - - wheel=0.37.0=pyhd8ed1ab_1 - wxyz_core=0.5.1=pyhd8ed1ab_0 - wxyz_html=0.5.1=pyhd8ed1ab_0 - wxyz_lab=0.5.1=pyhd8ed1ab_0 - zipp=3.5.0=pyhd8ed1ab_0 unix: + - entrypoints=0.3=pyhd8ed1ab_1003 - pexpect=4.8.0=pyh9f0ad1d_2 + - pickleshare=0.7.5=py_1003 - ptyprocess=0.7.0=pyhd3deb0d_0 linux-64: - _libgcc_mutex=0.1=conda_forge @@ -111,8 +124,10 @@ env_specs: - alsa-lib=1.2.3=h516909a_0 - anyio=3.3.0=py39hf3d152e_0 - argon2-cffi=20.1.0=py39h3811e60_2 + - atk-1.0=2.36.0=h3371d22_4 - brotlipy=0.7.0=py39h3811e60_1001 - ca-certificates=2021.5.30=ha878542_0 + - cairo=1.16.0=h6cf1ce9_1008 - certifi=2021.5.30=py39hf3d152e_0 - cffi=1.14.6=py39he32792d_0 - chardet=4.0.0=py39hf3d152e_1 @@ -122,11 +137,20 @@ env_specs: - expat=2.4.1=h9c3ff4c_0 - fontconfig=2.13.1=hba837de_1005 - freetype=2.10.4=h0708190_1 + - fribidi=1.0.10=h36c2ea0_0 + - future=0.18.2=py39hf3d152e_3 + - gdk-pixbuf=2.42.6=h04a7f16_0 - gettext=0.19.8.1=h0b5b191_1005 + - giflib=5.2.1=h36c2ea0_2 - glib-tools=2.68.3=h9c3ff4c_0 - glib=2.68.3=h9c3ff4c_0 + - graphite2=1.3.13=h58526e2_1001 + - graphviz=2.48.0=h85b4f2f_0 - gst-plugins-base=1.18.4=hf529b03_2 - gstreamer=1.18.4=h76c114f_2 + - gtk2=2.24.33=h539f30e_1 + - gts=0.7.6=h64030ff_2 + - harfbuzz=2.8.2=h83ec7ef_0 - icu=68.1=h58526e2_0 - importlib-metadata=4.6.4=py39hf3d152e_0 - ipykernel=6.1.0=py39hef51801_0 @@ -149,6 +173,7 @@ env_specs: - libevent=2.1.10=hcdb4288_3 - libffi=3.3=h58526e2_2 - libgcc-ng=11.1.0=hc902ee8_8 + - libgd=2.3.2=h78a0170_0 - libgfortran-ng=11.1.0=h69a702a_8 - libgfortran5=11.1.0=h6c583b3_8 - libglib=2.68.3=h3e27bee_0 @@ -161,12 +186,16 @@ env_specs: - libopus=1.3.1=h7f98852_1 - libpng=1.6.37=h21135ba_2 - libpq=13.3=hd57d9b9_0 + - librsvg=2.50.7=hc3c00ef_0 - libsodium=1.0.18=h36c2ea0_1 - libstdcxx-ng=11.1.0=h56837e0_8 - libtiff=4.3.0=hf544144_1 + - libtool=2.4.6=h58526e2_1007 - libuuid=2.32.1=h7f98852_1000 + - libuv=1.42.0=h7f98852_0 - libvorbis=1.3.7=h9c3ff4c_0 - libwebp-base=1.2.0=h7f98852_2 + - libwebp=1.2.0=h3452ae3_0 - libxcb=1.13=h7f98852_1003 - libxkbcommon=1.0.3=he3ba5ed_0 - libxml2=2.9.12=h72842e0_0 @@ -181,15 +210,19 @@ env_specs: - mysql-libs=8.0.25=hfa10184_2 - nbconvert=6.1.0=py39hf3d152e_0 - ncurses=6.2=h58526e2_4 + - nodejs=14.17.4=h92b4a50_0 - nspr=4.30=h9c3ff4c_0 - nss=3.69=hb5efdd6_0 - numpy=1.21.1=py39hdbf815f_0 + - openjdk=11.0.9.1=h5cc2fde_1 - openjpeg=2.4.0=hb52868f_1 - openssl=1.1.1k=h7f98852_0 - pandas=1.3.1=py39hde0f152_0 - pandoc=2.14.1=h7f98852_0 + - pango=1.48.7=hb8ff022_0 - pcre=8.45=h9c3ff4c_0 - pillow=8.3.1=py39ha612740_0 + - pixman=0.40.0=h36c2ea0_0 - pthread-stubs=0.4=h36c2ea0_1001 - pydantic=1.8.2=py39h3811e60_0 - pyqt-impl=5.12.3=py39h0fcd23e_7 @@ -205,6 +238,7 @@ env_specs: - rdflib-jsonld=0.5.0=py39hf3d152e_2 - rdflib=6.0.0=py39hf3d152e_0 - readline=8.1=h46c0cb4_0 + - rich=10.7.0=py39hf3d152e_0 - ruamel.yaml.clib=0.2.2=py39h3811e60_2 - ruamel.yaml=0.17.10=py39h3811e60_0 - scipy=1.7.1=py39hee8e79c_0 @@ -217,8 +251,23 @@ env_specs: - tornado=6.1=py39h3811e60_1 - websocket-client=0.57.0=py39hf3d152e_4 - widgetsnbextension=3.5.1=py39hf3d152e_4 + - xorg-fixesproto=5.0=h7f98852_1002 + - xorg-inputproto=2.3.2=h7f98852_1002 + - xorg-kbproto=1.0.7=h7f98852_1002 + - xorg-libice=1.0.10=h7f98852_0 + - xorg-libsm=1.2.3=hd9c2040_1000 + - xorg-libx11=1.7.2=h7f98852_0 - xorg-libxau=1.0.9=h7f98852_0 - xorg-libxdmcp=1.1.3=h7f98852_0 + - xorg-libxext=1.3.4=h7f98852_1 + - xorg-libxfixes=5.0.3=h7f98852_1004 + - xorg-libxi=1.7.10=h7f98852_0 + - xorg-libxrender=0.9.10=h7f98852_1003 + - xorg-libxtst=1.2.3=h7f98852_1002 + - xorg-recordproto=1.14.2=h7f98852_1002 + - xorg-renderproto=0.11.1=h7f98852_1002 + - xorg-xextproto=7.3.0=h7f98852_1002 + - xorg-xproto=7.0.31=h7f98852_1007 - xz=5.2.5=h516909a_1 - zeromq=4.3.4=h9c3ff4c_0 - zlib=1.2.11=h516909a_1010 @@ -227,14 +276,28 @@ env_specs: - anyio=3.3.0=py39h6e9494a_0 - appnope=0.1.2=py39h6e9494a_1 - argon2-cffi=20.1.0=py39h89e85a6_2 + - atk-1.0=2.36.0=he69c4ee_4 - brotlipy=0.7.0=py39h89e85a6_1001 - ca-certificates=2021.5.30=h033912b_0 + - cairo=1.16.0=he43a7df_1008 - certifi=2021.5.30=py39h6e9494a_0 - cffi=1.14.6=py39hb71fe58_0 - chardet=4.0.0=py39h6e9494a_1 - cryptography=3.4.7=py39ha2c9959_0 - debugpy=1.4.1=py39h9fcab8e_0 + - expat=2.4.1=he49afe7_0 + - fontconfig=2.13.1=h10f422b_1005 - freetype=2.10.4=h4cff582_1 + - fribidi=1.0.10=hbcb3906_0 + - future=0.18.2=py39h6e9494a_3 + - gdk-pixbuf=2.42.6=h2e6141f_0 + - gettext=0.19.8.1=h7937167_1005 + - giflib=5.2.1=hbcb3906_2 + - graphite2=1.3.13=h2e338ed_1001 + - graphviz=2.48.0=h77de9ca_0 + - gtk2=2.24.33=h675d97a_1 + - gts=0.7.6=hccb3bdf_2 + - harfbuzz=2.8.2=h159f659_0 - icu=68.1=h74dc148_0 - importlib-metadata=4.6.4=py39h6e9494a_0 - ipykernel=6.1.0=py39h71a6800_0 @@ -252,15 +315,21 @@ env_specs: - libcxx=12.0.1=habf9029_0 - libdeflate=1.7=h35c211d_5 - libffi=3.3=h046ec9c_2 + - libgd=2.3.2=h4e7a7ea_0 - libgfortran5=9.3.0=h6c81a4c_23 - libgfortran=5.0.0=9_3_0_h6c81a4c_23 + - libglib=2.68.3=hd556434_0 - libiconv=1.16=haf1e3a3_0 - liblapack=3.9.0=11_osx64_openblas - libopenblas=0.3.17=openmp_h3351f45_1 - libpng=1.6.37=h7cec526_2 + - librsvg=2.50.7=hd2a7919_0 - libsodium=1.0.18=hbcb3906_1 - libtiff=4.3.0=h1167814_1 + - libtool=2.4.6=h2e338ed_1007 + - libuv=1.42.0=h0d85af4_0 - libwebp-base=1.2.0=h0d85af4_2 + - libwebp=1.2.0=h1648767_0 - libxml2=2.9.12=h93ec3fd_0 - libxslt=1.1.33=h5739fc3_2 - llvm-openmp=12.0.1=hda6cdc1_1 @@ -272,12 +341,17 @@ env_specs: - mistune=0.8.4=py39h89e85a6_1004 - nbconvert=6.1.0=py39h6e9494a_0 - ncurses=6.2=h2e338ed_4 + - nodejs=14.17.4=hb529b34_0 - numpy=1.21.1=py39h7eed0ac_0 + - openjdk=11.0.9.1=hcf210ce_1 - openjpeg=2.4.0=h6e7aa92_1 - openssl=1.1.1k=h0d85af4_0 - pandas=1.3.1=py39h4d6be9b_0 - pandoc=2.14.1=h0d85af4_0 + - pango=1.48.7=ha05cd14_0 + - pcre=8.45=he49afe7_0 - pillow=8.3.1=py39he9bb72f_0 + - pixman=0.40.0=hbcb3906_0 - pydantic=1.8.2=py39h89e85a6_0 - pyrsistent=0.17.3=py39h89e85a6_2 - pysocks=1.7.1=py39h6e9494a_3 @@ -286,6 +360,7 @@ env_specs: - rdflib-jsonld=0.5.0=py39h6e9494a_2 - rdflib=6.0.0=py39h6e9494a_0 - readline=8.1=h05e3726_0 + - rich=10.7.0=py39h6e9494a_0 - ruamel.yaml.clib=0.2.2=py39hcbf5805_2 - ruamel.yaml=0.17.10=py39h89e85a6_0 - scipy=1.7.1=py39h056f1c0_0 @@ -307,13 +382,24 @@ env_specs: - argon2-cffi=20.1.0=py39hb82d6ee_2 - brotlipy=0.7.0=py39hb82d6ee_1001 - ca-certificates=2021.5.30=h5b45459_0 + - cairo=1.16.0=hb19e0ff_1008 - certifi=2021.5.30=py39hcbf5309_0 - cffi=1.14.6=py39h0878f49_0 - chardet=4.0.0=py39hcbf5309_1 - - colorama=0.4.4=pyh9f0ad1d_0 - cryptography=3.4.7=py39hd8d06c1_0 - debugpy=1.4.1=py39h415ef7b_0 + - entrypoints=0.3=py39hde42818_1002 + - expat=2.4.1=h39d44d4_0 + - fontconfig=2.13.1=h1989441_1005 - freetype=2.10.4=h546665d_1 + - fribidi=1.0.10=h8d14728_0 + - future=0.18.2=py39hcbf5309_3 + - getopt-win32=0.1=h8ffe710_0 + - gettext=0.19.8.1=h1a89ca6_1005 + - graphite2=1.3.13=1000 + - graphviz=2.48.0=hefbd956_0 + - gts=0.7.6=h7c369d9_2 + - harfbuzz=2.8.2=hc601d6f_0 - icu=68.1=h0e60522_0 - importlib-metadata=4.6.4=py39hcbf5309_0 - intel-openmp=2021.3.0=h57928b3_3372 @@ -331,11 +417,17 @@ env_specs: - libcblas=3.9.0=11_win64_mkl - libclang=11.1.0=default_h5c34c98_1 - libdeflate=1.7=h8ffe710_5 + - libffi=3.3=h0e60522_2 + - libgd=2.3.2=h138e682_0 + - libglib=2.68.3=h1e62bf3_0 - libiconv=1.16=he774522_0 - liblapack=3.9.0=11_win64_mkl - libpng=1.6.37=h1d00b33_2 - libsodium=1.0.18=h8d14728_1 - libtiff=4.3.0=h0c97f57_1 + - libwebp-base=1.2.0=h8ffe710_2 + - libwebp=1.2.0=h57928b3_0 + - libxcb=1.13=hcd874cb_1003 - libxml2=2.9.12=hf5bbc77_0 - libxslt=1.1.33=h65864e5_2 - lxml=4.6.3=py39h4fd7cdf_0 @@ -352,12 +444,19 @@ env_specs: - mkl=2021.3.0=hb70f87d_564 - msys2-conda-epoch=20160418=1 - nbconvert=6.1.0=py39hcbf5309_0 + - nodejs=14.17.4=h57928b3_0 - numpy=1.21.1=py39h6635163_0 + - openjdk=11.0.1=c_compilervs2015_1016 - openjpeg=2.4.0=hb211442_1 - openssl=1.1.1k=h8ffe710_0 - pandas=1.3.1=py39h2e25243_0 - pandoc=2.14.1=h8ffe710_0 + - pango=1.48.7=hd84fcdd_0 + - pcre=8.45=h0e60522_0 + - pickleshare=0.7.5=py39hde42818_1002 - pillow=8.3.1=py39h916092e_0 + - pixman=0.40.0=h8ffe710_0 + - pthread-stubs=0.4=hcd874cb_1001 - pydantic=1.8.2=py39hb82d6ee_0 - pyqt-impl=5.12.3=py39h415ef7b_7 - pyqt5-sip=4.19.18=py39h415ef7b_7 @@ -373,6 +472,7 @@ env_specs: - qt=5.12.9=h5909a2a_4 - rdflib-jsonld=0.5.0=py39hcbf5309_2 - rdflib=6.0.0=py39hcbf5309_0 + - rich=10.7.0=py39hcbf5309_0 - ruamel.yaml.clib=0.2.2=py39hb82d6ee_2 - ruamel.yaml=0.17.10=py39hb82d6ee_0 - scipy=1.7.1=py39hc0c34ad_0 @@ -392,13 +492,24 @@ env_specs: - win_inet_pton=1.1.0=py39hcbf5309_2 - wincertstore=0.2=py39hcbf5309_1006 - winpty=0.4.3=4 + - xorg-kbproto=1.0.7=hcd874cb_1002 + - xorg-libice=1.0.10=hcd874cb_0 + - xorg-libsm=1.2.3=hcd874cb_1000 + - xorg-libx11=1.7.2=hcd874cb_0 + - xorg-libxau=1.0.9=hcd874cb_0 + - xorg-libxdmcp=1.1.3=hcd874cb_0 + - xorg-libxext=1.3.4=hcd874cb_1 + - xorg-libxpm=3.5.13=hcd874cb_0 + - xorg-libxt=1.2.1=hcd874cb_2 + - xorg-xextproto=7.3.0=hcd874cb_1002 + - xorg-xproto=7.0.31=hcd874cb_1007 - xz=5.2.5=h62dcd97_1 - zeromq=4.3.4=h0e60522_0 - zlib=1.2.11=h62dcd97_1010 - zstd=1.5.0=h6255e5f_0 developer: locked: true - env_spec_hash: 83cefb5a81b3b887de5f16820b30b28c9208891c + env_spec_hash: 2882ffa56e94b269788f8aea9995d6d89415b361 platforms: - linux-64 - osx-64 @@ -418,13 +529,20 @@ env_specs: - cachetools=4.2.2=pyhd8ed1ab_0 - charset-normalizer=2.0.0=pyhd8ed1ab_0 - colorama=0.4.4=pyh9f0ad1d_0 + - commonmark=0.9.1=py_0 - cycler=0.10.0=py_2 - dataclasses=0.8=pyhc8e2a94_3 - decorator=4.4.2=py_0 - defusedxml=0.7.1=pyhd8ed1ab_0 - - entrypoints=0.3=pyhd8ed1ab_1003 + - euporie=0.1.16=pyhd8ed1ab_0 - execnet=1.9.0=pyhd8ed1ab_0 - flake8=3.9.2=pyhd8ed1ab_0 + - font-ttf-dejavu-sans-mono=2.37=hab24e00_0 + - font-ttf-inconsolata=3.000=h77eed37_0 + - font-ttf-source-code-pro=2.038=h77eed37_0 + - font-ttf-ubuntu=0.83=hab24e00_0 + - fonts-conda-ecosystem=1=0 + - fonts-conda-forge=1=0 - frozendict=2.0.3=pyhd8ed1ab_0 - html5lib=1.1=pyh9f0ad1d_0 - idna=3.1=pyhd3deb0d_0 @@ -463,7 +581,6 @@ env_specs: - pandocfilters=1.4.2=py_1 - parso=0.8.2=pyhd8ed1ab_0 - pathspec=0.9.0=pyhd8ed1ab_0 - - pickleshare=0.7.5=py_1003 - pip=21.2.4=pyhd8ed1ab_0 - pkginfo=1.7.1=pyhd8ed1ab_0 - prometheus_client=0.11.0=pyhd8ed1ab_0 @@ -478,6 +595,7 @@ env_specs: - pylint=2.9.6=pyhd8ed1ab_0 - pyopenssl=20.0.1=pyhd8ed1ab_0 - pyparsing=2.4.7=pyh9f0ad1d_0 + - pyperclip=1.8.2=pyhd8ed1ab_2 - pytest-asyncio=0.15.1=pyhd8ed1ab_0 - pytest-cov=2.12.1=pyhd8ed1ab_0 - pytest-forked=1.3.0=pyhd3deb0d_0 @@ -513,7 +631,9 @@ env_specs: - wxyz_lab=0.5.1=pyhd8ed1ab_0 - zipp=3.5.0=pyhd8ed1ab_0 unix: + - entrypoints=0.3=pyhd8ed1ab_1003 - pexpect=4.8.0=pyh9f0ad1d_2 + - pickleshare=0.7.5=py_1003 - ptyprocess=0.7.0=pyhd3deb0d_0 linux-64: - _libgcc_mutex=0.1=conda_forge @@ -522,10 +642,12 @@ env_specs: - anyio=3.3.0=py39hf3d152e_0 - argon2-cffi=20.1.0=py39h3811e60_2 - astroid=2.6.6=py39hf3d152e_0 + - atk-1.0=2.36.0=h3371d22_4 - brotlipy=0.7.0=py39h3811e60_1001 - bzip2=1.0.8=h7f98852_4 - c-ares=1.17.2=h7f98852_0 - ca-certificates=2021.5.30=ha878542_0 + - cairo=1.16.0=h6cf1ce9_1008 - certifi=2021.5.30=py39hf3d152e_0 - cffi=1.14.6=py39he32792d_0 - chardet=4.0.0=py39hf3d152e_1 @@ -540,12 +662,21 @@ env_specs: - expat=2.4.1=h9c3ff4c_0 - fontconfig=2.13.1=hba837de_1005 - freetype=2.10.4=h0708190_1 + - fribidi=1.0.10=h36c2ea0_0 + - future=0.18.2=py39hf3d152e_3 + - gdk-pixbuf=2.42.6=h04a7f16_0 - gettext=0.19.8.1=h0b5b191_1005 + - giflib=5.2.1=h36c2ea0_2 - git=2.32.0=pl5321hc30692c_0 - glib-tools=2.68.3=h9c3ff4c_0 - glib=2.68.3=h9c3ff4c_0 + - graphite2=1.3.13=h58526e2_1001 + - graphviz=2.48.0=h85b4f2f_0 - gst-plugins-base=1.18.4=hf529b03_2 - gstreamer=1.18.4=h76c114f_2 + - gtk2=2.24.33=h539f30e_1 + - gts=0.7.6=h64030ff_2 + - harfbuzz=2.8.2=h83ec7ef_0 - icu=68.1=h58526e2_0 - importlib-metadata=4.6.4=py39hf3d152e_0 - ipykernel=6.1.0=py39hef51801_0 @@ -573,6 +704,7 @@ env_specs: - libevent=2.1.10=hcdb4288_3 - libffi=3.3=h58526e2_2 - libgcc-ng=11.1.0=hc902ee8_8 + - libgd=2.3.2=h78a0170_0 - libgfortran-ng=11.1.0=h69a702a_8 - libgfortran5=11.1.0=h6c583b3_8 - libglib=2.68.3=h3e27bee_0 @@ -586,13 +718,17 @@ env_specs: - libopus=1.3.1=h7f98852_1 - libpng=1.6.37=h21135ba_2 - libpq=13.3=hd57d9b9_0 + - librsvg=2.50.7=hc3c00ef_0 - libsodium=1.0.18=h36c2ea0_1 - libssh2=1.9.0=ha56f1ee_6 - libstdcxx-ng=11.1.0=h56837e0_8 - libtiff=4.3.0=hf544144_1 + - libtool=2.4.6=h58526e2_1007 - libuuid=2.32.1=h7f98852_1000 + - libuv=1.42.0=h7f98852_0 - libvorbis=1.3.7=h9c3ff4c_0 - libwebp-base=1.2.0=h7f98852_2 + - libwebp=1.2.0=h3452ae3_0 - libxcb=1.13=h7f98852_1003 - libxkbcommon=1.0.3=he3ba5ed_0 - libxml2=2.9.12=h72842e0_0 @@ -608,17 +744,21 @@ env_specs: - mysql-libs=8.0.25=hfa10184_2 - nbconvert=6.1.0=py39hf3d152e_0 - ncurses=6.2=h58526e2_4 + - nodejs=14.17.4=h92b4a50_0 - nspr=4.30=h9c3ff4c_0 - nss=3.69=hb5efdd6_0 - numpy=1.21.1=py39hdbf815f_0 + - openjdk=11.0.9.1=h5cc2fde_1 - openjpeg=2.4.0=hb52868f_1 - openssl=1.1.1k=h7f98852_0 - pandas=1.3.1=py39hde0f152_0 - pandoc=2.14.1=h7f98852_0 + - pango=1.48.7=hb8ff022_0 - pcre2=10.37=h032f7d1_0 - pcre=8.45=h9c3ff4c_0 - perl=5.32.1=0_h7f98852_perl5 - pillow=8.3.1=py39ha612740_0 + - pixman=0.40.0=h36c2ea0_0 - pluggy=0.13.1=py39hf3d152e_4 - pthread-stubs=0.4=h36c2ea0_1001 - pydantic=1.8.2=py39h3811e60_0 @@ -637,6 +777,7 @@ env_specs: - rdflib=6.0.0=py39hf3d152e_0 - readline=8.1=h46c0cb4_0 - regex=2021.8.3=py39h3811e60_0 + - rich=10.7.0=py39hf3d152e_0 - ruamel.yaml.clib=0.2.2=py39h3811e60_2 - ruamel.yaml=0.17.10=py39h3811e60_0 - scipy=1.7.1=py39hee8e79c_0 @@ -653,8 +794,23 @@ env_specs: - websocket-client=0.57.0=py39hf3d152e_4 - widgetsnbextension=3.5.1=py39hf3d152e_4 - wrapt=1.12.1=py39h3811e60_3 + - xorg-fixesproto=5.0=h7f98852_1002 + - xorg-inputproto=2.3.2=h7f98852_1002 + - xorg-kbproto=1.0.7=h7f98852_1002 + - xorg-libice=1.0.10=h7f98852_0 + - xorg-libsm=1.2.3=hd9c2040_1000 + - xorg-libx11=1.7.2=h7f98852_0 - xorg-libxau=1.0.9=h7f98852_0 - xorg-libxdmcp=1.1.3=h7f98852_0 + - xorg-libxext=1.3.4=h7f98852_1 + - xorg-libxfixes=5.0.3=h7f98852_1004 + - xorg-libxi=1.7.10=h7f98852_0 + - xorg-libxrender=0.9.10=h7f98852_1003 + - xorg-libxtst=1.2.3=h7f98852_1002 + - xorg-recordproto=1.14.2=h7f98852_1002 + - xorg-renderproto=0.11.1=h7f98852_1002 + - xorg-xextproto=7.3.0=h7f98852_1002 + - xorg-xproto=7.0.31=h7f98852_1007 - xz=5.2.5=h516909a_1 - zeromq=4.3.4=h9c3ff4c_0 - zlib=1.2.11=h516909a_1010 @@ -664,10 +820,12 @@ env_specs: - appnope=0.1.2=py39h6e9494a_1 - argon2-cffi=20.1.0=py39h89e85a6_2 - astroid=2.6.6=py39h6e9494a_0 + - atk-1.0=2.36.0=he69c4ee_4 - brotlipy=0.7.0=py39h89e85a6_1001 - bzip2=1.0.8=h0d85af4_4 - c-ares=1.17.2=h0d85af4_0 - ca-certificates=2021.5.30=h033912b_0 + - cairo=1.16.0=he43a7df_1008 - certifi=2021.5.30=py39h6e9494a_0 - cffi=1.14.6=py39hb71fe58_0 - chardet=4.0.0=py39h6e9494a_1 @@ -679,9 +837,19 @@ env_specs: - debugpy=1.4.1=py39h9fcab8e_0 - docutils=0.17.1=py39h6e9494a_0 - expat=2.4.1=he49afe7_0 + - fontconfig=2.13.1=h10f422b_1005 - freetype=2.10.4=h4cff582_1 + - fribidi=1.0.10=hbcb3906_0 + - future=0.18.2=py39h6e9494a_3 + - gdk-pixbuf=2.42.6=h2e6141f_0 - gettext=0.19.8.1=h7937167_1005 + - giflib=5.2.1=hbcb3906_2 - git=2.32.0=pl5321h9a53687_0 + - graphite2=1.3.13=h2e338ed_1001 + - graphviz=2.48.0=h77de9ca_0 + - gtk2=2.24.33=h675d97a_1 + - gts=0.7.6=hccb3bdf_2 + - harfbuzz=2.8.2=h159f659_0 - icu=68.1=h74dc148_0 - importlib-metadata=4.6.4=py39h6e9494a_0 - ipykernel=6.1.0=py39h71a6800_0 @@ -705,17 +873,23 @@ env_specs: - libedit=3.1.20191231=h0678c8f_2 - libev=4.33=haf1e3a3_1 - libffi=3.3=h046ec9c_2 + - libgd=2.3.2=h4e7a7ea_0 - libgfortran5=9.3.0=h6c81a4c_23 - libgfortran=5.0.0=9_3_0_h6c81a4c_23 + - libglib=2.68.3=hd556434_0 - libiconv=1.16=haf1e3a3_0 - liblapack=3.9.0=11_osx64_openblas - libnghttp2=1.43.0=h07e645a_0 - libopenblas=0.3.17=openmp_h3351f45_1 - libpng=1.6.37=h7cec526_2 + - librsvg=2.50.7=hd2a7919_0 - libsodium=1.0.18=hbcb3906_1 - libssh2=1.9.0=h52ee1ee_6 - libtiff=4.3.0=h1167814_1 + - libtool=2.4.6=h2e338ed_1007 + - libuv=1.42.0=h0d85af4_0 - libwebp-base=1.2.0=h0d85af4_2 + - libwebp=1.2.0=h1648767_0 - libxml2=2.9.12=h93ec3fd_0 - libxslt=1.1.33=h5739fc3_2 - llvm-openmp=12.0.1=hda6cdc1_1 @@ -728,14 +902,19 @@ env_specs: - mypy_extensions=0.4.3=py39h6e9494a_3 - nbconvert=6.1.0=py39h6e9494a_0 - ncurses=6.2=h2e338ed_4 + - nodejs=14.17.4=hb529b34_0 - numpy=1.21.1=py39h7eed0ac_0 + - openjdk=11.0.9.1=hcf210ce_1 - openjpeg=2.4.0=h6e7aa92_1 - openssl=1.1.1k=h0d85af4_0 - pandas=1.3.1=py39h4d6be9b_0 - pandoc=2.14.1=h0d85af4_0 + - pango=1.48.7=ha05cd14_0 - pcre2=10.37=ha16e1b2_0 + - pcre=8.45=he49afe7_0 - perl=5.32.1=0_h0d85af4_perl5 - pillow=8.3.1=py39he9bb72f_0 + - pixman=0.40.0=hbcb3906_0 - pluggy=0.13.1=py39h6e9494a_4 - pydantic=1.8.2=py39h89e85a6_0 - pyrsistent=0.17.3=py39h89e85a6_2 @@ -747,6 +926,7 @@ env_specs: - rdflib=6.0.0=py39h6e9494a_0 - readline=8.1=h05e3726_0 - regex=2021.8.3=py39h89e85a6_0 + - rich=10.7.0=py39h6e9494a_0 - ruamel.yaml.clib=0.2.2=py39hcbf5805_2 - ruamel.yaml=0.17.10=py39h89e85a6_0 - scipy=1.7.1=py39h056f1c0_0 @@ -773,6 +953,7 @@ env_specs: - atomicwrites=1.4.0=pyh9f0ad1d_0 - brotlipy=0.7.0=py39hb82d6ee_1001 - ca-certificates=2021.5.30=h5b45459_0 + - cairo=1.16.0=hb19e0ff_1008 - certifi=2021.5.30=py39hcbf5309_0 - cffi=1.14.6=py39h0878f49_0 - chardet=4.0.0=py39hcbf5309_1 @@ -782,8 +963,19 @@ env_specs: - cryptography=3.4.7=py39hd8d06c1_0 - debugpy=1.4.1=py39h415ef7b_0 - docutils=0.17.1=py39hcbf5309_0 + - entrypoints=0.3=py39hde42818_1002 + - expat=2.4.1=h39d44d4_0 + - fontconfig=2.13.1=h1989441_1005 - freetype=2.10.4=h546665d_1 + - fribidi=1.0.10=h8d14728_0 + - future=0.18.2=py39hcbf5309_3 + - getopt-win32=0.1=h8ffe710_0 + - gettext=0.19.8.1=h1a89ca6_1005 - git=2.32.0=h57928b3_0 + - graphite2=1.3.13=1000 + - graphviz=2.48.0=hefbd956_0 + - gts=0.7.6=h7c369d9_2 + - harfbuzz=2.8.2=hc601d6f_0 - icu=68.1=h0e60522_0 - importlib-metadata=4.6.4=py39hcbf5309_0 - intel-openmp=2021.3.0=h57928b3_3372 @@ -803,11 +995,17 @@ env_specs: - libcblas=3.9.0=11_win64_mkl - libclang=11.1.0=default_h5c34c98_1 - libdeflate=1.7=h8ffe710_5 + - libffi=3.3=h0e60522_2 + - libgd=2.3.2=h138e682_0 + - libglib=2.68.3=h1e62bf3_0 - libiconv=1.16=he774522_0 - liblapack=3.9.0=11_win64_mkl - libpng=1.6.37=h1d00b33_2 - libsodium=1.0.18=h8d14728_1 - libtiff=4.3.0=h0c97f57_1 + - libwebp-base=1.2.0=h8ffe710_2 + - libwebp=1.2.0=h57928b3_0 + - libxcb=1.13=hcd874cb_1003 - libxml2=2.9.12=hf5bbc77_0 - libxslt=1.1.33=h65864e5_2 - lxml=4.6.3=py39h4fd7cdf_0 @@ -825,13 +1023,20 @@ env_specs: - msys2-conda-epoch=20160418=1 - mypy_extensions=0.4.3=py39hcbf5309_3 - nbconvert=6.1.0=py39hcbf5309_0 + - nodejs=14.17.4=h57928b3_0 - numpy=1.21.1=py39h6635163_0 + - openjdk=11.0.1=c_compilervs2015_1016 - openjpeg=2.4.0=hb211442_1 - openssl=1.1.1k=h8ffe710_0 - pandas=1.3.1=py39h2e25243_0 - pandoc=2.14.1=h8ffe710_0 + - pango=1.48.7=hd84fcdd_0 + - pcre=8.45=h0e60522_0 + - pickleshare=0.7.5=py39hde42818_1002 - pillow=8.3.1=py39h916092e_0 + - pixman=0.40.0=h8ffe710_0 - pluggy=0.13.1=py39hcbf5309_4 + - pthread-stubs=0.4=hcd874cb_1001 - pydantic=1.8.2=py39hb82d6ee_0 - pyqt-impl=5.12.3=py39h415ef7b_7 - pyqt5-sip=4.19.18=py39h415ef7b_7 @@ -850,6 +1055,7 @@ env_specs: - rdflib-jsonld=0.5.0=py39hcbf5309_2 - rdflib=6.0.0=py39hcbf5309_0 - regex=2021.8.3=py39hb82d6ee_0 + - rich=10.7.0=py39hcbf5309_0 - ruamel.yaml.clib=0.2.2=py39hb82d6ee_2 - ruamel.yaml=0.17.10=py39hb82d6ee_0 - scipy=1.7.1=py39hc0c34ad_0 @@ -872,7 +1078,168 @@ env_specs: - wincertstore=0.2=py39hcbf5309_1006 - winpty=0.4.3=4 - wrapt=1.12.1=py39hb82d6ee_3 + - xorg-kbproto=1.0.7=hcd874cb_1002 + - xorg-libice=1.0.10=hcd874cb_0 + - xorg-libsm=1.2.3=hcd874cb_1000 + - xorg-libx11=1.7.2=hcd874cb_0 + - xorg-libxau=1.0.9=hcd874cb_0 + - xorg-libxdmcp=1.1.3=hcd874cb_0 + - xorg-libxext=1.3.4=hcd874cb_1 + - xorg-libxpm=3.5.13=hcd874cb_0 + - xorg-libxt=1.2.1=hcd874cb_2 + - xorg-xextproto=7.3.0=hcd874cb_1002 + - xorg-xproto=7.0.31=hcd874cb_1007 - xz=5.2.5=h62dcd97_1 - zeromq=4.3.4=h0e60522_0 - zlib=1.2.11=h62dcd97_1010 - zstd=1.5.0=h6255e5f_0 + api_server: + locked: true + env_spec_hash: 26230e6b7fb6f33965f635bb27775eb2a150b92b + platforms: + - linux-64 + - osx-64 + - win-64 + packages: + all: + - charset-normalizer=2.0.0=pyhd8ed1ab_0 + - colorama=0.4.4=pyh9f0ad1d_0 + - pip=21.2.4=pyhd8ed1ab_0 + - pycparser=2.20=pyh9f0ad1d_2 + - pyopenssl=20.0.1=pyhd8ed1ab_0 + - python_abi=3.9=2_cp39 + - requests=2.26.0=pyhd8ed1ab_0 + - six=1.16.0=pyh6c4a22f_0 + - tzdata=2021a=he74cb21_1 + - urllib3=1.26.6=pyhd8ed1ab_0 + - wheel=0.37.0=pyhd8ed1ab_1 + unix: + - idna=3.1=pyhd3deb0d_0 + linux-64: + - _libgcc_mutex=0.1=conda_forge + - _openmp_mutex=4.5=1_gnu + - alsa-lib=1.2.3=h516909a_0 + - brotlipy=0.7.0=py39h3811e60_1001 + - ca-certificates=2021.5.30=ha878542_0 + - cairo=1.16.0=h6cf1ce9_1008 + - certifi=2021.5.30=py39hf3d152e_0 + - cffi=1.14.6=py39he32792d_0 + - chardet=4.0.0=py39hf3d152e_1 + - cryptography=3.4.7=py39hbca0aa6_0 + - fontconfig=2.13.1=hba837de_1005 + - freetype=2.10.4=h0708190_1 + - gettext=0.19.8.1=h0b5b191_1005 + - giflib=5.2.1=h36c2ea0_2 + - graphite2=1.3.13=h58526e2_1001 + - harfbuzz=2.8.2=h83ec7ef_0 + - icu=68.1=h58526e2_0 + - jbig=2.1=h7f98852_2003 + - jpeg=9d=h36c2ea0_0 + - krb5=1.19.2=hcc1bbae_0 + - lcms2=2.12=hddcbb42_0 + - ld_impl_linux-64=2.36.1=hea4e1c9_2 + - lerc=2.2.1=h9c3ff4c_0 + - libdeflate=1.7=h7f98852_5 + - libedit=3.1.20191231=he28a2e2_2 + - libffi=3.3=h58526e2_2 + - libgcc-ng=11.1.0=hc902ee8_8 + - libglib=2.68.3=h3e27bee_0 + - libgomp=11.1.0=hc902ee8_8 + - libiconv=1.16=h516909a_0 + - libpng=1.6.37=h21135ba_2 + - libpq=13.3=hd57d9b9_0 + - libstdcxx-ng=11.1.0=h56837e0_8 + - libtiff=4.3.0=hf544144_1 + - libuuid=2.32.1=h7f98852_1000 + - libwebp-base=1.2.1=h7f98852_0 + - libxcb=1.13=h7f98852_1003 + - libxml2=2.9.12=h72842e0_0 + - lz4-c=1.9.3=h9c3ff4c_1 + - ncurses=6.2=h58526e2_4 + - openjdk=11.0.9.1=h5cc2fde_1 + - openssl=1.1.1k=h7f98852_1 + - pcre=8.45=h9c3ff4c_0 + - pixman=0.40.0=h36c2ea0_0 + - postgresql=13.3=h2510834_0 + - pthread-stubs=0.4=h36c2ea0_1001 + - pysocks=1.7.1=py39hf3d152e_3 + - python=3.9.6=h49503c6_1_cpython + - readline=8.1=h46c0cb4_0 + - setuptools=57.4.0=py39hf3d152e_0 + - sqlite=3.36.0=h9cd32fc_0 + - tk=8.6.10=h21135ba_1 + - tzcode=2021a=h7f98852_2 + - xorg-fixesproto=5.0=h7f98852_1002 + - xorg-inputproto=2.3.2=h7f98852_1002 + - xorg-kbproto=1.0.7=h7f98852_1002 + - xorg-libice=1.0.10=h7f98852_0 + - xorg-libsm=1.2.3=hd9c2040_1000 + - xorg-libx11=1.7.2=h7f98852_0 + - xorg-libxau=1.0.9=h7f98852_0 + - xorg-libxdmcp=1.1.3=h7f98852_0 + - xorg-libxext=1.3.4=h7f98852_1 + - xorg-libxfixes=5.0.3=h7f98852_1004 + - xorg-libxi=1.7.10=h7f98852_0 + - xorg-libxrender=0.9.10=h7f98852_1003 + - xorg-libxtst=1.2.3=h7f98852_1002 + - xorg-recordproto=1.14.2=h7f98852_1002 + - xorg-renderproto=0.11.1=h7f98852_1002 + - xorg-xextproto=7.3.0=h7f98852_1002 + - xorg-xproto=7.0.31=h7f98852_1007 + - xz=5.2.5=h516909a_1 + - zlib=1.2.11=h516909a_1010 + - zstd=1.5.0=ha95c52a_0 + osx-64: + - brotlipy=0.7.0=py39h89e85a6_1001 + - ca-certificates=2021.5.30=h033912b_0 + - certifi=2021.5.30=py39h6e9494a_0 + - cffi=1.14.6=py39hb71fe58_0 + - chardet=4.0.0=py39h6e9494a_1 + - cryptography=3.4.7=py39ha2c9959_0 + - icu=68.1=h74dc148_0 + - krb5=1.19.2=hcfbf3a7_0 + - libcxx=12.0.1=habf9029_0 + - libedit=3.1.20191231=h0678c8f_2 + - libffi=3.3=h046ec9c_2 + - libiconv=1.16=haf1e3a3_0 + - libpq=13.3=hea3049e_0 + - libxml2=2.9.12=h93ec3fd_0 + - ncurses=6.2=h2e338ed_4 + - openjdk=11.0.9.1=hcf210ce_1 + - openssl=1.1.1k=h0d85af4_1 + - postgresql=13.3=he8fe76e_0 + - pysocks=1.7.1=py39h6e9494a_3 + - python=3.9.6=hd187cdc_1_cpython + - readline=8.1=h05e3726_0 + - setuptools=57.4.0=py39h6e9494a_0 + - sqlite=3.36.0=h23a322b_0 + - tk=8.6.10=h0419947_1 + - tzcode=2021a=h0d85af4_2 + - xz=5.2.5=haf1e3a3_1 + - zlib=1.2.11=h7795811_1010 + win-64: + - brotlipy=0.7.0=py39hb82d6ee_1001 + - ca-certificates=2021.7.5=haa95532_1 + - certifi=2021.5.30=py39hcbf5309_0 + - cffi=1.14.6=py39h0878f49_0 + - chardet=4.0.0=py39hcbf5309_1 + - cryptography=3.4.7=py39hd8d06c1_0 + - idna=3.2=pyhd3eb1b0_0 + - krb5=1.19.2=hbae68bd_0 + - libiconv=1.16=he774522_0 + - libpq=13.3=hfcc5ef8_0 + - libxml2=2.9.12=hf5bbc77_0 + - openjdk=11.0.9.1=h57928b3_1 + - openssl=1.1.1k=h8ffe710_1 + - postgresql=13.3=h1c22c4f_0 + - pysocks=1.7.1=py39hcbf5309_3 + - python=3.9.6=h7840368_1_cpython + - setuptools=57.4.0=py39hcbf5309_0 + - sqlite=3.36.0=h8ffe710_0 + - tk=8.6.10=h8ffe710_1 + - ucrt=10.0.20348.0=h57928b3_0 + - vc=14.2=hb210afc_5 + - vs2013_runtime=12.0.21005=1 + - vs2015_runtime=14.29.30037=h902a5da_5 + - win_inet_pton=1.1.0=py39hcbf5309_2 + - zlib=1.2.11=h62dcd97_1010 diff --git a/anaconda-project.yml b/anaconda-project.yml index 7d6dc081..7e4f1538 100644 --- a/anaconda-project.yml +++ b/anaconda-project.yml @@ -1,19 +1,39 @@ name: pymbe -icon: +icon: docs/_images/icon.png description: | A pythonic Model-based Engineering analysis framework based on SysML v2. +channels: +- conda-forge +- conda-forge/label/ipyelk_alpha # TODO: Remove this when ipyelk=2 is released +- nodefaults + +platforms: +- linux-64 +- osx-64 +- win-64 + variables: + CONDA_EXE: mamba MAX_LINE_LENGTH: 99 + PSQL_DBNAME: sysml2 + PSQL_HOST: 127.0.0.1 + PSQL_PORT: 5432 + PSQL_USERNAME: postgres + PSQL_PASSWORD: pUtY0uR$eCr3tP@$sW0rDh3R3 # <-- IMPORTANT: This is not secure, don't use your real password + REMOTE_API_SERVER_URL: http://sysml2-sst.intercax.com # or use http://sysml2.intercax.com + SBT_VERSION: 1.2.8 + SYSML2_API_RELEASE: 2021-06 + SYSML2_RELEASE: 2021-06.1 commands: lab: description: launch lab env_spec: developer - unix: &lab jupyter lab --no-browser --debug - windows: *lab + unix: jupyter lab --no-browser --debug + windows: jupyter lab --no-browser --debug setup: description: setup development environment env_spec: developer @@ -22,16 +42,21 @@ commands: pip install -e . --no-dependencies windows: | git submodule update --init & pip install -e . --no-dependencies + sysml:kernel:install: + description: setup SysML v2 kernel for JupyterLab in the `developer` environment + env_spec: developer + unix: python -m _scripts.kernel --install + windows: python -m _scripts.kernel --install lint: description: lint the code env_spec: developer unix: | isort . - black src/ tests/ -l {{MAX_LINE_LENGTH}} - flake8 src/ + black src/ tests/ _scripts/ -l {{MAX_LINE_LENGTH}} + flake8 src/ _scripts/ pylint src/ --rcfile=.pylintrc windows: | - isort . & black src/ tests/ -l {{MAX_LINE_LENGTH}} & flake8 src/ & pylint src/ --rcfile=.pylintrc + isort . & black src/ tests/ _scripts/ -l {{MAX_LINE_LENGTH}} & flake8 src/ _scripts/ & pylint src/ --rcfile=.pylintrc package: description: make a source distribution env_spec: developer @@ -47,6 +72,9 @@ commands: env_spec: developer unix: python -m tests.fixtures.update windows: python -m tests.fixtures.update + notebooks/Tutorial.ipynb: + env_spec: user + notebook: notebooks/Tutorial.ipynb push:fixtures: description: push the fixture data to the remote repo env_spec: developer @@ -76,44 +104,35 @@ commands: env_spec: developer unix: twine upload -r testpypi dist/* windows: twine upload -r testpypi dist/* --verbose - notebooks/Tutorial.ipynb: - env_spec: user - notebook: notebooks/Tutorial.ipynb + sysml:api:setup: + description: setup the SysML v2 Pilot Implementation API server + env_spec: api_server + unix: python -m _scripts.api_server --setup + windows: python -m _scripts.api_server --setup + sysml:api:start: + description: setup the API server + env_spec: api_server + unix: python -m _scripts.api_server --start + windows: python -m _scripts.api_server --start + sysml:api:stop: + description: setup the API server + env_spec: api_server + unix: python -m _scripts.api_server --stop + windows: python -m _scripts.api_server --stop vscode: env_spec: developer unix: code . windows: code . -channels: -- conda-forge -- conda-forge/label/ipyelk_alpha # TODO: Remove this when ipyelk=2 is released -- nodefaults - -platforms: -- linux-64 -- osx-64 -- win-64 - env_specs: - user: - description: The environment for running the notebooks + api_server: + description: The environment for the SysML v2 Pilot API Server + inherit_from: + - _java packages: - - importnb - - ipyelk >=2.0.0a0,<3 - - ipytree >=0.2.1,<1 - - jupyterlab >=3.0,<4 - - matplotlib - - networkx >=2.0,<3 - - notebook - - numpy - - openmdao >=3.0,<4 - - pyld - - rdflib - - rdflib-jsonld - - ruamel.yaml - - tabulate - - wxyz_html - - wxyz_lab + - colorama + - postgresql + - requests developer: description: The environment for developing pymbe inherit_from: @@ -134,3 +153,37 @@ env_specs: - testbook - twine >=3.0,<3.4 - wheel + user: + description: The environment for running the notebooks + inherit_from: + - _sysml_kernel + packages: + - importnb + - ipyelk >=2.0.0a0,<3 + - ipytree >=0.2.1,<1 + - jupyterlab >=3.0,<4 + - matplotlib + - networkx >=2.0,<3 + - notebook + - numpy + - openmdao >=3.0,<4 + - pyld + - rdflib + - rdflib-jsonld + - ruamel.yaml + - tabulate + - wxyz_html + - wxyz_lab + # partial environments used for aggregating + _java: + description: The environment for the SysML v2 Pilot API Server + packages: + - openjdk >=11,<12 + _sysml_kernel: + description: packages required by the SysML v2 Kernel + inherit_from: + - _java + packages: + - euporie + - graphviz =2 + - nodejs =14 diff --git a/docs/experimental/Circuit Topology.ipynb b/docs/experimental/Circuit Topology.ipynb new file mode 100644 index 00000000..dae19eea --- /dev/null +++ b/docs/experimental/Circuit Topology.ipynb @@ -0,0 +1,212 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1e4fd219-6dfb-49b2-bca8-436db1dbfe09", + "metadata": {}, + "source": [ + "Using NetworkX to explore an electrical circuit topology." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "34a508fb-4730-4b1e-b44a-51c710897d54", + "metadata": {}, + "outputs": [], + "source": [ + "import networkx as nx\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d1727134-7256-4ed6-add0-dcc74c4ba08d", + "metadata": {}, + "outputs": [], + "source": [ + "internal_edge_list = [\n", + " (\"R1 Neg\", \"R1 Pos\"),\n", + " (\"R2 Neg\", \"R2 Pos\"),\n", + " (\"D1 Neg\", \"D1 Pos\"),\n", + " (\"R3 Neg\", \"R3 Pos\")\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "93fcff4f-fe9f-4850-8faf-2d83c857b658", + "metadata": {}, + "outputs": [], + "source": [ + "connect_edge_list = [\n", + " (\"EMF Pos\", \"R1 Neg\"),\n", + " (\"R1 Pos\", \"D1 Neg\"),\n", + " (\"R1 Pos\", \"R2 Neg\"),\n", + " (\"R2 Pos\", \"R3 Neg\"),\n", + " (\"R3 Pos\", \"EMF Neg\"),\n", + " (\"D1 Pos\", \"EMF Neg\"),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3fb7f0bb-c168-4415-97d4-41dea6ecc31e", + "metadata": {}, + "outputs": [], + "source": [ + "color_dict = {\n", + " \"blue\": [\"R1 Neg\", \"R1 Pos\", \"R2 Neg\", \"R2 Pos\", \"R3 Neg\", \"R3 Pos\"],\n", + " \"#A020F0\": [\"D1 Neg\", \"D1 Pos\"],\n", + " \"red\": [\"EMF Neg\", \"EMF Pos\"]\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "37a4bf5a-8a74-4667-9678-6b831e20a0a9", + "metadata": {}, + "outputs": [], + "source": [ + "circuit_graph = nx.DiGraph()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0ace14f9-adf3-4815-a54f-ee888f2c3fd6", + "metadata": {}, + "outputs": [], + "source": [ + "circuit_graph.add_edges_from(internal_edge_list + connect_edge_list)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2fb66201-da5d-4bc3-9e58-bf4ea219fa75", + "metadata": {}, + "outputs": [], + "source": [ + "node_pos = nx.kamada_kawai_layout(circuit_graph)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a0e4666c-67c8-47db-ace2-ff39ca599c81", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'R1 Neg': array([0.67773802, 0.0598481 ]),\n", + " 'R1 Pos': array([0.34525184, 0.0759176 ]),\n", + " 'R2 Neg': array([0.10365002, 0.31490686]),\n", + " 'R2 Pos': array([-0.23494755, 0.31680085]),\n", + " 'D1 Neg': array([ 0.06054838, -0.10194618]),\n", + " 'D1 Pos': array([-0.22087253, -0.2721648 ]),\n", + " 'R3 Neg': array([-0.5321336 , 0.14976348]),\n", + " 'R3 Pos': array([-0.68997638, -0.1557316 ]),\n", + " 'EMF Pos': array([1. , 0.05870214]),\n", + " 'EMF Neg': array([-0.5092582 , -0.44609646])}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "node_pos" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "7bb19ab6-cc95-4983-b47c-9d9b55df8618", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'R1 Neg': Text(0.6777380167879264, 0.05984810486966587, 'R1 Neg'),\n", + " 'R1 Pos': Text(0.34525184132745734, 0.07591760143197503, 'R1 Pos'),\n", + " 'R2 Neg': Text(0.1036500196637759, 0.314906862261936, 'R2 Neg'),\n", + " 'R2 Pos': Text(-0.2349475462664004, 0.31680084560163935, 'R2 Pos'),\n", + " 'D1 Neg': Text(0.06054838045218313, -0.10194617792519632, 'D1 Neg'),\n", + " 'D1 Pos': Text(-0.2208725278791149, -0.27216479711700486, 'D1 Pos'),\n", + " 'R3 Neg': Text(-0.5321335982420786, 0.14976347776146173, 'R3 Neg'),\n", + " 'R3 Pos': Text(-0.6899763811428207, -0.15573160223007906, 'R3 Pos'),\n", + " 'EMF Pos': Text(1.0, 0.05870214159279685, 'EMF Pos'),\n", + " 'EMF Neg': Text(-0.5092582047009284, -0.4460964562471946, 'EMF Neg')}" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for color, colored_nodes in color_dict.items():\n", + " poses = {\n", + " node: loc\n", + " for node, loc in node_pos.items() if node in colored_nodes\n", + " }\n", + " nx.draw_networkx_nodes(circuit_graph, poses, nodelist=list(poses.keys()), node_size=600, node_color=color)\n", + " \n", + "nx.draw_networkx_edges(circuit_graph, node_pos, edgelist=connect_edge_list, edge_color=\"blue\")\n", + "nx.draw_networkx_edges(circuit_graph, node_pos, edgelist=internal_edge_list, edge_color=\"red\")\n", + "\n", + "labels = {\n", + " node_name: node_name for node_name in node_pos\n", + "}\n", + "label_options = {\"ec\": \"k\", \"fc\": \"white\", \"alpha\": 0.9}\n", + "nx.draw_networkx_labels(circuit_graph, node_pos, labels, font_size=8, font_color=\"Black\", bbox=label_options)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52b05e8d-391d-47ad-b194-92e49b07d2ab", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/experimental/Experiments.ipynb b/docs/experimental/Experiments.ipynb index f8720b46..9a84c5c4 100644 --- a/docs/experimental/Experiments.ipynb +++ b/docs/experimental/Experiments.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "ea67fea0", + "id": "53953e23", "metadata": {}, "source": [ "# Tutorial & Widget Experiments\n", @@ -17,7 +17,7 @@ }, { "cell_type": "markdown", - "id": "2822b5ee", + "id": "564a74a7", "metadata": {}, "source": [ "## 1. Import `pymbe` and create a new user interface" @@ -26,7 +26,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b5ad7fff", + "id": "abf6c0c9", "metadata": {}, "outputs": [], "source": [ @@ -36,7 +36,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33c7c6b0", + "id": "6ef453b2", "metadata": { "tags": [] }, @@ -47,7 +47,7 @@ }, { "cell_type": "markdown", - "id": "db605615", + "id": "7de9fbf5", "metadata": {}, "source": [ "## 2. Use the widget\n", @@ -56,7 +56,7 @@ }, { "cell_type": "markdown", - "id": "41b9fecd", + "id": "b96e93fe", "metadata": {}, "source": [ "1. Grabbing the individual widgets\n", @@ -66,19 +66,19 @@ { "cell_type": "code", "execution_count": null, - "id": "0e2f18ad-65f1-4182-b030-75262ee4d711", + "id": "78780b74", "metadata": { "tags": [] }, "outputs": [], "source": [ "ui, *_ = _.children\n", - "client, tree, inspector, lpg, *interpreter = ui.children" + "tree, inspector, m1_diagram, m0_diagram = ui.children" ] }, { "cell_type": "markdown", - "id": "2db2f9fa", + "id": "3ec0eec5", "metadata": {}, "source": [ "2. load a small model from a file or the big model from the internet" @@ -87,122 +87,48 @@ { "cell_type": "code", "execution_count": null, - "id": "06ad74c1-c4a0-4025-9ca9-7b7b45010bba", + "id": "0c68cb03-5903-405c-af40-250dd7e25166", "metadata": {}, "outputs": [], "source": [ - "USE_FILE = False\n", - "\n", - "if USE_FILE:\n", - " # client._load_from_file(\"../tests/fixtures/2a-Parts Interconnection.json\")\n", - " # client._load_from_file(\"../tests/fixtures/12b-Allocation.json\")\n", - " client._load_from_file(\"../tests/fixtures/Kerbal.json\")\n", - "else:\n", - " kerbal_project = [name for name in client.project_selector.options if name.startswith(\"Kerbal\")]\n", - " if kerbal_project:\n", - " client.project_selector.value = client.project_selector.options[kerbal_project[-1]]\n", - " client._download_elements()" - ] - }, - { - "cell_type": "markdown", - "id": "f22cac88-99e2-4064-a603-01208e1fef8e", - "metadata": {}, - "source": [ - "4. Click load" + "client.project_selector.options" ] }, { "cell_type": "code", "execution_count": null, - "id": "ac0a940a-1846-4109-8160-9c1ca085fc5f", + "id": "4f588a94", "metadata": {}, "outputs": [], "source": [ - "lpg.diagram.toolbar.refresh_diagram.click()" - ] - }, - { - "cell_type": "markdown", - "id": "2fd95db2-8015-47b6-9cf6-5872317842fe", - "metadata": {}, - "source": [ - "# Download with what is in the client" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0c509a22-bf86-423a-b300-1acf3ec1d395", - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "from pathlib import Path\n", - "\n", - "kerbal_file = Path(\"../tests/fixtures/Kerbal.json\")\n", - "kerbal_file.unlink()\n", + "USE_FILE = False\n", "\n", - "kerbal_file.write_text(json.dumps(list(client.elements_by_id.values()), indent=2))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "438c4316-be7f-4da6-bc70-6ff38bb99950", - "metadata": {}, - "outputs": [], - "source": [ - "lpg.id_mapper = lpg._make_id_mapper()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "391ae242-82ad-4df1-82ba-6f3a4a2b9748", - "metadata": {}, - "outputs": [], - "source": [ - "lpg.diagram.view.selection.ids" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "917a4237-4599-48d0-bd4c-93dbd0a0ea36", - "metadata": {}, - "outputs": [], - "source": [ - "lpg.id_mapper.get('cefe44fd-fae9-44c8-8465-c30b717e43ab')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7976d3d9-a3c2-4ebf-a71f-031272e5b314", - "metadata": {}, - "outputs": [], - "source": [ - "pipe = lpg.diagram.pipe\n", + "PROJECT_NAME = \"12b-Allocation\"\n", "\n", - "pipe.inlet.flow, pipe.outlet.flow, pipe.observes" + "if USE_FILE:\n", + " tree.model = pm.Model.load_from_file(FIXTURES / f\"{PROJECT_NAME}.json\")\n", + "else:\n", + " client = tree.api_client\n", + " project_names = [name for name in client.project_selector.options if name.startswith(PROJECT_NAME)]\n", + " if project_names:\n", + " *_, last_project = project_names\n", + " client.project_selector.label = last_project\n", + " client.download_model.click()" ] }, { "cell_type": "code", "execution_count": null, - "id": "9b531791-1488-4db4-a256-f1bcec0f9b77", + "id": "d48a0325", "metadata": {}, "outputs": [], "source": [ - "print(lpg.diagram.pipe.disposition)\n", - "for pipe in lpg.diagram.pipe.pipes:\n", - " print(pipe.__class__, pipe.disposition)" + "m1_diagram.elk_diagram.toolbar.refresh_diagram.click()" ] }, { "cell_type": "markdown", - "id": "dcf50302-858b-4f1a-ae2f-0ba618f807c8", + "id": "f48293c6", "metadata": {}, "source": [ "# Intepretation Diagram\n", @@ -213,7 +139,7 @@ }, { "cell_type": "markdown", - "id": "cd6aad75-6266-447c-861b-474f84010ad4", + "id": "a0beb149", "metadata": {}, "source": [ "# Add all implied edges" @@ -222,21 +148,21 @@ { "cell_type": "code", "execution_count": null, - "id": "10ce0f16-d420-4caf-86c8-f7f2a66aa725", + "id": "a67dc6fc", "metadata": {}, "outputs": [], "source": [ "from pymbe.interpretation.calc_dependencies import *\n", "from pymbe.interpretation.interp_playbooks import random_generator_playbook\n", "\n", - "instances = random_generator_playbook(lpg)\n", + "instances = random_generator_playbook(m1=lpg)\n", "pairs = generate_execution_order(lpg=lpg, instance_dict=instances)" ] }, { "cell_type": "code", "execution_count": null, - "id": "c401417e-d9a5-47a4-988c-1397a1bfb7d4", + "id": "acdac862", "metadata": {}, "outputs": [], "source": [ @@ -248,7 +174,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18e8d176-d95a-49bf-9975-4b107410e51a", + "id": "2c26cb03", "metadata": {}, "outputs": [], "source": [ @@ -258,7 +184,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28ef2d72-774c-4780-84e7-67110a02920c", + "id": "4e82d3c5", "metadata": {}, "outputs": [], "source": [ @@ -268,7 +194,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b2cccba6-c112-4596-864a-39efef467798", + "id": "a15f673a", "metadata": {}, "outputs": [], "source": [ @@ -277,7 +203,7 @@ }, { "cell_type": "markdown", - "id": "52cbcb0f", + "id": "45917337", "metadata": {}, "source": [ "...or you can directly adapt the graph and invert edges" @@ -286,7 +212,7 @@ { "cell_type": "code", "execution_count": null, - "id": "652c26e9-3be2-44ce-a149-572d99fb3316", + "id": "bd5a8593", "metadata": {}, "outputs": [], "source": [ @@ -296,17 +222,17 @@ { "cell_type": "code", "execution_count": null, - "id": "f7c8da82-279f-442e-bc61-ee15cc7c4aaa", + "id": "1f28b7a3", "metadata": {}, "outputs": [], "source": [ - "from pymbe.client import SysML2Client\n", + "from pymbe.client import APIClient\n", "from pathlib import Path\n", "\n", "\n", - "def kerbal_model_loaded_client() -> SysML2Client:\n", - " helper_client = SysML2Client()\n", - " helper_client._load_from_file(Path(data_loader.__file__).parent / \"data\" / \"Kerbal\" / \"elements.json\")\n", + "def kerbal_model_loaded_client() -> APIClient:\n", + " helper_client = APIClient()\n", + " helper_client.model = pm.Model.load_from_file(Path(data_loader.__file__).parent / \"data\" / \"Kerbal\" / \"elements.json\")\n", " return helper_client\n", "\n", "model = kerbal_model_loaded_client()" @@ -315,7 +241,7 @@ { "cell_type": "code", "execution_count": null, - "id": "f2b7b025-c5ad-4dea-ae51-bbb9dfe56b44", + "id": "03063c34", "metadata": {}, "outputs": [], "source": [ @@ -325,7 +251,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3250ea7d-18d2-4c54-869d-651fc4d10c98", + "id": "eed7aa2b", "metadata": {}, "outputs": [], "source": [ @@ -335,7 +261,7 @@ { "cell_type": "code", "execution_count": null, - "id": "06312528-6a16-4db1-8138-b0051d8447c4", + "id": "ab2ff200", "metadata": {}, "outputs": [], "source": [ @@ -345,7 +271,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8798802a-9872-451d-9f4e-a379c7f2a3b3", + "id": "5d3cadae", "metadata": {}, "outputs": [], "source": [ @@ -357,7 +283,7 @@ { "cell_type": "code", "execution_count": null, - "id": "69b1f0aa-c172-4187-9aca-19c6c930b164", + "id": "5a9d76b0", "metadata": {}, "outputs": [], "source": [ @@ -369,7 +295,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a8ac600b-dcf6-48c9-82d9-580a6783e4bf", + "id": "dfefc88b", "metadata": {}, "outputs": [], "source": [ @@ -378,7 +304,7 @@ }, { "cell_type": "markdown", - "id": "101f1f71-d3ee-4f3b-97d5-bee98e8732a0", + "id": "3652f29a", "metadata": {}, "source": [ "# Add RDF Capabilities with `ipyradiant`" @@ -387,7 +313,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20d887ff-1aa0-43d7-86ee-10fcb7c99708", + "id": "a388fb56", "metadata": {}, "outputs": [], "source": [ @@ -399,7 +325,7 @@ { "cell_type": "code", "execution_count": null, - "id": "611e1bdb-97a9-482f-b8f9-6b778d742d3d", + "id": "d6e31457", "metadata": {}, "outputs": [], "source": [ @@ -410,7 +336,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63587afe-21fa-40db-96bb-ccf641ba6a90", + "id": "df9ebf83", "metadata": {}, "outputs": [], "source": [ @@ -420,7 +346,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4b664981-244a-4263-98e1-266bbb676fc3", + "id": "c0827e95", "metadata": {}, "outputs": [], "source": [ @@ -430,7 +356,7 @@ { "cell_type": "code", "execution_count": null, - "id": "d2cd65f2-ef10-4ad7-9a98-32a59829ce7f", + "id": "c2381f9c", "metadata": {}, "outputs": [], "source": [ @@ -444,7 +370,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c93815f6-242a-462a-8605-49c18bb51fd5", + "id": "1631c984", "metadata": {}, "outputs": [], "source": [ @@ -454,7 +380,7 @@ { "cell_type": "code", "execution_count": null, - "id": "796f8fe6-4b3c-41bb-bab5-f4d3aea44c95", + "id": "7899a5b7", "metadata": {}, "outputs": [], "source": [ @@ -470,7 +396,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33bceaaf-4c05-49a9-ab1e-f843f3bc596a", + "id": "7e710be9", "metadata": {}, "outputs": [], "source": [ @@ -482,7 +408,7 @@ { "cell_type": "code", "execution_count": null, - "id": "937e52a7-6489-47e2-983b-69b468267047", + "id": "e29a15fc", "metadata": {}, "outputs": [], "source": [ @@ -510,7 +436,7 @@ { "cell_type": "code", "execution_count": null, - "id": "dc89fe28-cdcb-4a8e-8fb9-dcf512e1bcd9", + "id": "f5a97eed", "metadata": {}, "outputs": [], "source": [ @@ -524,7 +450,7 @@ }, { "cell_type": "markdown", - "id": "a1061c68", + "id": "f7e9d78b", "metadata": {}, "source": [ "# Interpretation\n", @@ -535,7 +461,7 @@ { "cell_type": "code", "execution_count": null, - "id": "047730fd-28c5-460f-9863-7ad817a24641", + "id": "e46213a5", "metadata": {}, "outputs": [], "source": [ @@ -545,7 +471,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c5fb1f1e", + "id": "00f6045a", "metadata": {}, "outputs": [], "source": [ @@ -562,7 +488,7 @@ { "cell_type": "code", "execution_count": null, - "id": "bde9737a", + "id": "16cbb6ec", "metadata": {}, "outputs": [], "source": [ @@ -607,7 +533,7 @@ { "cell_type": "code", "execution_count": null, - "id": "75203978", + "id": "efdfc656", "metadata": { "tags": [] }, @@ -642,7 +568,7 @@ }, { "cell_type": "markdown", - "id": "fb413585", + "id": "3e44f4a9", "metadata": {}, "source": [ "# Troubleshooting" @@ -650,7 +576,7 @@ }, { "cell_type": "markdown", - "id": "ec1c77ba", + "id": "93d4a955", "metadata": {}, "source": [ "## Fix linking issue with diagram element selector" @@ -659,7 +585,7 @@ { "cell_type": "code", "execution_count": null, - "id": "e676e9d0", + "id": "7ed96fb9", "metadata": {}, "outputs": [], "source": [ @@ -671,7 +597,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c6ce680a", + "id": "1a88989b", "metadata": {}, "outputs": [], "source": [ @@ -681,7 +607,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c4c3386f", + "id": "9989b83e", "metadata": {}, "outputs": [], "source": [ @@ -691,7 +617,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77ef2a4a", + "id": "fe75f5c3", "metadata": {}, "outputs": [], "source": [ @@ -703,7 +629,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1a2d587c", + "id": "21451864", "metadata": {}, "outputs": [], "source": [ @@ -727,7 +653,7 @@ { "cell_type": "code", "execution_count": null, - "id": "01f7053e", + "id": "45f08cf0", "metadata": {}, "outputs": [], "source": [ @@ -737,7 +663,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fa72a559", + "id": "ec5e6493", "metadata": {}, "outputs": [], "source": [ @@ -750,7 +676,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c80fc487", + "id": "34e0854a", "metadata": {}, "outputs": [], "source": [ @@ -760,7 +686,7 @@ { "cell_type": "code", "execution_count": null, - "id": "52be99a6", + "id": "bfac4c68", "metadata": {}, "outputs": [], "source": [ @@ -798,14 +724,14 @@ { "cell_type": "code", "execution_count": null, - "id": "365c822c", + "id": "16a6e7de", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", - "id": "b3f85347", + "id": "3b14e70b", "metadata": {}, "source": [ "---------------------" @@ -814,7 +740,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40555d37", + "id": "3aba2b0e", "metadata": {}, "outputs": [], "source": [ @@ -835,7 +761,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2f261d5b", + "id": "45202f6b", "metadata": {}, "outputs": [], "source": [ @@ -845,7 +771,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4da55b41", + "id": "d5da9004", "metadata": {}, "outputs": [], "source": [ @@ -855,7 +781,7 @@ { "cell_type": "code", "execution_count": null, - "id": "544aa0cb", + "id": "e5dbee8f", "metadata": {}, "outputs": [], "source": [ @@ -865,7 +791,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7e1d03dd", + "id": "47bd22c6", "metadata": { "tags": [] }, @@ -885,7 +811,7 @@ { "cell_type": "code", "execution_count": null, - "id": "f4f68e4d", + "id": "d8489a63", "metadata": {}, "outputs": [], "source": [ @@ -894,7 +820,7 @@ }, { "cell_type": "markdown", - "id": "a9bdefa8", + "id": "4f609ded", "metadata": {}, "source": [ "---------------" @@ -903,7 +829,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1b951ab2", + "id": "7db650ff", "metadata": {}, "outputs": [], "source": [ @@ -938,7 +864,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5e4838af", + "id": "bbc558cb", "metadata": {}, "outputs": [], "source": [ @@ -949,7 +875,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2718dd52", + "id": "2ddf1260", "metadata": {}, "outputs": [], "source": [ @@ -959,7 +885,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1fee9f6c", + "id": "5b990703", "metadata": {}, "outputs": [], "source": [ @@ -969,7 +895,7 @@ { "cell_type": "code", "execution_count": null, - "id": "260bc20a", + "id": "0221cb81", "metadata": {}, "outputs": [], "source": [ @@ -1012,7 +938,7 @@ { "cell_type": "code", "execution_count": null, - "id": "dcf4aad5", + "id": "a8a0ff63", "metadata": {}, "outputs": [], "source": [ @@ -1022,7 +948,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17e8af30", + "id": "3038d608", "metadata": {}, "outputs": [], "source": [ @@ -1033,7 +959,7 @@ { "cell_type": "code", "execution_count": null, - "id": "e1629d4c", + "id": "1f773c07", "metadata": {}, "outputs": [], "source": [ @@ -1042,7 +968,7 @@ }, { "cell_type": "markdown", - "id": "6e6ef285", + "id": "0058ca79", "metadata": {}, "source": [ "# Scratch Pad\n", @@ -1051,7 +977,7 @@ }, { "cell_type": "markdown", - "id": "6a6264e4", + "id": "8fc7867e", "metadata": {}, "source": [ "## RDF Experiments" @@ -1060,7 +986,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15b6228a", + "id": "3cf86a8b", "metadata": {}, "outputs": [], "source": [ @@ -1081,7 +1007,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51ecb619", + "id": "c97af903", "metadata": {}, "outputs": [], "source": [ @@ -1091,7 +1017,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9890d4be", + "id": "527bbf0d", "metadata": {}, "outputs": [], "source": [ @@ -1105,7 +1031,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3e4e6712", + "id": "531fd90b", "metadata": {}, "outputs": [], "source": [ @@ -1117,7 +1043,7 @@ { "cell_type": "code", "execution_count": null, - "id": "316bb6ec", + "id": "24fb3e89", "metadata": { "tags": [] }, @@ -1129,7 +1055,7 @@ }, { "cell_type": "markdown", - "id": "f0336664", + "id": "72ccf8e6", "metadata": {}, "source": [ "# Parse JSON-LD into RDF" @@ -1138,7 +1064,7 @@ { "cell_type": "code", "execution_count": null, - "id": "edc2e147", + "id": "5bbae2f6", "metadata": {}, "outputs": [], "source": [ @@ -1160,7 +1086,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c71f193d", + "id": "a5b97d33", "metadata": {}, "outputs": [], "source": [ @@ -1171,7 +1097,7 @@ }, { "cell_type": "markdown", - "id": "c057b5bf", + "id": "29c72308", "metadata": {}, "source": [ "# TODOs\n", @@ -1202,7 +1128,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.5" + "version": "3.9.6" } }, "nbformat": 4, diff --git a/docs/experimental/LPG Play.ipynb b/docs/experimental/LPG Play.ipynb index 6544d708..3986df18 100644 --- a/docs/experimental/LPG Play.ipynb +++ b/docs/experimental/LPG Play.ipynb @@ -120,11 +120,7 @@ "metadata": {}, "outputs": [], "source": [ - "random_rez = random_generator_playbook(\n", - " client,\n", - " lpg,\n", - " shorten_pre_bake\n", - ")" + "random_rez = random_generator_playbook(lpg)" ] }, { @@ -276,7 +272,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -290,7 +286,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.4" + "version": "3.9.6" } }, "nbformat": 4, diff --git a/docs/explanatory/Circuit Permutations.ipynb b/docs/explanatory/Circuit Permutations.ipynb index fcb12ffc..0defb551 100644 --- a/docs/explanatory/Circuit Permutations.ipynb +++ b/docs/explanatory/Circuit Permutations.ipynb @@ -2,61 +2,48 @@ "cells": [ { "cell_type": "markdown", - "id": "1577df35-0a5d-4ff0-8d54-bb0aff415b5e", + "id": "51e1ce1b-c8c6-4d0e-ab0d-a362fab8dd12", "metadata": {}, "source": [ "# Permutations of a Simple Circuit\n", "\n", - "This notebook walks through how to utilize the core semantics of SysML v2 to generate alternative circuits as inputs to an OpenMDAO solution of these circuits. \n", + "This notebook walks through how to utilize the core semantics of SysML v2 to generate alternative circuits as inputs to an OpenMDAO solution of these circuits.\n", "\n", - "## Background\n", - "\n", - "The M1 user model in SysML v2 is meant to be a set of constraints and rules under which legal instances can be created. Those instances should be taken as alternative produced systems and they can be analyzed in that way." - ] - }, - { - "cell_type": "markdown", - "id": "084ae194-f231-4775-8d8d-20bf0a6c47b6", - "metadata": {}, - "source": [ - "## Libraries Load-Up\n", - "\n", - "Load up PyMBE and its various libraries." + "If you want to play with a widget that does this, just run the cell below, if you want to understand the rest of the process, follow the rest of the notebook." ] }, { "cell_type": "code", "execution_count": null, - "id": "74c6fcfb-bcc4-4e07-b4bd-e9bd2a3f17f1", + "id": "43fb7bce-ae74-4c86-8c34-a4794642d980", "metadata": {}, "outputs": [], "source": [ + "import sys\n", "from pathlib import Path\n", - "import networkx as nx\n", - "import matplotlib as plt\n", "\n", "import pymbe.api as pm\n", "\n", - "from pymbe.client import SysML2Client\n", - "from pymbe.graph.lpg import SysML2LabeledPropertyGraph\n", - "from pymbe.interpretation.interpretation import repack_instance_dictionaries\n", - "from pymbe.interpretation.interp_playbooks import (\n", - " build_expression_sequence_templates,\n", - " build_sequence_templates,\n", - " random_generator_playbook,\n", - " random_generator_phase_1_multiplicities,\n", - ")\n", - "from pymbe.interpretation.results import *\n", - "from pymbe.label import get_label_for_id\n", - "from pymbe.query.metamodel_navigator import feature_multiplicity\n", - "from pymbe.query.query import (\n", - " roll_up_multiplicity,\n", - " roll_up_upper_multiplicity,\n", - " roll_up_multiplicity_for_type,\n", - " get_types_for_feature,\n", - " get_features_typed_by_type,\n", - ")\n", - "from pymbe.local.stablization import build_stable_id_lookups" + "pymbe_tests = (Path(pm.__file__).parent / \"../../tests\").resolve().absolute()\n", + "assert pymbe_tests.is_dir(), \"Cannot find pymbe tests folder!\"\n", + "\n", + "if str(pymbe_tests) not in sys.path:\n", + " sys.path.append(str(pymbe_tests))\n", + " \n", + "from interpretation.circuit_example import CircuitComponent, CircuitUI\n", + "\n", + "ui = CircuitUI()\n", + "ui" + ] + }, + { + "cell_type": "markdown", + "id": "54870ec5-2ba3-48af-bbdc-b6dd1eb35161", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "The M1 user model in SysML v2 is meant to be a set of constraints and rules under which legal instances can be created. Those instances should be taken as alternative produced systems and they can be analyzed in that way." ] }, { @@ -76,20 +63,10 @@ "metadata": {}, "outputs": [], "source": [ - "parts_client = SysML2Client()\n", - "\n", - "simple_parts_file = Path(pm.__file__).parent / \"../../tests/fixtures/Circuit Builder.json\"\n", - "\n", - "parts_client._load_from_file(simple_parts_file)\n", + "circuit_file = pymbe_tests / \"fixtures/Circuit Builder.json\"\n", "\n", - "parts_lpg = SysML2LabeledPropertyGraph()\n", - "parts_lpg.model = parts_client.model\n", - "\n", - "SIMPLE_MODEL = \"Model::Simple Parts Model::\"\n", - "\n", - "[id_to_parts_name_lookup, parts_name_to_id_lookup] = build_stable_id_lookups(parts_lpg)\n", - "\n", - "parts_lpg.model.MAX_MULTIPLICITY = 10" + "circuit_model = pm.Model.load_from_file(circuit_file)\n", + "circuit_model.max_multiplicity = 100" ] }, { @@ -109,7 +86,7 @@ "metadata": {}, "outputs": [], "source": [ - "parts_lpg.model.packages" + "circuit_model.packages" ] }, { @@ -119,7 +96,7 @@ "metadata": {}, "outputs": [], "source": [ - "parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement" + "circuit_model.ownedElement[\"Circuit Builder\"].ownedElement" ] }, { @@ -129,7 +106,7 @@ "metadata": {}, "outputs": [], "source": [ - "circuit_def = parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"]" + "circuit_def = circuit_model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"]" ] }, { @@ -162,445 +139,182 @@ "circuit_def.ownedMember" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "1d57f5e6-dffc-4484-8650-f6ea44b8b34c", - "metadata": {}, - "outputs": [], - "source": [ - "id_to_parts_name_lookup[circuit_def.ownedMember[\"Circuit Diode\"]._id]" - ] - }, { "cell_type": "markdown", - "id": "51c07d8b-26e8-475e-8a36-935226a33db8", - "metadata": {}, - "source": [ - "#### Feature types for sequences\n", - "\n", - "We can also inspect the types of each of the features (or classifier themselves) to see what will be placed into each step of the sequence.\n", - "\n", - "In the below, what we mean to say is that for a given position, the atom in that place will also appear in the 1-tail of the given type. For example, the type that corresponds to the Power User: Part::Power In: Port in the second position, which is a Part, will have all atoms in the 1-tail of Part's sequences. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd25238b-448b-4ec1-8c1b-dc664c62997a", + "id": "fae3a150-6770-4567-b617-c8284b36600d", "metadata": {}, - "outputs": [], - "source": [ - "feature_templates_with_ids = [[item for item in seq] for seq in build_sequence_templates(parts_lpg)\n", - " if id_to_parts_name_lookup[seq[0]].startswith(\"Model::Circuit Builder\")]\n", - "feature_templates_with_names = [[id_to_parts_name_lookup[item] for item in seq] for seq in build_sequence_templates(parts_lpg)\n", - " if id_to_parts_name_lookup[seq[0]].startswith(\"Model::Circuit Builder\")]\n", - "feature_templates_with_names" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "110b7ca4-4004-4eaf-a46e-7608328f7e33", - "metadata": {}, - "outputs": [], - "source": [ - "[\n", - " [\n", - " [id_to_parts_name_lookup[typ] for typ in get_types_for_feature(parts_lpg, parts_lpg.model.elements[item])]\n", - " for item in seq\n", - " ]\n", - "for seq in feature_templates_with_ids\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "adba109a-bb51-4fd6-9947-b43cbeda220b", - "metadata": {}, - "outputs": [], "source": [ - "expr_templates_with_ids = [[item for item in seq] for seq in build_expression_sequence_templates(parts_lpg)\n", - " if id_to_parts_name_lookup[seq[0]].startswith(\"Model::Circuit Builder\")]\n", - "expr_templates_with_names = [[id_to_parts_name_lookup[item] for item in seq] for seq in build_expression_sequence_templates(parts_lpg)\n", - " if id_to_parts_name_lookup[seq[0]].startswith(\"Model::Circuit Builder\")]\n", - "expr_templates_with_names" + "## Update multiplicities\n", + "for `Resistors`, `Diodes`, and `Connections`" ] }, { "cell_type": "markdown", - "id": "62b0b8b4-1ccd-44fc-9485-c3e6b65fed9d", + "id": "94ea2e9f-f4af-4e97-9e5e-713f3833c12a", "metadata": {}, "source": [ - "#### Exploring Connections and Ends\n", + "## Generate M0 instances from the M1 model\n", "\n", - "An important kind of feature is the feature that is typed by a Connection, which has two ends." + "Use the M1 model to start creating a series of instances to represent the circuits that should be analyzed." ] }, { "cell_type": "code", "execution_count": null, - "id": "28e665b2-1488-448e-b4ca-a027d4b57f19", + "id": "5d107dc0-f186-476d-800e-723e4a1ed6e6", "metadata": {}, "outputs": [], "source": [ - "connection_usage = [usage for usage in parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"].ownedElement\n", - " if usage._metatype == \"ConnectionUsage\"][0]\n", - "connection_usage" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bd5ef697-66c5-4dc4-a86f-4c3acbbe59fb", - "metadata": {}, - "outputs": [], - "source": [ - "ptg = parts_lpg.get_projection(\"Part Typing\")\n", - "scg = parts_lpg.get_projection(\"Part Definition\")\n", - "ssg = parts_lpg.get_projection(\"Redefinition and Subsetting\")\n", - "bdg = parts_lpg.get_projection(projection=\"Expanded Banded\", packages=[parts_lpg.model.ownedElement[\"Occurrences\"]])\n", - "bdg_all = parts_lpg.get_projection(projection=\"Expanded Banded\")\n", + "NUM_INTERPRETATIONS = 10\n", "\n", - "full_multiplicities = random_generator_phase_1_multiplicities(parts_lpg, ptg, scg)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f98cc909-2ac3-4de1-a8e0-005ae96f88ea", - "metadata": {}, - "outputs": [], - "source": [ - "{\n", - " id_to_parts_name_lookup[k]:v\n", - " for k, v in full_multiplicities.items()\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4846f0fe-ea1a-4279-8b40-235d72f6a973", - "metadata": {}, - "outputs": [], - "source": [ - "{\n", - " k:v\n", - " for k, v in full_multiplicities.items()\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8519a01a-694d-4cd2-a3b8-02da67d1119d", - "metadata": {}, - "outputs": [], - "source": [ - "[id_to_parts_name_lookup[typ_] for typ_ in get_features_typed_by_type(parts_lpg, '81b05f03-a17a-4f51-83a2-06d7f19d5c6d')]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c4c67be5-9c05-427a-a537-871d2d7761fc", - "metadata": {}, - "outputs": [], - "source": [ - "nx.is_directed_acyclic_graph(ptg)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e91a8a33-b7c1-4a48-8bc8-8f75e2cb6582", - "metadata": {}, - "outputs": [], - "source": [ - "nx.is_directed_acyclic_graph(scg)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6f58fcf2-83b5-4868-b6c1-fa7b5d125e52", - "metadata": {}, - "outputs": [], - "source": [ - "nx.is_directed_acyclic_graph(ssg)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7c7a121c-029a-43e2-b0ce-fef09be41725", - "metadata": {}, - "outputs": [], - "source": [ - "nx.is_directed_acyclic_graph(bdg)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bf774694-0ed4-4372-a632-b0c14e3b2ed1", - "metadata": {}, - "outputs": [], - "source": [ - "len(ssg.nodes)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e3c894d0-1856-464c-9f98-979e592b4074", - "metadata": {}, - "outputs": [], - "source": [ - "len(ssg.edges)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eeeaaf38-92c1-4b5a-9b68-b04cceca2ffb", - "metadata": {}, - "outputs": [], - "source": [ - "[id_to_parts_name_lookup[node] for listing in nx.simple_cycles(ssg) for node in listing]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57c22fef-d14b-4049-b774-231dee403d32", - "metadata": {}, - "outputs": [], - "source": [ - "[\n", - " id_to_parts_name_lookup[node]\n", - " for node in bdg_all.nodes\n", - " if bdg_all.out_degree(node) < 1\n", + "m0_interpretations = [\n", + " pm.random_generator_playbook(\n", + " m1=circuit_model,\n", + " filtered_feat_packages=[circuit_model.ownedElement[\"Circuit Builder\"]],\n", + " ) for _ in range(NUM_INTERPRETATIONS)\n", "]" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "d5adb9f3-d1d7-463c-a05d-c1eb7a1a40ac", - "metadata": {}, - "outputs": [], - "source": [ - "[[id_to_parts_name_lookup[node] for node in listing] for listing in nx.simple_cycles(bdg_all)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f69caa26-5365-4c89-a9ec-2360bf07a8b6", - "metadata": {}, - "outputs": [], - "source": [ - "item_def_id = parts_name_to_id_lookup[\"Model::Items::Item <>\"]\n", - "done_item_id = parts_name_to_id_lookup[\"Model::Items::Item::done: Item <>\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c10beecb-0b76-45cc-8a85-a79182c86d0b", - "metadata": {}, - "outputs": [], - "source": [ - "list(nx.all_simple_paths(\n", - " bdg_all,\n", - " source=item_def_id,\n", - " target=done_item_id,\n", - "))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "879ec85d-4ef6-42ab-a219-f5195a8bf981", - "metadata": {}, - "outputs": [], - "source": [ - "[id_to_parts_name_lookup[item] for item in bdg_all.predecessors(item_def_id)]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f16e420-1009-4948-82fa-81dc00b3e0dd", - "metadata": {}, - "outputs": [], - "source": [ - "cycle_check = [node for listing in nx.simple_cycles(ssg) for node in listing]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b4165f3d-9e2d-4e46-b756-d52433a58264", - "metadata": {}, - "outputs": [], - "source": [ - "list(ssg.predecessors(cycle_check[1]))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a4f053bb-8cd8-40a4-973f-e075ef8f29c9", - "metadata": {}, - "outputs": [], - "source": [ - "[parts_lpg.model.elements[item] for item in parts_lpg.get_projection(\"Generalization\")]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "475a133f-5e81-419f-a1a7-4c68033dff33", + "cell_type": "markdown", + "id": "1999bb54-3183-4f0f-8ee0-5e2bd172e602", "metadata": {}, - "outputs": [], "source": [ - "m0_interpretation = random_generator_playbook(\n", - " lpg=parts_lpg,\n", - " name_hints={},\n", - " filtered_feat_packages=[parts_lpg.model.ownedElement[\"Circuit Builder\"]],\n", - " phase_limit=10\n", - ")" + "### Sort the interpretations by number of connections\n", + "Sorted from `most` to `least`, and pick the first one." ] }, { "cell_type": "code", "execution_count": null, - "id": "42cc1aad-21a3-4518-97ca-14d54451c177", + "id": "f19a60d2-dff3-4f4a-b226-a880344703a1", "metadata": {}, "outputs": [], "source": [ - "parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit Connection\"]" + "circuit_model.elements[\"9cf7b7c6-194a-4882-8480-05a807d1391f\"]" ] }, { "cell_type": "code", "execution_count": null, - "id": "fdddca37-3b9a-4ba7-852a-91c08cf3bec7", + "id": "02c01d8f-a321-421b-a6c1-76c2c171926f", "metadata": {}, "outputs": [], "source": [ - "parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"].ownedMember[\"Component\"]" + "cf = circuit_model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"]\n", + "#cf.relationships[\"reverseFeatureTyping\"][0].owner\n", + "cf.throughFeatureMembership" ] }, { "cell_type": "code", "execution_count": null, - "id": "1a00a024-b58e-4f91-8427-00f5b1d0bb55", + "id": "ee0363db-54ad-46b0-b633-191f1dac0e08", "metadata": {}, "outputs": [], "source": [ - "m0_interpretation[parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit Connection\"]._id]" + "# sort the interpretations from most connections to fewer connections\n", + "for member in circuit_def.ownedMember:\n", + " if member._metatype == \"ConnectionUsage\":\n", + " connection = member\n", + " break\n", + "\n", + "m0_interpretations = [*sorted(m0_interpretations, key=lambda x: len(x[connection._id]), reverse=True)]\n", + "m0_interpretation = m0_interpretations[0]" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "8873ae60-778e-4f08-8261-3c01ae3a3591", + "cell_type": "markdown", + "id": "e0d3308f-053c-4c28-8d09-3c681b48c941", "metadata": {}, - "outputs": [], "source": [ - "m0_interpretation[parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"].ownedMember[\"Circuit Resistor\"]._id]" + "## Filter M0 Instances for Reasonable Circuits\n", + "\n", + "Until we get more sophisticated and can interpret constraints, the initial approach is to filter out solutions with unanalyzable layouts or trim the layouts to something more tractable." ] }, { - "cell_type": "code", - "execution_count": null, - "id": "132ea127-29c7-41e2-b79a-25850053e1f0", + "cell_type": "markdown", + "id": "fa01e6bf-8afc-4182-b5b4-dca690f03ded", "metadata": {}, - "outputs": [], "source": [ - "m0_interpretation[parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"].ownedMember[\"Circuit Diode\"]._id]" + "### Connector End Checks\n", + "\n", + "Look at the ends of the three main kinds of connectors." ] }, { "cell_type": "code", "execution_count": null, - "id": "385a1f5b-9d27-4cf2-90dd-039fd33fefa5", + "id": "70a0f874-05ea-4643-9162-ea836cc0d31d", "metadata": {}, "outputs": [], "source": [ - "m0_interpretation[parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"].ownedMember[\"Motive Force\"]._id]" + "p2p = circuit_def.ownedMember[\"Part to Part\"]\n", + "p2p.endFeature[0]._id" ] }, { "cell_type": "code", "execution_count": null, - "id": "5d6803c9-b364-4e9f-8bb6-e1ed4c242e81", + "id": "b24b8b54-4d59-4a27-bc29-cbfefd5d01f0", "metadata": {}, "outputs": [], "source": [ - "m0_interpretation[parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"EMF\"]._id]" + "source_feat, target_feat = p2p.endFeature\n", + "for source, target in zip(m0_interpretation[source_feat._id], m0_interpretation[target_feat._id]):\n", + " print(source, \"-->\", target)" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "5e7fd7fe-6149-4569-ac29-8fc05e118c78", + "cell_type": "markdown", + "id": "f89bbedc-46ed-4d92-aee0-56a6a6cfd029", "metadata": {}, - "outputs": [], "source": [ - "dict(nx.bfs_successors(parts_lpg.get_projection(\"Generalization\"), '9b93e6a8-d44e-4e17-a859-9cdedf60c144'))" + "# OpenMDAO\n", + "> Based on OpenMDAO's [nonlinear circuit analysis example](https://openmdao.org/newdocs/versions/latest/examples/circuit_analysis_examples.html)." ] }, { "cell_type": "code", "execution_count": null, - "id": "6186a51f-380c-43dd-897f-5ce5b1d7a668", + "id": "a15340a7-5964-4e42-b79e-3062aafbcd57", "metadata": {}, "outputs": [], "source": [ - "parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"].ownedMember[\"Circuit Diode\"]._id" + "from importlib import import_module\n", + "\n", + "import networkx as nx\n", + "import openmdao.api as om" ] }, { "cell_type": "code", "execution_count": null, - "id": "97187dbd-e417-4c5a-897b-4249befa504d", + "id": "397767c6-d841-45b4-a9b9-2fb4c4cd2021", "metadata": {}, "outputs": [], "source": [ - "m0_interpretation[parts_lpg.model.ownedElement[\"Circuit Builder\"].ownedElement[\"Circuit\"].ownedMember[\"Component\"]._id]" + "problem = ui.parametric_executor.problem\n", + "\n", + "if problem:\n", + " om.view_connections(problem)\n", + "else:\n", + " print(\"Click the '+' button in the dashboard until it generates a circuit\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "ce02435e-e884-4827-9758-9b223d525718", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "12fe3f73-8db5-47c6-ab02-afe4b1483752", + "id": "84b83e84-c4cd-482b-866b-eacd795244ba", "metadata": {}, "outputs": [], "source": [ - "pprint_interpretation(m0_interpretation, parts_lpg.model)" + "if problem:\n", + " om.n2(problem)\n", + "else:\n", + " print(\"Click the '+' button in the dashboard until it generates a circuit\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3a4df784-a091-44a6-81c6-e0041d176d74", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/explanatory/Expression Basics at M0.ipynb b/docs/explanatory/Expression Basics at M0.ipynb index ee575ff0..025490b0 100644 --- a/docs/explanatory/Expression Basics at M0.ipynb +++ b/docs/explanatory/Expression Basics at M0.ipynb @@ -22,7 +22,6 @@ "\n", "import pymbe.api as pm\n", "\n", - "from pymbe.client import SysML2Client\n", "from pymbe.graph.calc_lpg import CalculationGroup\n", "from pymbe.graph.lpg import SysML2LabeledPropertyGraph\n", "from pymbe.interpretation.calc_dependencies import (\n", @@ -55,14 +54,10 @@ "metadata": {}, "outputs": [], "source": [ - "parts_client = SysML2Client()\n", - "\n", - "simple_parts_file = Path(pm.__file__).parent / \"../../tests/fixtures/Simple Expressions.json\"\n", - "\n", - "parts_client._load_from_file(simple_parts_file)\n", - "\n", "parts_lpg = SysML2LabeledPropertyGraph()\n", - "parts_lpg.model = parts_client.model\n", + "parts_lpg.model = pm.Model.load_from_file(\n", + " Path(pm.__file__).parent / \"../../tests/fixtures/Simple Expressions.json\"\n", + ")\n", "\n", "SIMPLE_MODEL = \"Model::Simple Parts Model::\"\n", "\n", @@ -294,9 +289,8 @@ "outputs": [], "source": [ "m0_interpretation = random_generator_playbook(\n", - " parts_lpg,\n", - " {},\n", - " [parts_lpg.model.ownedElement[\"Simple Expressions\"]]\n", + " m1=parts_lpg,\n", + " filtered_feat_packages=[parts_lpg.model.ownedElement[\"Simple Expressions\"]],\n", ")" ] }, @@ -404,14 +398,6 @@ "source": [ "pprint_interpretation(m0_interpretation, parts_lpg.model)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "15d60311-646a-46a5-b3fe-87815c6708ac", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/explanatory/Feature Basics at M0.ipynb b/docs/explanatory/Feature Basics at M0.ipynb index 1dc10e8e..aea7b233 100644 --- a/docs/explanatory/Feature Basics at M0.ipynb +++ b/docs/explanatory/Feature Basics at M0.ipynb @@ -38,7 +38,6 @@ "\n", "import pymbe.api as pm\n", "\n", - "from pymbe.client import SysML2Client\n", "from pymbe.graph.lpg import SysML2LabeledPropertyGraph\n", "from pymbe.interpretation.interpretation import repack_instance_dictionaries\n", "from pymbe.interpretation.interp_playbooks import (\n", @@ -77,14 +76,10 @@ "metadata": {}, "outputs": [], "source": [ - "parts_client = SysML2Client()\n", - "\n", "simple_parts_file = Path(pm.__file__).parent / \"../../tests/fixtures/Simple Parts Model.json\"\n", "\n", - "parts_client._load_from_file(simple_parts_file)\n", - "\n", "parts_lpg = SysML2LabeledPropertyGraph()\n", - "parts_lpg.model = parts_client.model\n", + "parts_lpg.model = pm.Model.load_from_file(simple_parts_file)\n", "\n", "SIMPLE_MODEL = \"Model::Simple Parts Model::\"\n", "\n", @@ -323,14 +318,10 @@ "metadata": {}, "outputs": [], "source": [ - "parts_banded_client = SysML2Client()\n", - "\n", "simple_parts_banded_file = Path(pm.__file__).parent / \"../../tests/fixtures/Simple Parts Model Banded.json\"\n", "\n", - "parts_banded_client._load_from_file(simple_parts_banded_file)\n", - "\n", "parts_banded_lpg = SysML2LabeledPropertyGraph()\n", - "parts_banded_lpg.model = parts_banded_client.model\n", + "parts_banded_lpg.model = pm.Model.load_from_file(simple_parts_banded_file)\n", "\n", "SIMPLE_MODEL = \"Model::Simple Parts Model::\"\n", "\n", @@ -555,14 +546,6 @@ "source": [ "pprint_interpretation(m0_interpretation, parts_lpg.model)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "475a133f-5e81-419f-a1a7-4c68033dff33", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/how_to_guides/Understanding Test Approach.ipynb b/docs/how_to_guides/Understanding Test Approach.ipynb index 4db6ecee..67f2caca 100644 --- a/docs/how_to_guides/Understanding Test Approach.ipynb +++ b/docs/how_to_guides/Understanding Test Approach.ipynb @@ -410,10 +410,10 @@ "outputs": [], "source": [ "m0_interpretation = random_generator_playbook(\n", - " lpg=circuit_lpg,\n", + " m1=circuit_lpg,\n", " name_hints={},\n", " filtered_feat_packages=[circuit_lpg.model.ownedElement[\"Circuit Builder\"]],\n", - " phase_limit=10\n", + " phase_limit=10,\n", ")" ] }, diff --git a/docs/tutorials/01-Basic.ipynb b/docs/tutorials/01-Basic.ipynb index eb8a7ccb..605728db 100644 --- a/docs/tutorials/01-Basic.ipynb +++ b/docs/tutorials/01-Basic.ipynb @@ -47,7 +47,7 @@ "metadata": {}, "source": [ "## 2. Use the widget\n", - "![Example use of the UI](https://user-images.githubusercontent.com/1438114/115174713-c032e980-a097-11eb-99ed-27d56a9d0d91.gif)" + "![Example use of the UI](https://user-images.githubusercontent.com/1438114/132993186-858063a7-1bb7-483b-b3be-0d61d3d27fca.gif)" ] }, { @@ -101,15 +101,14 @@ "source": [ "USE_FILE = True\n", "\n", - "PROJECT_NAME = \"2a-Parts Interconnection\" # \"Simple Parts Model\" is too large for testing\n", + "PROJECT_NAME = \"Circuit Builder\" # \"Simple Parts Model\" is too large for testing\n", "\n", "FIXTURES = Path(pm.__file__).parent / \"../../tests/fixtures\"\n", "\n", - "client = tree.client\n", - "\n", "if USE_FILE:\n", - " client._load_from_file(FIXTURES / f\"{PROJECT_NAME}.json\")\n", + " tree.model = pm.Model.load_from_file(FIXTURES / f\"{PROJECT_NAME}.json\")\n", "else:\n", + " client = tree.api_client\n", " project_names = [name for name in client.project_selector.options if name.startswith(PROJECT_NAME)]\n", " if project_names:\n", " *_, last_project = project_names\n", diff --git a/src/pymbe/api.py b/src/pymbe/api.py index 57d403e7..d0d695ac 100644 --- a/src/pymbe/api.py +++ b/src/pymbe/api.py @@ -1,3 +1,11 @@ +from .interpretation.interp_playbooks import random_generator_playbook +from .model import Instance, Model +from .widget.client import APIClientWidget +from .widget.containment import ContainmentTree +from .widget.diagram import M0Viewer, M1Viewer +from .widget.inspector import ElementInspector +from .widget.ui import UI + __all__ = ( "ContainmentTree", "ElementInspector", @@ -5,14 +13,7 @@ "M0Viewer", "M1Viewer", "Model", - "SysML2ClientWidget", + "APIClientWidget", "UI", + "random_generator_playbook", ) - - -from .model import Instance, Model -from .widget.client import SysML2ClientWidget -from .widget.containment import ContainmentTree -from .widget.diagram import M0Viewer, M1Viewer -from .widget.inspector import ElementInspector -from .widget.ui import UI diff --git a/src/pymbe/client.py b/src/pymbe/client.py index e4887eeb..2552b09e 100644 --- a/src/pymbe/client.py +++ b/src/pymbe/client.py @@ -1,18 +1,17 @@ import re from datetime import datetime, timezone from functools import lru_cache -from pathlib import Path -from typing import Dict, List, Tuple, Union +from typing import Callable, Dict, List, Optional, Union from warnings import warn import ipywidgets as ipyw import requests import traitlets as trt from dateutil import parser -from ipywidgets.widgets.trait_types import TypedTuple -from .label import get_label -from .model import Model +from .model import Model, ModelClient + +URL_CACHE_SIZE = 1024 TIMEZONES = { "CEST": "UTC+2", @@ -37,7 +36,7 @@ } -class SysML2Client(trt.HasTraits): +class APIClient(trt.HasTraits, ModelClient): """ A traitleted SysML v2 API Client. @@ -46,8 +45,6 @@ class SysML2Client(trt.HasTraits): """ - model: Model = trt.Instance(Model, allow_none=True) - host_url = trt.Unicode( default_value="http://localhost", ) @@ -59,16 +56,12 @@ class SysML2Client(trt.HasTraits): ) page_size = trt.Integer( - default_value=5000, + default_value=200, min=1, ) paginate = trt.Bool(default_value=True) - folder_path: Path = trt.Instance(Path, allow_none=True) - json_files: Tuple[Path] = TypedTuple(trt.Instance(Path)) - json_file: Path = trt.Instance(Path, allow_none=True) - selected_project: str = trt.Unicode(allow_none=True) selected_commit: str = trt.Unicode(allow_none=True) @@ -116,34 +109,6 @@ def process_project_safely(project) -> dict: def _update_api_configuration(self, *_): self.projects = self._make_projects() - @trt.observe("selected_commit") - def _update_elements(self, *_, elements=None): - if not (self.selected_commit or elements): - return - elements = elements or [] - self.model = Model.load( - elements=elements, - name=f"""{ - self.projects[self.selected_project]["name"] - } ({self.host})""", - source=self.elements_url, - ) - for element in self.model.elements.values(): - if "label" not in element._derived: - element._derived["label"] = get_label(element) - - @trt.observe("folder_path") - def _update_json_files(self, *_): - if self.folder_path.exists(): - self.json_files = tuple(self.folder_path.glob("*.json")) - - @trt.observe("json_file") - def _update_elements_from_file(self, change: trt.Bunch = None): - if change is None: - return - if change.new != change.old and change.new.exists(): - self.model = Model.load_from_file(self.json_file) - @property def host(self): return f"{self.host_url}:{self.host_port}" @@ -167,34 +132,48 @@ def elements_url(self): raise SystemError("No selected project!") if not self.selected_commit: raise SystemError("No selected commit!") - arguments = f"?page[size]={self.page_size}" if self.page_size else "" - return f"{self.commits_url}/{self.selected_commit}/elements{arguments}" + return f"{self.commits_url}/{self.selected_commit}/elements" - @lru_cache - def _retrieve_data(self, url: str) -> List[Dict]: + def reset_cache(self): + self._retrieve_paginated_data.cache_clear() + + @staticmethod + def _retrieve_data( + url: str, process_response: bool = True + ) -> Union[List, Dict, requests.Response]: + response = requests.get(url) + if not response.ok: + raise requests.HTTPError( + f"Failed to retrieve elements from '{url}', reason: {response.reason}" + ) + if not process_response: + return response + return response.json() + + @lru_cache(maxsize=URL_CACHE_SIZE) + def _retrieve_paginated_data( + self, url: str, on_page: Callable = None, remove_empty_data: bool = True + ) -> List[Dict]: """Retrieve model data from a URL using pagination""" - result = [] + data = [] while url: - response = requests.get(url) - - if not response.ok: - raise requests.HTTPError( - f"Failed to retrieve elements from '{url}', reason: {response.reason}" - ) - - result += response.json() - + response = self._retrieve_data(url, process_response=False) + data += response.json() + if on_page: + on_page() link = response.headers.get("Link") if not link: break - urls = self._next_url_regex.findall(link) - url = None - if len(urls) == 1: - url = urls[0] - elif len(urls) > 1: - raise SystemError(f"Found multiple 'next' pagination urls: {urls}") - return result + if len(urls) > 1: + raise requests.HTTPError( + "Found multiple 'next' pagination urls: " + ", ".join(map(lambda x: f"<{x}>", urls)) + ) + url = urls[0] if urls else None + if remove_empty_data: + return [item for item in data if item] + return data @staticmethod def _parse_timestamp(timestamp: str) -> datetime: @@ -214,12 +193,38 @@ def clean_fields(data: dict) -> dict: commits = sorted(self._retrieve_data(self.commits_url), key=lambda x: x["timestamp"]) return {commit["@id"]: clean_fields(commit) for commit in commits} - def _download_elements(self): - elements = self._retrieve_data(self.elements_url) + def get_model(self, on_page: Callable = None) -> Optional[Model]: + """Download a model from the current `elements_url`.""" + if not self.selected_commit: + return None + url = self.elements_url + if self.page_size: + url += f"?page[size]={self.page_size}" + elements = self._retrieve_paginated_data(url=url, on_page=on_page) + total = len(elements) + # Remove bogus empty elements + elements = [ + element for element in elements if isinstance(element, dict) and "@id" in element + ] + if total > len(elements): + warn(f"Downloaded {total-len(elements)} bad elements, will have to ignore them") + if not elements: + return None + max_elements = self.page_size if self.paginate else 100 if len(elements) == max_elements: warn("There are probably more elements that were not retrieved!") - self._update_elements(elements=elements) - def _load_from_file(self, file_path: Union[str, Path]): - self.model = Model.load_from_file(file_path) + return Model.load( + elements=elements, + name=self.projects[self.selected_project]["name"], + source=self.elements_url, + _api=self, + ) + + def get_element_data(self, element_id: str) -> dict: + try: + return self._retrieve_data(f"{self.elements_url}/{element_id}") + except requests.HTTPError: + warn(f"Could not retrieve data for {element_id}") + return {"@id": element_id, "@type": "Element"} diff --git a/src/pymbe/graph/lpg.py b/src/pymbe/graph/lpg.py index 0b348e0f..bc6ce24e 100644 --- a/src/pymbe/graph/lpg.py +++ b/src/pymbe/graph/lpg.py @@ -226,7 +226,7 @@ def adapt( included_packages=tuple(sorted(included_packages)), ).copy() - @lru_cache(maxsize=128) + @lru_cache(maxsize=1024) def _adapt( self, excluded_node_types: ty.Union[list, set, tuple] = None, diff --git a/src/pymbe/interpretation/interp_playbooks.py b/src/pymbe/interpretation/interp_playbooks.py index b93336f2..2581482a 100644 --- a/src/pymbe/interpretation/interp_playbooks.py +++ b/src/pymbe/interpretation/interp_playbooks.py @@ -6,7 +6,7 @@ import logging import traceback from random import randint, sample -from typing import Dict, List +from typing import Dict, List, Union import networkx as nx @@ -46,9 +46,11 @@ "StateDefinition", ) +TYPES_FOR_CONNECTOR_INSTANCES = ("ConnectionUsage", "InterfaceUsage", "SuccessionUsage") -def random_generator_playbook( - lpg: SysML2LabeledPropertyGraph, + +def random_generator_playbook( # pylint: disable=invalid-name + m1: Union[Model, SysML2LabeledPropertyGraph], name_hints: Dict[str, str] = None, filtered_feat_packages: List[Element] = None, phase_limit: int = 10, @@ -57,14 +59,25 @@ def random_generator_playbook( Main routine to execute a playbook to randomly generate sequences as an interpretation of a SysML v2 model - :param lpg: Labeled propery graph of the M1 model + :param lpg: Labeled property graph of the M1 model :param name_hints: A dictionary to make labeling instances more clean :param filtered_feat_packages: A list of packages by which to down filter feature and expression sequence templates + :param phase_limit: Stop process before running the specified phase :return: A dictionary of sequences keyed by the id of a given M1 type """ + if isinstance(m1, Model): + model = m1 + lpg = SysML2LabeledPropertyGraph(model=model) + elif isinstance(m1, SysML2LabeledPropertyGraph): + lpg = m1 + model = m1.model + else: + raise TypeError( + f"`m1` must either be a pymbe `Model` or `SysML2LabeledPropertyGraph` not a {type(m1)}" + ) - all_elements = lpg.model.elements + all_elements = model.elements name_hints = name_hints or {} filtered_feat_packages = filtered_feat_packages or [] @@ -116,6 +129,9 @@ def random_generator_playbook( # Move through existing sequences and then start to pave further with new steps random_generator_playbook_phase_4(lpg.model, expression_sequences, instances_dict) + if phase_limit < 5: + return instances_dict + # PHASE 5: Interpret connection usages and map ConnectionEnds at M0 random_generator_playbook_phase_5(lpg, lpg.get_projection("Connection"), instances_dict) @@ -186,7 +202,7 @@ def random_generator_playbook_phase_1_singletons( Calculates instances for classifiers that aren't directly typed (but may have members or be superclasses for model elements that have sequences generated for them). - :param lpg: Active SysML graph + :param model: A SysML v2 model :param scg: Subclassing Graph projection from the LPG :param instances_dict: Working dictionary of interpreted sequences for the model :return: None - side effect is addition of new instances to the instances dictionary @@ -213,7 +229,6 @@ def random_generator_playbook_phase_2_rollup( Build up set of sequences for classifiers by taking the union of sequences already generated for the classifier subclasses. - :param lpg: Active SysML graph :param scg: Subclassing Graph projection from the LPG :param instances_dict: Working dictionary of interpreted sequences for the model :return: None - side effect is addition of new instances to the instances dictionary @@ -240,7 +255,7 @@ def random_generator_playbook_phase_2_unconnected( """ Final pass to generate sequences for classifiers that haven't been given sequences yet. - :param all_elements: Full dictionary of elements in the working memory + :param model: A SysML v2 Model :param instances_dict: Working dictionary of interpreted sequences for the model :return: None - side effect is addition of new instances to the instances dictionary """ @@ -269,10 +284,8 @@ def random_generator_playbook_phase_3( classifier sequences with randomly selected instances of classifiers that type nested features. + :param model: The SysML v2 Model :param feature_sequences: Sequences that represent the nesting structure of the features - :param all_elements: Full dictionary of elements in the working memory - :param lpg: Active SysML graph - :param ptg: Part Typing Graph projection from the LPG :param instances_dict: Working dictionary of interpreted sequences for the model :return: (Temporarily return a trace of actions) None - side effect is addition of new instances to the instances dictionary @@ -283,7 +296,7 @@ def random_generator_playbook_phase_3( # skip if the feature is abstract or its owning type is last_item = model.elements[feature_sequence[-1]] if last_item.isAbstract: - print(f"Skipped sequence ending in {last_item}") + # print(f"Skipped sequence ending in {last_item}") continue new_sequences = [] @@ -409,7 +422,7 @@ def random_generator_playbook_phase_3_new_instances( # skip if the feature is abstract or its owning type is last_item = model.elements[feature_sequence[-1]] if last_item.isAbstract or last_item.owner.isAbstract: - print(f"Skipped sequnce ending in {last_item}") + # print(f"Skipped sequnce ending in {last_item}") continue new_sequences = [] @@ -442,7 +455,7 @@ def random_generator_playbook_phase_3_new_instances( typ_id = feature_id lower_mult = feature_multiplicity(feature, "lower") - upper_mult = min(feature_multiplicity(feature, "upper"), model.MAX_MULTIPLICITY) + upper_mult = min(feature_multiplicity(feature, "upper"), model.max_multiplicity) new_sequences = extend_sequences_with_new_instance( new_sequences, @@ -505,9 +518,9 @@ def random_generator_playbook_phase_4( """ Generate interpreting sequences for Expressions in the model + :param model: The SysML v2 Model :param expr_sequences: Sequences that represent the membership structure for expressions in the model and the features to which expressions provide values - :param lpg: Active SysML graph :param instances_dict: Working dictionary of interpreted sequences for the model :return: None - side effect is addition of new instances to the instances dictionary """ @@ -578,92 +591,88 @@ def random_generator_playbook_phase_5( """ Generate instances for connector usages and their specializations and randomly connect ends to legal sources and targets + :param lpg: Active SysML graph :param cug: A connector usage graph projection to see where their source/targets are linked :param instances_dict: Working dictionary of interpreted sequences for the model + :return: None - side effect is addition of new instances to the instances dictionary """ - + elements = lpg.model.elements # Generate sequences for connection and interface ends - for node_id in list(cug.nodes): - node = lpg.model.elements[node_id] - if node._metatype in ("ConnectionUsage", "InterfaceUsage", "SuccessionUsage"): - - connector_ends = node.connectorEnd - - connector_id = node._id - - source_feat_id = node.source[0].chainingFeature[-1]._id - target_feat_id = node.target[0].chainingFeature[-1]._id - - try: - source_sequences = instances_dict[source_feat_id] - except KeyError as exc: - raise KeyError( - f"Cannot find {lpg.model.elements[source_feat_id]} in the interpretation!" - ) from exc - - try: - target_sequences = instances_dict[target_feat_id] - except KeyError as exc: - raise KeyError( - f"Cannot find {lpg.model.elements[target_feat_id]} in the interpretation!" - ) from exc - - connectors = instances_dict[connector_id] - - extended_source_sequences = [] - extended_target_sequences = [] - - min_side = min(len(source_sequences), len(target_sequences)) - max_side = max(len(source_sequences), len(target_sequences)) - - try: - if len(source_sequences) <= len(target_sequences): - source_indices = list(range(0, min_side)) - other_steps = sample(range(0, min_side), (max_side - min_side)) - source_indices.extend(other_steps) - target_indices = sample(range(0, max_side), max_side) - else: - source_indices = sample(range(0, max_side), max_side) - other_steps = sample(range(0, min_side), (max_side - min_side)) - target_indices = list(range(0, min_side)) - target_indices.extend(other_steps) - except ValueError as exc: - raise ValueError( - f"Sample larger than population or is negative. Min_side size is {min_side} " - f"and max_side size is {max_side}." - ) from exc - - # taking over indx to wrap around - indx = 0 + for connector_id in cug.nodes: + connector = elements[connector_id] + if connector._metatype not in TYPES_FOR_CONNECTOR_INSTANCES: + continue - for seq in connectors: - new_source_seq = [] - new_target_seq = [] + connector_ends = connector.connectorEnd + if len(connector_ends) != 2: + raise NotImplementedError( + f"Cannot process connector {connector} because it" + f" it has {len(connector_ends) or 'no'} connector end(s)" + ) - for item in seq: - new_source_seq.append(item) - new_target_seq.append(item) + source, *other_sources = connector.source + target, *other_targets = connector.target + if other_sources or other_targets: + raise NotImplementedError( + f"Cannot process connector {connector} because it" + " has multiple sources and/or targets" + ) - for jndx, item in enumerate(source_sequences[source_indices[indx]]): - if jndx > 0: - new_source_seq.append(item) + try: + source_sequences = instances_dict[source.chainingFeature[-1]._id] + except IndexError as exc: + raise IndexError(f"{source} has no chainingFeatures!") from exc + except KeyError as exc: + raise KeyError(f"Cannot find chaining feature sequences for {source}!") from exc - for jndx, item in enumerate(target_sequences[target_indices[indx]]): - if jndx > 0: - new_target_seq.append(item) + try: + target_sequences = instances_dict[target.chainingFeature[-1]._id] + except IndexError as exc: + raise IndexError(f"{target} has no chainingFeatures!") from exc + except KeyError as exc: + raise KeyError(f"Cannot find chaining feature sequences for {target}!") from exc - extended_source_sequences.append(new_source_seq) - extended_target_sequences.append(new_target_seq) + # Get sequence sizes for each side + sizes = len(source_sequences), len(target_sequences) + min_side, max_side = min(sizes), max(sizes) - if indx >= len(target_indices) - 1: - indx = 0 - else: - indx = indx + 1 + # if one side has no connections, there's nothing to connect + if min_side < 1: + for connector_end in connector_ends: + instances_dict[connector_end._id] = [] + continue - instances_dict[connector_ends[0]._id] = extended_source_sequences - instances_dict[connector_ends[1]._id] = extended_target_sequences + try: + indeces = list(range(min_side)) + [ + randint(0, min_side - 1) for _ in range(max_side - min_side) + ] + if len(source_sequences) <= len(target_sequences): + connector_ends_indeces = indeces, sample(range(max_side), max_side) + else: + connector_ends_indeces = sample(range(max_side), max_side), indeces + except ValueError as exc: + raise ValueError( + "Sample larger than population or is negative. Sizes are:" + f" {min_side} and {max_side}." + ) from exc + + connected_feature_sequences = (source_sequences, target_sequences) + connector_sequences = instances_dict[connector_id] + for connector_end, connector_end_indeces, connected_feature_sequence in zip( + connector_ends, connector_ends_indeces, connected_feature_sequences + ): + instances_dict[connector_end._id] = [ + connector_sequences[idx] + + connected_feature_sequence[connector_end_indeces[idx]][1:] + for idx in range(min(len(connector_sequences), max_side)) + ] + for idx in range(len(connector_sequences) - max_side): + connector_sequence = connector_sequences[idx + max_side] + instances_dict[connector_end._id] += [ + connector_sequence + sample(connected_feature_sequence, 1)[0][1:] + ] def build_sequence_templates(lpg: SysML2LabeledPropertyGraph) -> List[List[str]]: @@ -734,7 +743,6 @@ def build_expression_sequence_templates(lpg: SysML2LabeledPropertyGraph) -> List :return: list of lists of Element IDs (as strings) representing feature nesting. """ - evg = lpg.get_projection("Expression Value") sorted_feature_groups = [] diff --git a/src/pymbe/model.py b/src/pymbe/model.py index d2306f3d..0ffa128b 100644 --- a/src/pymbe/model.py +++ b/src/pymbe/model.py @@ -4,7 +4,7 @@ from enum import Enum from functools import lru_cache from pathlib import Path -from typing import Any, Dict, List, Set, Tuple, Union +from typing import Any, Collection, Dict, List, Optional, Tuple, Union from uuid import uuid4 from warnings import warn @@ -55,6 +55,11 @@ def get_name(self, element: "Element") -> str: return f"""<{name} «{data["@type"]}»>""" +class ModelClient: + def get_element_data(self, element_id: str) -> dict: + raise NotImplementedError("Must be implemented by the subclass") + + @dataclass(repr=False) class Model: # pylint: disable=too-many-instance-attributes """A SysML v2 Model""" @@ -81,6 +86,8 @@ class Model: # pylint: disable=too-many-instance-attributes source: Any = None + _api: ModelClient = None + _initializing: bool = True _naming: Naming = Naming.LONG # The scheme to use for repr'ing the elements def __post_init__(self): @@ -95,6 +102,7 @@ def __post_init__(self): # Modify and add derived data to the elements self._add_relationships() self._add_labels() + self._initializing = False def __repr__(self) -> str: data = self.source or f"{len(self.elements)} elements" @@ -102,12 +110,12 @@ def __repr__(self) -> str: @staticmethod def load( - elements: Union[List[Dict], Set[Dict], Tuple[Dict]], + elements: Collection[Dict], **kwargs, ) -> "Model": """Make a Model from an iterable container of elements""" return Model( - elements={element["@id"]: element for element in elements}, + elements={element["@id"]: element for element in elements if "@id" in element}, **kwargs, ) @@ -132,24 +140,77 @@ def packages(self) -> Tuple["Element"]: element for element in self.elements.values() if element._metatype == "Package" ) - def save_to_file(self, filepath: Union[Path, str], indent: int = 2): + def get_element( + self, element_id: str, fail: bool = True, resolve: bool = True + ) -> Optional["Element"]: + """Get an element, or retrieve it from the API if it is there""" + element = self.elements.get(element_id) + if element and not isinstance(element, Element): + return element + if not element and self._api: + data = self._api.get_element_data(element_id) if resolve else {} + element = Element(_id=element_id, _data=data, _model=self) + if element and resolve and element._is_proxy: + element.resolve() + if fail and element is None: + raise KeyError(f"Could not retrieve '{element_id}' from the API") + return element + + def _add_element(self, element: "Element") -> "Element": + id_ = element._id + metatype = element._metatype + + self.elements[id_] = element + + if not self._initializing: + self._add_labels(element) + + if element.get_owner() is None: + if element not in self.ownedElement: + self.ownedElement += [element] + + if metatype not in self.ownedMetatype: + self.ownedMetatype[metatype] = [] + if element not in self.ownedMetatype[metatype]: + self.ownedMetatype[metatype] += [element] + + if element._is_relationship: + if element not in self.ownedRelationship: + self.ownedRelationship += [element] + if id_ not in self.all_relationships: + self.all_relationships[id_] = element + else: + if id_ not in self.all_non_relationships: + self.all_non_relationships[id_] = element + return element + + def save_to_file(self, filepath: Union[Path, str] = None, indent: int = 2): + filepath = filepath or self.name + if not self.elements: + warn("Model has no elements, nothing to save!") + return if isinstance(filepath, str): filepath = Path(filepath) + if not filepath.name.endswith(".json"): + filepath = filepath.parent / f"{filepath.name}.json" + if filepath.exists(): + warn(f"Overwriting {filepath}") filepath.write_text( json.dumps( - [element._data for element in self.elements.values()], + [element._data for element in self.elements.values() if element._data], indent=indent, ), ) - def _add_labels(self): + def _add_labels(self, *elements): """Attempts to add a label to the elements""" from .label import get_label # pylint: disable=import-outside-toplevel - for element in self.elements.values(): + elements = elements or self.elements.values() + for element in elements: label = get_label(element=element) if label: - element._derived["label"] = label + element._label = label def _add_owned(self): """Adds owned elements, relationships, and metatypes to the model""" @@ -186,7 +247,7 @@ def _add_relationships(self): for relationship in self.all_relationships.values(): endpoints = { endpoint_type: [ - self.elements[endpoint["@id"]] + self.get_element(endpoint["@id"]) for endpoint in relationship._data[endpoint_type] ] for endpoint_type in ("source", "target") @@ -203,38 +264,48 @@ def _add_relationships(self): class Element: # pylint: disable=too-many-instance-attributes """A SysML v2 Element""" - _data: dict + _data: Dict[str, Any] _model: Model _id: str = field(default_factory=lambda: str(uuid4())) + _label: str = None _metatype: str = "Element" _derived: Dict[str, List] = field(default_factory=lambda: defaultdict(list)) # TODO: replace this with instances sequences # _instances: List["Instance"] = field(default_factory=list) _is_abstract: bool = False + _is_proxy: bool = True _is_relationship: bool = False _package: "Element" = None # TODO: Add comparison to allow sorting of elements (e.g., by name and then by id) def __post_init__(self): - if not self._data: - self._is_proxy = True - else: + if not self._model._initializing: + self._model.elements[self._id] = self + if self._data: self.resolve() def resolve(self): + if not self._is_proxy: + return + + model = self._model if not self._data: - raise NotImplementedError("Need to add functionality to get data for the element") + if not model._api: + raise SystemError("Model must have an API to retrieve the data from!") + self._data = model._api.get_element_data(self._id) data = self._data self._id = data["@id"] self._metatype = data["@type"] self._is_abstract = bool(data.get("isAbstract")) - self._is_relationship = "relatedElement" in data + self._is_relationship = bool(data.get("relatedElement")) for key, items in data.items(): if key.startswith("owned") and isinstance(items, list): data[key] = ListOfNamedItems(items) + if not model._initializing: + self._model._add_element(self) self._is_proxy = False def __call__(self, *args, **kwargs): @@ -281,17 +352,22 @@ def __getitem__(self, key: str): if isinstance(item, (dict, str)): item = self.__safe_dereference(item) elif isinstance(item, (list, tuple, set)): - items = [self.__safe_dereference(subitem) for subitem in item] + items = [self.__safe_dereference(sub_item) for sub_item in item] return type(item)(items) return item def __hash__(self): - return hash(self._data["@id"]) + id_ = self._id + if id_ is None: + warn( + f"Element (oid={id(self)}, data={self._data}) has no '@id'! Generating one for it" + ) + id_ = self._data["@id"] = str(uuid4()) + return hash(id_) def __lt__(self, other): if isinstance(other, str): - if other in self._model.elements: - other = self._model.elements[other] + other = self._model.get_element(other, fail=False) or other if not isinstance(other, Element): raise ValueError(f"Cannot compare an element to {type(other)}") if self.get("name", None) and other.get("name", None): @@ -301,13 +377,9 @@ def __lt__(self, other): def __repr__(self): return self._model._naming.get_name(element=self) - @property - def is_proxy(self): - return not self._data - @property def label(self) -> str: - return self._derived.get("label") + return self._label @property def relationships(self) -> Dict[str, Any]: @@ -343,7 +415,7 @@ def get(self, key: str, default: Any = None) -> Any: return default def get_element(self, element_id) -> "Element": - return self._model.elements.get(element_id) + return self._model.get(element_id) def get_owner(self) -> "Element": data = self._data @@ -354,7 +426,7 @@ def get_owner(self) -> "Element": break if owner_id is None: return None - return self._model.elements[owner_id] + return self._model.get_element(owner_id) @staticmethod def new(data: dict, model: Model) -> "Element": @@ -367,7 +439,7 @@ def __safe_dereference(self, item): if len(item) > 1: warn(f"Found a reference with more than one entry: {item}") item = item["@id"] - return self._model.elements[item] + return self._model.get_element(item) except KeyError: return item diff --git a/src/pymbe/widget/client.py b/src/pymbe/widget/client.py index f7b6efa6..fb171ed1 100644 --- a/src/pymbe/widget/client.py +++ b/src/pymbe/widget/client.py @@ -1,27 +1,76 @@ +import json from collections import Counter import ipywidgets as ipyw import traitlets as trt +from wxyz.html import File, FileBox -from ..client import SysML2Client +from ..client import APIClient +from ..model import Model +from .core import BaseWidget -__all__ = ("SysML2ClientWidget",) +__all__ = ("APIClientWidget", "FileLoader") @ipyw.register -class SysML2ClientWidget(SysML2Client, ipyw.GridspecLayout): +class FileLoader(FileBox, BaseWidget): + """A simple UI for loading SysML models from disk.""" + + closable: bool = trt.Bool(True).tag(sync=True) + description: str = trt.Unicode("File Loader").tag(sync=True) + icon_class: str = trt.Unicode("jp-JsonIcon").tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.accept = ["json"] + self.multiple = False + + def update(self, *_): + self.children = [] + + def _load_model(self, change: trt.Bunch): + if not change.new: + return + with self.log_out: + model_file, *other = self.children + assert not other, f"Should only have one file, but also found {other}" + self.model = Model.load( + elements=json.loads(change.new), + name=".".join(model_file.name.split(".")[:-1]), + ) + + @trt.observe("children") + def _update_model(self, change: trt.Bunch): + with self.log_out: + if isinstance(change.old, (list, tuple)): + for old in change.old: + if isinstance(old, File): + old.unobserve_all() + if isinstance(change.new, (list, tuple)) and change.new: + new, *other = change.new + assert not other, f"Should only have one file, but also found {other}" + new.observe(self._load_model, "value") + + +@ipyw.register +class APIClientWidget(APIClient, ipyw.GridspecLayout): """An ipywidget to interact with a SysML v2 API.""" + closable: bool = trt.Bool(True).tag(sync=True) description: str = trt.Unicode("API Client").tag(sync=True) icon_class: str = trt.Unicode("jp-DownloadIcon").tag(sync=True) + model: Model = trt.Instance(Model, allow_none=True) + # file_uploader: ipyw.FileUpload = trt.Instance(ipyw.FileUpload) host_url_input: ipyw.Text = trt.Instance(ipyw.Text) host_port_input: ipyw.IntText = trt.Instance(ipyw.IntText) project_selector: ipyw.Dropdown = trt.Instance(ipyw.Dropdown) commit_selector: ipyw.Dropdown = trt.Instance(ipyw.Dropdown) - download_elements: ipyw.Button = trt.Instance(ipyw.Button) - progress_bar: ipyw.IntProgress = trt.Instance(ipyw.IntProgress) + download_model: ipyw.Button = trt.Instance(ipyw.Button) + progress_bar: ipyw.FloatProgress = trt.Instance(ipyw.FloatProgress) + + log_out: ipyw.Output = trt.Instance(ipyw.Output, args=()) def __init__(self, n_rows=4, n_columns=12, **kwargs): super().__init__(n_rows=n_rows, n_columns=n_columns, **kwargs) @@ -40,7 +89,7 @@ def _set_children(self, proposal): self.host_port_input, self.project_selector, self.commit_selector, - self.download_elements, + self.download_model, self.progress_bar, ] @@ -54,7 +103,7 @@ def _set_layout(self): self[0, idx:] = self.host_port_input self[1, :idx] = self.project_selector self[2, :idx] = self.commit_selector - self[1:3, idx:] = self.download_elements + self[1:3, idx:] = self.download_model self[3, :] = self.progress_bar for widget in ( @@ -62,7 +111,7 @@ def _set_layout(self): self.host_port_input, self.project_selector, self.commit_selector, - self.download_elements, + self.download_model, self.progress_bar, ): widget.layout.height = "95%" @@ -77,7 +126,7 @@ def _set_layout(self): self.layout = layout @trt.default("host_url_input") - def _make_host_url_input(self): + def _make_host_url_input(self) -> ipyw.Text: input_box = ipyw.Text( default_value=self.host_url, description="Server:", @@ -90,7 +139,7 @@ def _make_host_url_input(self): return input_box @trt.default("host_port_input") - def _make_host_port_input(self): + def _make_host_port_input(self) -> ipyw.IntText: input_box = ipyw.IntText( default_value=self.host_port, min=1, @@ -104,7 +153,7 @@ def _make_host_port_input(self): return input_box @trt.default("project_selector") - def _make_project_selector(self): + def _make_project_selector(self) -> ipyw.Dropdown: selector = ipyw.Dropdown( description="Project:", options=self._get_project_options(), @@ -113,7 +162,7 @@ def _make_project_selector(self): return selector @trt.default("commit_selector") - def _make_commit_selector(self): + def _make_commit_selector(self) -> ipyw.Dropdown: selector = ipyw.Dropdown( description="Commit:", options=self._get_commit_selector_options() if self.project_selector.options else {}, @@ -121,27 +170,27 @@ def _make_commit_selector(self): trt.link((selector, "value"), (self, "selected_commit")) return selector - @trt.default("download_elements") - def _make_download_elements_button(self): + @trt.default("download_model") + def _make_download_model_button(self) -> ipyw.Button: button = ipyw.Button( icon="cloud-download", tooltip="Fetch elements from remote host.", layout=dict(max_width="6rem"), ) - button.on_click(self._download_elements) + button.on_click(self._download_model) return button @trt.default("progress_bar") - def _make_progress_bar(self): - progress_bar = ipyw.IntProgress( + def _make_progress_bar(self) -> ipyw.FloatProgress: + progress_bar = ipyw.FloatProgress( description="Loading:", min=0, - max=4, - step=1, + max=1, value=0, ) progress_bar.style.bar_color = "gray" progress_bar.layout.visibility = "hidden" + progress_bar.description_tooltip = "Loading model..." return progress_bar @trt.observe("projects") @@ -159,18 +208,41 @@ def _get_commit_selector_options(self): for id_, data in self._get_project_commits().items() } - def _download_elements(self, *_): - progress = self.progress_bar - progress.value = 0 - progress.layout.visibility = "visible" + def _download_model(self, *_): + with self.log_out: + progress = self.progress_bar + + progress.style.bar_color = "gray" + progress.value = 0 + progress.layout.visibility = "visible" + + progress.value += 0.05 + + step_per_element = 0.0007 + data = dict(page=0) + + def on_page(): + data["page"] += 1 + page = data["page"] + elements = page * self.page_size + if (elements * step_per_element) > (0.75 * progress.max): + progress.max *= 1.5 + progress.description_tooltip = f"Retrieved page {page} ({elements} model elements)" + progress.value += self.page_size * step_per_element + + progress.description_tooltip = "Downloading model..." + model = self.get_model(on_page=on_page) - progress.value += 1 + if not model or not model.elements: + progress.description_tooltip = "Failed to download the model..." + progress.style = "danger" + raise ValueError(f"Could not download model from: {self.elements_url}") - super()._download_elements() - progress.value += 1 + progress.description_tooltip = "Finished downloading the model..." + self.model = model - progress.value = progress.max - progress.layout.visibility = "hidden" + progress.value = progress.max + progress.layout.visibility = "hidden" def _get_project_options(self): project_name_instances = Counter(project["name"] for project in self.projects.values()) @@ -178,12 +250,12 @@ def _get_project_options(self): return { data["name"] + ( - f""" ({data["created"].strftime("%Y-%m-%d %H:%M:%S")})""" + f""" ({data["created"].strftime("%Y-%m-%d %H:%M:%S")} UTC)""" if project_name_instances[data["name"]] > 1 else "" ): id_ for id_, data in sorted( self.projects.items(), - key=lambda x: x[1]["name"], + key=lambda x: (x[1]["name"], x[1]["created"]), ) } diff --git a/src/pymbe/widget/containment.py b/src/pymbe/widget/containment.py index d6e60905..3e7ee8e7 100644 --- a/src/pymbe/widget/containment.py +++ b/src/pymbe/widget/containment.py @@ -1,18 +1,18 @@ import asyncio -import json import typing as ty from pathlib import Path import ipytree as ipyt import ipywidgets as ipyw import traitlets as trt -from wxyz.html import File, FileBox from wxyz.lab import DockPop from ..model import Element, Model -from .client import SysML2ClientWidget +from .client import APIClientWidget, FileLoader from .core import BaseWidget +__all__ = ("ContainmentTree",) + class ElementNode(ipyt.Node): """A project element node compatible with ipytree.""" @@ -29,37 +29,6 @@ def __init__(self, *args, **kwargs): self.open_icon = "caret-right" -@ipyw.register -class SysML2FileLoader(FileBox, BaseWidget): - """A simple UI for loading SysML models from disk.""" - - description: str = trt.Unicode("File Loader").tag(sync=True) - icon_class: str = trt.Unicode("jp-JsonIcon").tag(sync=True) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.accept = ["json"] - self.multiple = False - - def update(self, *_): - self.children = [] - - def _load_model(self, change: trt.Bunch): - with self.log_out: - self.model = Model.load(json.loads(change.new)) - - @trt.observe("children") - def _update_model(self, change: trt.Bunch): - with self.log_out: - if isinstance(change.old, (list, tuple)) and change.old: - old, *_ = change.old - if isinstance(old, File): - old.unobserve(self._load_model) - if isinstance(change.new, (list, tuple)) and change.new: - new, *_ = change.new - new.observe(self._load_model, "value") - - @ipyw.register class ContainmentTree(ipyw.VBox, BaseWidget): """A widget to explore the structure and data in a project.""" @@ -67,8 +36,8 @@ class ContainmentTree(ipyw.VBox, BaseWidget): description: str = trt.Unicode("Containment Tree").tag(sync=True) icon_class: str = trt.Unicode("jp-TreeViewIcon").tag(sync=True) - client: SysML2ClientWidget = trt.Instance(SysML2ClientWidget) - file_loader: SysML2FileLoader = trt.Instance(SysML2FileLoader, args=()) + api_client: APIClientWidget = trt.Instance(APIClientWidget) + file_loader: FileLoader = trt.Instance(FileLoader, args=()) default_icon: str = trt.Unicode("genderless").tag(sync=True) indeterminate_icon: str = trt.Unicode("question").tag(sync=True) @@ -81,7 +50,7 @@ class ContainmentTree(ipyw.VBox, BaseWidget): kw=dict( icon="folder-open", layout=dict(width="40px"), - tooltip="Load from file", + tooltip="Launch file loader for SysML v2 models", ), ) launch_api: ipyw.Button = trt.Instance( @@ -89,7 +58,7 @@ class ContainmentTree(ipyw.VBox, BaseWidget): kw=dict( icon="cloud-download-alt", layout=dict(width="40px"), - tooltip="Launch SysML Rest API client", + tooltip="Launch client for Pilot Implementation of the SysMLv2 ReST API", ), ) pop_log: ipyw.Button = trt.Instance( @@ -116,8 +85,8 @@ class ContainmentTree(ipyw.VBox, BaseWidget): ActionUsage="copy", AttributeUsage="copy", # "underline" Expression="code", - Feature="terminal", # "pencil-alt", - Function="square-root-alt", + Feature="terminal", # "pencil-alt" + Function="calculator", # "square-root-alt" InvocationExpression="comment-alt", ItemDefinition="file-invoice", # info LiteralInteger="quote-right", @@ -150,14 +119,14 @@ def __init__(self, *args, **kwargs): self.save_model.on_click(self._save_to_disk) for linked_attribute in ("model", "log_out"): - for widget in (self.client, self.file_loader): + for widget in (self.api_client, self.file_loader): trt.link((self, linked_attribute), (widget, linked_attribute)) - @trt.default("client") - def _make_client(self) -> SysML2ClientWidget: - client = SysML2ClientWidget(host_url="http://sysml2.intercax.com") - client._set_layout() - return client + @trt.default("api_client") + def _make_api_client(self) -> APIClientWidget: + api_client = APIClientWidget(host_url="http://sysml2.intercax.com") + api_client._set_layout() + return api_client @trt.default("add_widget") def _make_add_widget(self) -> ty.Callable: @@ -179,17 +148,11 @@ def _save_to_disk(self, *_): if not self.model: print("No model loaded!") return - - filepath = Path(".") / f"{self.model.name}.json" - filepath = filepath.resolve().absolute() - if filepath.exists(): - print(f"Overwriting {filepath}") - self.model.save_to_file(filepath=filepath) - print(f"Saved model to '{filepath}'") + self.model.save_to_file(filepath=Path(".") / self.model.name) def _pop_api_client(self, *_): with self.log_out: - self.add_widget(self.client, mode="split-top") + self.add_widget(self.api_client, mode="split-top") def _pop_log_out(self, *_): with self.log_out: @@ -298,9 +261,12 @@ def update(self, change: trt.Bunch): return model_id = str(model.source) or "SYSML_MODEL" + model_name = model.name + if model.source: + model_name += f" ({model.source})" model_node = ElementNode( icon=self.icons_by_type["Model"], - name=model.name, + name=model_name, _data=dict(source=model.source), _identifier=model_id, _owner=None, diff --git a/src/pymbe/widget/core.py b/src/pymbe/widget/core.py index 60da539d..0da67010 100644 --- a/src/pymbe/widget/core.py +++ b/src/pymbe/widget/core.py @@ -10,6 +10,7 @@ class BaseWidget(ipyw.DOMWidget): """A base widget to enforce standardization with selectors.""" + closable: bool = trt.Bool(False).tag(sync=True) description = trt.Unicode("Unnamed Widget").tag(sync=True) model: Model = trt.Instance(Model, allow_none=True, help="The instance of the SysML model.") diff --git a/src/pymbe/widget/diagram/interpretation.py b/src/pymbe/widget/diagram/interpretation.py index 321be6e2..68119f7c 100644 --- a/src/pymbe/widget/diagram/interpretation.py +++ b/src/pymbe/widget/diagram/interpretation.py @@ -112,7 +112,7 @@ def update(self, *_): def _generate_random_interpretation(self, *_): with self.log_out: self.interpretation = random_generator_playbook( - lpg=self.lpg, + m1=self.lpg, filtered_feat_packages=self.package_selector.value, ) diff --git a/src/pymbe/widget/ui.py b/src/pymbe/widget/ui.py index 57de2c5e..38667b3a 100644 --- a/src/pymbe/widget/ui.py +++ b/src/pymbe/widget/ui.py @@ -32,7 +32,7 @@ def __init__(self, host_url, *args, **kwargs): super().__init__(*args, **kwargs) self.description = "SysML Model" - self.tree.client.host_url = host_url + self.tree.api_client.host_url = host_url self.children = [ self.tree, diff --git a/tests/client/test_client_loading.py b/tests/client/test_client_loading.py index 5027c04e..0cb3da59 100644 --- a/tests/client/test_client_loading.py +++ b/tests/client/test_client_loading.py @@ -1,7 +1,10 @@ +from warnings import warn + import pytest import requests -from tests.conftest import all_kerbal_names, kerbal_client, kerbal_ids_by_type +from pymbe.client import APIClient +from pymbe.model import Element SYSML_SERVER_URL = "http://sysml2-sst.intercax.com" # Alternative: sysml2.intercax.com @@ -10,12 +13,12 @@ def can_connect(host: str, port: int = 9000): try: requests.get(f"{host}:{port}") return True - except: + except: # pylint: disable=bare-except # noqa: E722 return False -def test_client_load_kerbal(kerbal_client): - assert len(kerbal_client.model.elements) == 380 +def test_client_load_kerbal(kerbal_model): + assert len(kerbal_model.elements) == 380 def test_client_load_find_names(all_kerbal_names): @@ -36,8 +39,8 @@ def test_client_load_find_types(kerbal_ids_by_type): not can_connect(host=SYSML_SERVER_URL), reason=f"Can't connect to {SYSML_SERVER_URL}", ) -def test_remote_connection(kerbal_client): - client = kerbal_client +def test_remote_connection(): + client = APIClient() client.host_url = SYSML_SERVER_URL assert client.projects @@ -46,7 +49,46 @@ def test_remote_connection(kerbal_client): client.selected_project = list(client.projects)[0] client.selected_commit = list(client._get_project_commits())[0] - client._download_elements() + model = client.get_model() - model = client.model assert model.elements + + # Test resolving an element from the API + element: Element = [ + el for el in model.ownedElement if el.get("name") and not el._is_relationship + ][0] + label = element.label + element._data = {} + element._derived = {} + element._is_proxy = True + model.ownedElement.remove(element) + model.ownedMetatype.pop(element._metatype) + model.all_non_relationships.pop(element._id) + model.elements.pop(element._id) + element.resolve() + assert label == element.label + assert element in model.ownedElement + assert element in model.ownedMetatype[element._metatype] + assert model.all_non_relationships[element._id] == element + assert model.elements[element._id] == element + + owned_relationships: Element = [rel for rel in model.ownedElement if rel._is_relationship] + if not owned_relationships: + warn("Model does not have any owned relationships!") + else: + relationship = owned_relationships[0] + label = relationship.label + relationship._data = {} + relationship._derived = {} + relationship._is_proxy = True + model.ownedElement.remove(relationship) + model.ownedRelationship.remove(relationship) + model.ownedMetatype[relationship._metatype].remove(relationship) + model.all_relationships.pop(relationship._id) + model.elements.pop(relationship._id) + relationship.resolve() + assert label == relationship.label + assert relationship in model.ownedElement + assert relationship in model.ownedMetatype[relationship._metatype] + assert model.all_relationships[relationship._id] == relationship + assert model.elements[relationship._id] == relationship diff --git a/tests/conftest.py b/tests/conftest.py index a945a5db..cd40c7ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,11 @@ +# pylint: disable=redefined-outer-name from pathlib import Path from typing import Dict import pytest -import traitlets as trt import pymbe import pymbe.api as pm -from pymbe.client import SysML2Client from pymbe.graph import SysML2LabeledPropertyGraph from pymbe.interpretation.interp_playbooks import ( build_expression_sequence_templates, @@ -27,7 +26,7 @@ FIXTURES = TESTS_ROOT / "fixtures" -def get_client(filename: str) -> SysML2Client: +def get_model(filename: str) -> pm.Model: if not filename.endswith(".json"): filename += ".json" @@ -46,61 +45,39 @@ def get_client(filename: str) -> SysML2Client: f"{FIXTURES} exists: {fixtures_exists}" + contents ) - helper_client = SysML2Client() - helper_client._load_from_file(json_file) - - return helper_client - - -def kerbal_model_loaded_client() -> SysML2Client: - return get_client("Kerbal") - - -def simple_parts_model_loaded_client() -> SysML2Client: - return get_client("Simple Parts Model") - - -def simple_actions_model_loaded_client() -> SysML2Client: - return get_client("Simple Actions Example") + return pm.Model.load_from_file(json_file) @pytest.fixture -def kerbal_client() -> SysML2Client: - return kerbal_model_loaded_client() +def kerbal_model() -> pm.Model: + return get_model("Kerbal") @pytest.fixture -def kerbal_ids_by_type(kerbal_client) -> dict: +def kerbal_ids_by_type(kerbal_model) -> dict: return { metatype: [element._id for element in elements] - for metatype, elements in kerbal_client.model.ownedMetatype.items() + for metatype, elements in kerbal_model.ownedMetatype.items() } @pytest.fixture -def kerbal_stable_names(): - client = kerbal_model_loaded_client() - lpg = SysML2LabeledPropertyGraph() - lpg.model = client.model - return build_stable_id_lookups(lpg) +def kerbal_stable_names(kerbal_model): + return build_stable_id_lookups(SysML2LabeledPropertyGraph(model=kerbal_model)) @pytest.fixture() -def all_kerbal_names(kerbal_client) -> list: - all_elements = kerbal_client.model.elements - - return [element._data["name"] for element in all_elements.values() if "name" in element._data] +def all_kerbal_names(kerbal_model) -> list: + return [ + element._data["name"] + for element in kerbal_model.elements.values() + if "name" in element._data + ] @pytest.fixture -def kerbal_lpg() -> SysML2LabeledPropertyGraph: - new_lpg = SysML2LabeledPropertyGraph() - client = kerbal_model_loaded_client() - trt.link( - (client, "model"), - (new_lpg, "model"), - ) - return new_lpg +def kerbal_lpg(kerbal_model) -> SysML2LabeledPropertyGraph: + return SysML2LabeledPropertyGraph(model=kerbal_model) @pytest.fixture @@ -186,26 +163,18 @@ def kerbal_random_stage_5_complete( @pytest.fixture -def simple_parts_client() -> SysML2Client: - return simple_parts_model_loaded_client() +def simple_parts_model() -> pm.Model: + return get_model("Simple Parts Model") @pytest.fixture -def simple_parts_lpg() -> SysML2LabeledPropertyGraph: - new_lpg = SysML2LabeledPropertyGraph() - client = simple_parts_model_loaded_client() - - new_lpg.model = client.model - - return new_lpg +def simple_parts_lpg(simple_parts_model) -> SysML2LabeledPropertyGraph: + return SysML2LabeledPropertyGraph(model=simple_parts_model) @pytest.fixture -def simple_parts_stable_names(): - client = simple_parts_model_loaded_client() - lpg = SysML2LabeledPropertyGraph() - lpg.model = client.model - return build_stable_id_lookups(lpg) +def simple_parts_stable_names(simple_parts_model): + return build_stable_id_lookups(SysML2LabeledPropertyGraph(model=simple_parts_model)) @pytest.fixture @@ -270,23 +239,13 @@ def simple_parts_random_stage_3_complete( @pytest.fixture -def simple_actions_client() -> SysML2Client: - return simple_actions_model_loaded_client() +def simple_actions_model() -> pm.Model: + return get_model("Simple Action Example") @pytest.fixture -def simple_actions_lpg() -> SysML2LabeledPropertyGraph: - new_lpg = SysML2LabeledPropertyGraph() - client = simple_actions_model_loaded_client() - - new_lpg.model = client.model - - return new_lpg - - -@pytest.fixture -def kerbal_model() -> pm.Model: - return pm.Model.load_from_file(FIXTURES / "Kerbal.json") +def simple_actions_lpg(simple_actions_model) -> SysML2LabeledPropertyGraph: + return SysML2LabeledPropertyGraph(model=simple_actions_model) @pytest.fixture @@ -298,11 +257,8 @@ def all_models() -> Dict[Path, pm.Model]: @pytest.fixture -def simple_actions_stable_names(): - client = simple_actions_model_loaded_client() - lpg = SysML2LabeledPropertyGraph() - lpg.model = client.model - return build_stable_id_lookups(lpg) +def simple_actions_stable_names(simple_actions_model): + return build_stable_id_lookups(SysML2LabeledPropertyGraph(simple_actions_model)) @pytest.fixture diff --git a/tests/fixtures b/tests/fixtures index f9ac09b5..4adddf3f 160000 --- a/tests/fixtures +++ b/tests/fixtures @@ -1 +1 @@ -Subproject commit f9ac09b5936b47359740600f0c7ae45e13e1e19b +Subproject commit 4adddf3f6d6ed693fddec955e3f4b938b78bfe73 diff --git a/tests/graph/test_graph_load.py b/tests/graph/test_graph_load.py index f8465053..56d0088a 100644 --- a/tests/graph/test_graph_load.py +++ b/tests/graph/test_graph_load.py @@ -1,8 +1,6 @@ import networkx as nx import pytest -from tests.conftest import kerbal_ids_by_type, kerbal_lpg, kerbal_model_loaded_client - def test_graph_load(kerbal_lpg): assert len(kerbal_lpg.nodes) == 152 @@ -11,7 +9,7 @@ def test_graph_load(kerbal_lpg): ) # nodes + edges should be all elements from the client -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_graph_projection_part_def_node_filter(kerbal_lpg, kerbal_ids_by_type): pdg = kerbal_lpg.get_projection("Part Definition") @@ -27,7 +25,7 @@ def test_graph_projection_part_def_node_filter(kerbal_lpg, kerbal_ids_by_type): assert len(pdg.nodes) == len(kerbal_ids_by_type["PartDefinition"]) - 1 -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_graph_projection_part_def_components(kerbal_lpg): pdg = kerbal_lpg.get_projection("Part Definition") @@ -46,4 +44,4 @@ def test_graph_projection_part_def_edges(kerbal_lpg): # check that nothing but Superclassing edges are allowed by the filter pdg = kerbal_lpg.get_projection("Part Definition") - assert all([edge[2] == "Superclassing^-1" for edge in pdg.edges]) + assert all(edge[2] == "Superclassing^-1" for edge in pdg.edges) diff --git a/tests/graph/test_graph_solve.py b/tests/graph/test_graph_solve.py index 88625f07..9556d98d 100644 --- a/tests/graph/test_graph_solve.py +++ b/tests/graph/test_graph_solve.py @@ -13,14 +13,15 @@ logger = logging.getLogger(__name__) -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_basic_kerbal_solve(kerbal_lpg, kerbal_random_stage_5_complete, kerbal_stable_names): # check that literal assignments go correctly *_, qualified_name_to_id = kerbal_stable_names rt_10_isp_id = qualified_name_to_id[ - f"""{PARTS_LIBRARY}RT-10 "Hammer" Solid Fuel Booster::Specific Impulse: Real <>""" + f"""{PARTS_LIBRARY}RT-10 "Hammer" Solid Fuel Booster""" + "::Specific Impulse: Real <>" ] ft200_full_mass = qualified_name_to_id[ f"{PARTS_LIBRARY}FL-T200 Fuel Tank::Full Mass: Real <>" @@ -48,7 +49,7 @@ def test_basic_kerbal_solve(kerbal_lpg, kerbal_random_stage_5_complete, kerbal_s try: cg1.solve_graph(kerbal_lpg) - except: + except: # pylint: disable=bare-except pass for move in literal_output_moves: @@ -74,10 +75,14 @@ def test_path_step_expression_kerbal_solve( id_to_qualified_name, qualified_name_to_id = kerbal_stable_names + assert id_to_qualified_name, "Could not find ID to qualified name mapping" + pse_engine_mass = qualified_name_to_id[ f"""{PARTS_LIBRARY}RT-10 "Hammer" Solid Fuel Booster::Specific Impulse: Real <>""" ] + assert pse_engine_mass, "No PSE mass!" + # incrementally step through the calculations and check progress dcg = generate_execution_order(kerbal_lpg) diff --git a/tests/graph/test_lpg.py b/tests/graph/test_lpg.py index d9d61f34..bd7599d4 100644 --- a/tests/graph/test_lpg.py +++ b/tests/graph/test_lpg.py @@ -1,10 +1,8 @@ import warnings -import networkx as nx import pytest from pymbe.graph import SysML2LabeledPropertyGraph -from tests.conftest import kerbal_model SEED_NODE_NAMES = ("FL-T100 Fuel Tank", """RT-5 "Flea" Solid Fuel Booster""") diff --git a/tests/interpretation/circuit_example/__init__.py b/tests/interpretation/circuit_example/__init__.py new file mode 100644 index 00000000..02f7f2af --- /dev/null +++ b/tests/interpretation/circuit_example/__init__.py @@ -0,0 +1,4 @@ +from .openmdao import CircuitComponent +from .ui import CircuitUI + +__all__ = ("CircuitUI", "CircuitComponent") diff --git a/tests/interpretation/circuit_example/generator.py b/tests/interpretation/circuit_example/generator.py new file mode 100644 index 00000000..1abf42d3 --- /dev/null +++ b/tests/interpretation/circuit_example/generator.py @@ -0,0 +1,176 @@ +import traceback +import typing as ty + +import networkx as nx + +import pymbe.api as pm +from pymbe.interpretation.interp_playbooks import random_generator_playbook +from pymbe.model import Element + +from .openmdao import execute_interpretation + +__all__ = ("get_circuit_data",) + + +def get_unique_connections(instances, feature): + source_feat, target_feat = feature + m0_connector_ends = [ + (tuple(source), tuple(target)) + for source, target in zip(instances[source_feat._id], instances[target_feat._id]) + ] + + m0_connector_ends = tuple( + {(source, target) for source, target in m0_connector_ends if source[:-1] != target[:-1]} + ) + + unique_connections = { + (source[-2:], target[-2:]): (source, target) for source, target in m0_connector_ends + } + return tuple((source, target) for source, target in unique_connections.values()) + # return tuple((source, target) for source, target in m0_connector_ends) + + +def get_circuit_data(interpretation: dict, connector_feature: Element): + """ + A very rudimentary filter for simple circuit connectors. + + Filters out self-connections and duplicate connectors between the same m0 pins. + """ + unique_connections = get_unique_connections(interpretation, connector_feature) + circuit_data = dict(interpretation=interpretation) + + def get_el_name(element): + name = element.name + name, num = name.split("#") + name = name if name == "EMF" else name[0] + return name + num + + def legal_predecessors(graph, node, valids): + all_pred = set(graph.predecessors(node)) + return {pred for pred in all_pred if pred in valids} + + def legal_successors(graph, node, valids): + all_suc = set(graph.successors(node)) + return {pred for pred in all_suc if pred in valids} + + graph = nx.DiGraph() + edges = [ + ((get_el_name(source[-2]), "Pos"), (get_el_name(target[-2]), "Neg")) + for source, target in unique_connections + ] + nodes = {node for node, _ in sum(map(list, edges), [])} + edges += [((node, "Neg"), (node, "Pos")) for node in nodes if not node.startswith("EMF")] + graph.add_edges_from(edges) + + emf_nodes = {node for node in nodes if node.upper().startswith("EMF")} + if not emf_nodes: + raise RuntimeError("There are not EMF nodes in the graph!") + + if len(emf_nodes) > 1: + raise RuntimeError( + f"Found {len(emf_nodes)} EMF nodes ({emf_nodes})! There should only be one!" + ) + emf_node = emf_nodes.pop() + emf_pos_name = (emf_node, "Pos") + emf_neg_name = (emf_node, "Neg") + + # find all paths in the graph from EMF positive side to EMF negative side + + flows = list(nx.all_simple_paths(graph, emf_pos_name, emf_neg_name)) + flow_edges = list(nx.all_simple_edge_paths(graph, emf_pos_name, emf_neg_name)) + + flowed_nodes = {node for flow in flows for node in flow} + + circuit_data["valid_edges"] = set(sum(flow_edges, [])) + + valid_graph = nx.DiGraph() + valid_graph.add_edges_from(circuit_data["valid_edges"]) + + # look at junctions in the graph + + # split junctions - where a positive pin has multiple outputs + circuit_data["split_junctions"] = { + node: legal_successors(valid_graph, node, flowed_nodes) + for node in graph.nodes + if len(legal_successors(valid_graph, node, flowed_nodes)) > 1 + } + + # join junctions - where a negative pin has multiple inputs + circuit_data["join_junctions"] = { + node: legal_predecessors(valid_graph, node, flowed_nodes) + for node in graph.nodes + if len(legal_predecessors(valid_graph, node, flowed_nodes)) > 1 + } + + circuit_data["both_ways_junctions"] = { + node + for node in graph.nodes + if len(legal_successors(valid_graph, node, flowed_nodes)) > 1 + if node in sum(map(list, circuit_data["join_junctions"].values()), []) + } + + circuit_data["number_loops"] = len(flows) + circuit_data["valid_nodes"] = flowed_nodes + + circuit_data["loop_edges"] = flow_edges + + circuit_data["cleaned_graph"] = valid_graph + circuit_data["plain_graph"] = graph + + # EMF positive is always in loop #1 + circuit_data["emf_pins"] = {"+": emf_pos_name, "-": emf_neg_name} + + # compute the independent loops on the graph + circuit_data["loop_unique_edges"] = [] + if circuit_data["number_loops"] > 1: + encountered_edges = set() + for indx in range(circuit_data["number_loops"]): + new_edges = [] + for edg in flow_edges[indx]: + if edg not in encountered_edges: + new_edges.append(edg) + encountered_edges.add(edg) + circuit_data["loop_unique_edges"].append(new_edges) + else: + circuit_data["loop_unique_edges"] = list(flow_edges) + + return circuit_data + + +def make_circuit_interpretations( + m1_model: pm.Model, + required_interpretations: int = 1, + max_tries: int = 20, + print_exceptions: bool = True, + on_attempt: ty.Callable = None, +) -> tuple: + """Returns a tuple of data on valid randomly generated M0 interpretations.""" + num_tries = 0 + interpretations = [] + circuit_pkg = m1_model.ownedElement["Circuit Builder"] + connector_feature = circuit_pkg.ownedElement["Circuit"].ownedMember["Part to Part"].endFeature + while len(interpretations) < required_interpretations and num_tries < max_tries: + num_tries += 1 + if on_attempt is not None: + on_attempt() + try: + interpretation = random_generator_playbook( + m1=m1_model, + filtered_feat_packages=[circuit_pkg], + ) + interpretation_data = get_circuit_data(interpretation, connector_feature) + graph: nx.DiGraph = interpretation_data["cleaned_graph"] + assert len(graph.nodes) > 0, "Graph has no nodes!" + assert len(graph.edges) > 0, "Graph has no edges!" + interpretation_data["problem"] = execute_interpretation( + interpretation_data=interpretation_data, + print_exceptions=print_exceptions, + ) + interpretations += [interpretation_data] + except: # pylint: disable=bare-except # noqa: E722 + if print_exceptions: + print( + f">>> Failed to generate interpretation try {num_tries}!\n", + f"\n\tTRACEBACK:\n\t{traceback.format_exc()}", + ) + return tuple(interpretations) diff --git a/tests/interpretation/circuit_example/openmdao.py b/tests/interpretation/circuit_example/openmdao.py new file mode 100644 index 00000000..3771409a --- /dev/null +++ b/tests/interpretation/circuit_example/openmdao.py @@ -0,0 +1,195 @@ +import traceback +from importlib import import_module + +import networkx as nx + +import openmdao.api as om + +__all__ = ( + "CircuitComponent", + "execute_interpretation", +) + + +def baseline_openmdao_example_circuit() -> dict: + edges = ( + (("EMF", "Pos"), ("R1", "Neg")), + (("EMF", "Pos"), ("R2", "Neg")), + (("R1", "Pos"), ("EMF", "Neg")), + (("R2", "Pos"), ("D1", "Neg")), + (("D1", "Pos"), ("EMF", "Neg")), + ) + baseline_digraph = nx.DiGraph() + baseline_digraph.add_edges_from(edges) + return dict( + cleaned_graph=baseline_digraph, + emf_pins={"+": ("EMF", "Pos"), "-": ("EMF", "Neg")}, + params=dict(R1=dict(R=100), R2=dict(R=10000)), + ) + + +def load_class(class_path: str) -> type: + *module_path, class_name = class_path.split(".") + module = import_module(".".join(module_path)) + return getattr(module, class_name) + + +class CircuitComponent(om.Group): + """An OpenMDAO Circuit for circuits of Diodes and Resistors.""" + + # FIXME: this should be retrieved from the SysML model + OM_COMPONENTS = { + class_path.rsplit(".", 1)[-1][0]: load_class(class_path) + for class_path in ( + "openmdao.test_suite.test_examples.test_circuit_analysis.Diode", + "openmdao.test_suite.test_examples.test_circuit_analysis.Resistor", + "openmdao.test_suite.test_examples.test_circuit_analysis.Node", + ) + } + + def initialize(self): + self.options.declare("interpretation_data", types=dict) + self.options.declare("print_exceptions", False, types=bool) + self.options.declare("run_baseline", False, types=bool) + self.options.declare("electric_params", {}, types=dict) + + def setup(self): + print_exceptions = self.options["print_exceptions"] + + if self.options["run_baseline"]: + data = baseline_openmdao_example_circuit() + electric_params = data["electric_params"] + else: + data = self.options["interpretation_data"] + electric_params = self.options["electric_params"] + + digraph: nx.DiGraph = data["cleaned_graph"] + + # V, *_ = next(digraph.successors(data["emf_pins"]["+"])) + grounded = [el for el, *_ in digraph.predecessors(data["emf_pins"]["-"])] + + elements = { + element for element, polarity in digraph.nodes if not element.upper().startswith("EMF") + } + for element in elements: + comp_cls = self.OM_COMPONENTS.get(element[0]) + if comp_cls is None: + if print_exceptions: + print(f"{element} doesn't have class!") + continue + kwargs = {} + params = electric_params.get(element, {}) + if params: + if print_exceptions: + print(f"Setting params={params} for {element}") + if element in grounded: + kwargs["promotes_inputs"] = [("V_out", "Vg")] + self.add_subsystem(f"{element}", comp_cls(**params), **kwargs) + + self._add_nodes(digraph=digraph, emf_pins=data["emf_pins"]) + + self.nonlinear_solver = om.NewtonSolver() + self.linear_solver = om.DirectSolver() + + self.nonlinear_solver.options["iprint"] = 2 if print_exceptions else 0 + self.nonlinear_solver.options["maxiter"] = 10 + self.nonlinear_solver.options["solve_subsystems"] = True + self.nonlinear_solver.linesearch = om.ArmijoGoldsteinLS() + self.nonlinear_solver.linesearch.options["maxiter"] = 10 + self.nonlinear_solver.linesearch.options["iprint"] = 2 + + return True + + def _add_nodes(self, digraph: nx.DiGraph, emf_pins: dict, print_exceptions: bool = False): + connectors = nx.DiGraph() + connectors.add_edges_from([(src, tgt) for src, tgt in digraph.edges if src[0] != tgt[0]]) + self.node_names = node_names = [] + for node_id, comp in enumerate(nx.connected_components(connectors.to_undirected())): + node_name = f"node_{node_id}" + + if emf_pins["-"] in comp: + if print_exceptions: + print( + f" > Not adding '{node_name}' because it is connected to" + f" the ground ({emf_pins['-']})" + ) + continue + node_names += [node_name] + + has_pos_emf = emf_pins["+"] in comp + if has_pos_emf: + self.source_node = node_name + + n_in = sum(1 for node in comp if node[1] == "Pos") + n_out = sum(1 for node in comp if node[1] == "Neg") + + kwargs = dict(promotes_inputs=[("I_in:0", "I_in")]) if has_pos_emf else {} + self.add_subsystem( + node_name, + self.OM_COMPONENTS["N"](n_in=n_in, n_out=n_out), + **kwargs, + ) + if print_exceptions: + print(f" > Adding '{node_name}' with {n_in} inputs and {n_out} outputs") + indeces = {"in": 1 * has_pos_emf, "out": 0} + elec_volt_pins = [] + for element, polarity in comp: + if element.startswith("EMF"): + continue + elem_dir = "out" if polarity == "Pos" else "in" + node_dir = "out" if elem_dir == "in" else "in" + elec_volt_pins += [f"{element}.V_{elem_dir}"] + + node_current = f"{node_name}.I_{node_dir}:{indeces[node_dir]}" + self.connect(f"{element}.I", node_current) + if print_exceptions: + print(f" > Connecting currents: {element}.I --> {node_current}") + indeces[node_dir] += 1 + if elec_volt_pins: + try: + self.connect(f"{node_name}.V", elec_volt_pins) + if print_exceptions: + print( + f" > Connecting voltages for node {node_name}.V --> {elec_volt_pins}" + ) + except: # pylint: disable=bare-except # noqa: E722 + if print_exceptions: + print(f" ! Could not connect: {node_name}.V --> {elec_volt_pins}") + + +def execute_interpretation(interpretation_data: dict, print_exceptions=False): + try: + problem = interpretation_data.get("problem") + if problem is None: + circuit_name = "circuit" + problem = om.Problem() + problem.model.add_subsystem( + circuit_name, + CircuitComponent( + interpretation_data=interpretation_data, print_exceptions=print_exceptions + ), + ) + is_valid = problem.setup() + if not is_valid: + raise ValueError(" ! Interpretation does not produce a valid circuit!") + if print_exceptions: + print(" >> Successfully set up interpretation") + try: + circuit = getattr(problem.model, circuit_name) + problem.set_val(f"{circuit_name}.I_in", 0.1) + problem.set_val(f"{circuit_name}.Vg", 0) + for node_name in circuit.node_names: + problem.set_val( + f"{circuit_name}.{node_name}.V", + (10.0 if node_name == circuit.source_node else 0.1), + ) + problem.run_model() + if print_exceptions: + print(" + Successfully ran interpretation!\n") + except Exception: # pylint: disable=broad-except + if print_exceptions: + print(f" ! Failed to run interpretation!\n\n{traceback.format_exc()}\n") + except Exception: # pylint: disable=broad-except + if print_exceptions: + print(f" ! Failed to setup interpretation!\n\n{traceback.format_exc()}\n") + return problem diff --git a/tests/interpretation/circuit_example/ui.py b/tests/interpretation/circuit_example/ui.py new file mode 100644 index 00000000..aa184e5d --- /dev/null +++ b/tests/interpretation/circuit_example/ui.py @@ -0,0 +1,669 @@ +import typing as ty +from pathlib import Path +from warnings import warn + +import ipywidgets as ipyw +import matplotlib.pyplot as plt +import networkx as nx +import traitlets as trt +from IPython.display import HTML, IFrame, display +from ipywidgets.widgets.trait_types import TypedTuple +from wxyz.lab import DockBox, DockPop + +import openmdao.api as om +import pymbe.api as pm + +from .generator import make_circuit_interpretations + +__all__ = ( + "CircuitUI", + "draw_circuit", + "make_circuit_interpretations", + "update_multiplicities", +) + +# The valid graph layouts available in networkx +NX_LAYOUTS = sorted( + [ + nx_graph_layout.replace("_layout", "") + for nx_graph_layout in dir(nx.layout) + if nx_graph_layout.endswith("_layout") + and not any(bad_stuff in nx_graph_layout for bad_stuff in ("partite", "rescale", "planar")) + ] +) + +DISCRETE_COLOR_SCALE = ( + "#0077bb", + "#33bbee", + "#009988", + "#ee7733", + "#cc3311", + "#ee3377", + "#bbbbbb", +) + +NODE_COLORS = { + "R": "#8888DD", + "D": "#A020F0", + "EMF": "#DD8888", +} + + +def update_multiplicities( + circuit: pm.Model, + num_resistors: ty.Tuple[int, int] = None, + num_diodes: ty.Tuple[int, int] = None, + num_connectors: ty.Tuple[int, int] = None, +) -> pm.Model: + num_resistors = tuple(sorted(num_resistors or [])) or (3, 8) + num_diodes = tuple(sorted(num_diodes or [])) or (1, 4) + min_connectors = num_resistors[1] + num_diodes[1] + num_connectors = tuple(sorted(num_connectors or [])) or (min_connectors, 2 * min_connectors) + + circuit_def = circuit.ownedElement["Circuit Builder"].ownedElement["Circuit"] + multiplicity = circuit_def.ownedMember["Circuit Resistor"].multiplicity + multiplicity.lowerBound._data["value"], multiplicity.upperBound._data["value"] = num_resistors + + multiplicity = circuit_def.ownedMember["Circuit Diode"].multiplicity + multiplicity.lowerBound._data["value"], multiplicity.upperBound._data["value"] = num_diodes + + # Get the ConnectionUsage + for member in circuit_def.ownedMember: + if member._metatype != "ConnectionUsage": + continue + ( + member.multiplicity.lowerBound._data["value"], + member.multiplicity.upperBound._data["value"], + ) = num_connectors + return circuit + + +def draw_circuit( + circuit_graph: nx.DiGraph, + figsize=None, + rad=0.1, + arrowsize=40, + linewidth=2, + layout="kamada_kawai", +): + figsize = figsize or (20, 20) + + layout_algorithm = getattr(nx.layout, f"{layout}_layout") + node_pos = layout_algorithm(circuit_graph) + + internal_edges = {(n1, n2) for n1, n2 in circuit_graph.edges if n1[0] == n2[0]} + + nodes = {node for node, _ in sum(map(list, circuit_graph.edges), [])} + + emf_nodes = [node for node in nodes if node.startswith("EMF")] + if not emf_nodes: + raise ValueError("No EMF node in graph!") + if len(emf_nodes) > 1: + warn(f"Found multiple EMF nodes ({emf_nodes}), will use {emf_nodes[0]}!") + emf = emf_nodes[0] + first_path, *other_paths = list( + nx.all_simple_edge_paths(circuit_graph, (emf, "Pos"), (emf, "Neg")) + ) + + loop_edge_list = set(first_path).difference(internal_edges) + encountered_edges = loop_edge_list | internal_edges + for edges in other_paths: + new_edges = set(edges).difference(encountered_edges) + loop_edge_list |= new_edges + encountered_edges |= new_edges + + _ = plt.figure(figsize=figsize) + for node_name_designator, color in NODE_COLORS.items(): + poses = { + node: loc for node, loc in node_pos.items() if node[0].startswith(node_name_designator) + } + nx.draw_networkx_nodes( + circuit_graph, poses, nodelist=list(poses.keys()), node_size=1200, node_color=color + ) + + num_flow_paths = len(other_paths) + 1 + num_scale_colors = len(DISCRETE_COLOR_SCALE) + + edge_colors = [ + DISCRETE_COLOR_SCALE[idx % num_scale_colors] for idx in range(num_flow_paths) + ] + ["gray"] + styles = ["-"] * num_flow_paths + [(0, (5, 5))] + arrowstyles = ["-|>"] * num_flow_paths + ["->"] + + for edgelist, style, edge_color, arrowstyle in zip( + [[*loop_edge_list]] + [list(internal_edges)], styles, edge_colors, arrowstyles + ): + nx.draw_networkx_edges( + circuit_graph, + node_pos, + arrowstyle=arrowstyle, + edgelist=edgelist, + edge_color=edge_color, + width=linewidth, + style=style, + arrowsize=arrowsize, + connectionstyle=f"arc3,rad={rad}", + ) + + labels = { + (node, polarity): f"{node}{'-' if polarity=='Neg' else '+'}" for node, polarity in node_pos + } + label_options = {"boxstyle": "circle", "ec": "white", "fc": "white", "alpha": 0.0} + + _ = nx.draw_networkx_labels( + circuit_graph, + node_pos, + labels, + font_size=8, + font_color="white", + font_weight="bold", + bbox=label_options, + ) + plt.show() + + +class CircuitPlotter(ipyw.VBox): + """A widget to plot circuits.""" + + graph: nx.DiGraph = trt.Instance(nx.DiGraph, args=()) + edge_curvature: ipyw.FloatSlider = trt.Instance( + ipyw.FloatSlider, + kw=dict(description="Edge Curvature", value=0.25, min=0, max=0.5, step=0.05), + ) + line_width: ipyw.FloatSlider = trt.Instance( + ipyw.FloatSlider, kw=dict(description="Line Width", value=2, min=1, max=3, step=0.2) + ) + graph_layout: ipyw.Dropdown = trt.Instance( + ipyw.Dropdown, kw=dict(options=NX_LAYOUTS, label="kamada_kawai") + ) + plot: ipyw.Output = trt.Instance(ipyw.Output, args=()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for control in (self.edge_curvature, self.line_width, self.graph_layout): + control.observe(self._update_plot, "value") + + @trt.validate("children") + def _validate_children(self, proposal: trt.Bunch) -> tuple: + children = proposal.value + if children: + return children + return [ + ipyw.VBox( + [ + self.edge_curvature, + self.line_width, + self.graph_layout, + ] + ), + self.plot, + ] + + @trt.observe("graph") + def _update_plot(self, *_): + if len(self.graph.nodes) < 1: + return + self.plot.clear_output() + with self.plot: + plt.clf() + draw_circuit( + self.graph, + figsize=(20, 10), + rad=self.edge_curvature.value, + linewidth=self.line_width.value, + layout=self.graph_layout.value, + ) + + +class CircuitGenerator(ipyw.VBox): + """An M0 instance generator for circuits.""" + + selector: ipyw.IntSlider = trt.Instance( + ipyw.IntSlider, + kw=dict( + description="Interpretation", + description_tooltip="Index of M0 interpretation selected", + layout=dict(visibility="hidden", height="0px", width="auto"), + min=1, + max=1, + ), + ) + + make_new: ipyw.Button = trt.Instance( + ipyw.Button, kw=dict(icon="plus", tooltip="Make new instances", layout=dict(width="40px")) + ) + max_tries: ipyw.IntSlider = trt.Instance( + ipyw.IntSlider, + kw=dict( + description="Max attempts", + min=2, + value=5, + max=20, + layout=dict(width="100%", min_width="300px"), + ), + ) + + num_resistors: ipyw.IntRangeSlider = trt.Instance( + ipyw.IntRangeSlider, + kw=dict( + description="# Resistors", min=1, max=12, layout=dict(width="auto", min_width="300px") + ), + ) + num_diodes: ipyw.IntRangeSlider = trt.Instance( + ipyw.IntRangeSlider, + kw=dict( + description="# Diodes", min=0, max=5, layout=dict(width="auto", min_width="300px") + ), + ) + + progress_bar: ipyw.IntProgress = trt.Instance( + ipyw.IntProgress, + kw=dict( + min=0, + max=5, + layout=dict(visibility="hidden", height="0px", width="auto", min_width="300px"), + ), + ) + + circuit_model: pm.Model = trt.Instance(pm.Model) + interpretations: tuple = trt.Tuple() + selected_interpretation: dict = trt.Dict() + + @trt.default("circuit_model") + def _make_circuit_model(self) -> pm.Model: + circuit_file = Path(pm.__file__).parent / "../../tests/fixtures/Circuit Builder.json" + model = pm.Model.load_from_file(circuit_file) + model.max_multiplicity = 100 + return model + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.children = [ + self.selector, + ipyw.Label("Generate M0 interpretations:"), + ipyw.VBox((self.num_resistors, self.num_diodes), layout=dict(width="100%")), + ipyw.HBox((self.max_tries, self.make_new), layout=dict(width="100%")), + self.progress_bar, + ] + self.selector.observe(self._update_interpretation, "value") + for slider in (self.num_resistors, self.num_diodes): + slider.observe(self.update_multiplicities, "value") + + self._update_on_new_interpretations() + self.make_new.on_click(self._make_new_interpretations) + + @trt.observe("interpretations") + def _update_on_new_interpretations(self, *_): + interpretations = self.interpretations + selector = self.selector + if interpretations: + selector.disabled = False + selector.layout.visibility = "visible" + selector.layout.height = "40px" + selector.max = len(interpretations) + if not self.selected_interpretation: + self._update_interpretation() + else: + selector.disabled = True + selector.layout.visibility = "hidden" + selector.layout.height = "0px" + + def _update_interpretation(self, *_): + self.selected_interpretation = self.interpretations[self.selector.value - 1] + + def _make_new_interpretations(self, *_): + self.update_multiplicities() + max_tries = self.max_tries.value + progress_bar = self.progress_bar + progress_bar.value, progress_bar.max = 0, max_tries + bar_layout = progress_bar.layout + bar_layout.visibility = "visible" + bar_layout.height = "40px" + + def on_attempt(): + progress_bar.value += 1 + + new = make_circuit_interpretations( + m1_model=self.circuit_model, + required_interpretations=1, + max_tries=max_tries, + on_attempt=on_attempt, + ) + if not new: + warn(f"All {max_tries} attempts to make new M0 interpretations failed!") + bar_layout.visibility = "hidden" + bar_layout.height = "0px" + self.interpretations += new + + def update_multiplicities(self, *_): + update_multiplicities( + circuit=self.circuit_model, + num_resistors=self.num_resistors.value, + num_diodes=self.num_diodes.value, + ) + + +class ParametricExecutor(DockBox): + """ + A controller for a model's parameters, runs it, and displays the results. + + .. note:: + Currently only looks at the options in a model. + + """ + + description: str = trt.Unicode("Parametric Model").tag(sync=True) + float_sliders: ty.Tuple[ipyw.FloatSlider] = TypedTuple(trt.Instance(ipyw.FloatSlider)) + result_labels: ty.Tuple[ipyw.Label] = TypedTuple(trt.Instance(ipyw.Label)) + + problem: om.Problem = trt.Instance(om.Problem, allow_none=True) + inputs: ipyw.VBox = trt.Instance(ipyw.VBox) + input_sliders: ipyw.VBox = trt.Instance(ipyw.VBox, args=()) + results: ipyw.VBox = trt.Instance(ipyw.VBox) + + run_problem: ipyw.Button = trt.Instance(ipyw.Button) + + on_run_callbacks: ty.Tuple[ty.Callable] = TypedTuple(trt.Callable()) + + DEFAULT_DOCK_LAYOUT = { + "type": "split-area", + "orientation": "vertical", + "children": [ + {"type": "tab-area", "widgets": [0], "currentIndex": 0}, + {"type": "tab-area", "widgets": [1], "currentIndex": 0}, + ], + "sizes": [0.5, 0.5], + } + + def __init__(self, *args, **kwargs): + kwargs["dock_layout"] = kwargs.get("dock_layout", self.DEFAULT_DOCK_LAYOUT) + super().__init__(*args, **kwargs) + + @trt.validate("children") + def _validate_children(self, proposal: trt.Bunch) -> tuple: + children = proposal.value + if children: + return children + return tuple( + [ + self.inputs, + self.results, + ] + ) + + @trt.default("inputs") + def _make_inputs(self) -> ipyw.VBox: + box = ipyw.VBox( + children=(self.run_problem, self.input_sliders), + layout=dict(height="100%", width="100%"), + ) + box.add_traits(description=trt.Unicode("Model Parameters").tag(sync=True)) + return box + + @trt.default("results") + def _make_results(self) -> ipyw.VBox: + box = ipyw.VBox(layout=dict(height="100%", width="100%")) + box.add_traits(description=trt.Unicode("Model Results").tag(sync=True)) + return box + + @trt.default("run_problem") + def _make_run_problem_button(self) -> ipyw.Button: + btn = ipyw.Button( + icon="play", + tooltip="Run OpenMDAO problem", + disabled=True, + layout=dict(width="40px"), + ) + btn.on_click(self._run_problem) + return btn + + def _run_problem(self, *_): + self.run_problem.disabled = True + self.results.children = [] + self.update_parameter_values() + self.problem.run_model() + # TODO: update problem values + self.update_results(self.problem) + + def update_parameter_values(self): + problem = self.problem + for slider in self.input_sliders.children: + name = slider.description + value = slider.value + try: + problem.set_val(name, value) + # If not an attribute, try to update the option of a system + except KeyError: + item = problem.model + *keys, attribute = name.split(".") + for key in keys: + item = getattr(item, key) + item.options[attribute] = value + + @trt.observe("problem") + def _on_new_problem(self, *_): + problem = self.problem + if not isinstance(problem, om.Problem): + self.run_problem.disabled = True + return + self.update_parameter_controllers(problem=problem) + self.update_results(problem=problem) + self.run_problem.disabled = False + + @staticmethod + def get_kwargs( + name, + spec: dict, + num_steps: int = 20, + upper_mult: float = 2.0, + lower_mult: float = 0.5, + ) -> dict: + value = spec.get("val") + if spec.get("shape") == (1,): + value = value[0] + upper = spec.get("upper") + if upper is None: + if not value: + upper = 100 # TODO: figure out a better way to do this with other widgets + upper = value * upper_mult + lower = spec.get("lower") + if lower is None: + lower = value * lower_mult + return dict( + value=value, + max=upper, + min=lower, + step=(upper - lower) / num_steps, + description=name, + description_tooltip=spec.get("desc") or f"Set the value for '{name}'", + ) + + def update_parameter_controllers(self, problem: om.Problem): + # Get all the model systems' options that have a float as a value + system_parameters = { + system.pathname: { + key: spec + for key, spec in system.options._dict.items() + if isinstance(spec.get("val"), float) + } + for system in problem.model.system_iter() + if not system.name.startswith("_") + } + parameters = { + f"{system_name}.{param_name}": self.get_kwargs( + f"{system_name}.{param_name}", param_spec + ) + for system_name, parameters in system_parameters.items() + for param_name, param_spec in parameters.items() + } + parameters.update( + { + spec["prom_name"]: self.get_kwargs(spec["prom_name"], spec) + for _, spec in problem.model.list_inputs(prom_name=True, out_stream=False) + } + ) + parameters = dict(sorted(parameters.items())) + + # if necessary, create new sliders + num_sliders = len(parameters) + slider_layout = dict(min_width="100px", width="98%") + additional_sliders_required = num_sliders - len(self.float_sliders) + if additional_sliders_required > 0: + new_sliders = tuple( + ipyw.FloatSlider(layout=slider_layout) for _ in range(additional_sliders_required) + ) + for slider in new_sliders: + slider.observe(self._update_run_button, "value") + self.float_sliders += new_sliders + + # configure sliders + sliders = self.float_sliders[:num_sliders] + for slider, (_, parameter_spec) in zip(sliders, sorted(parameters.items())): + slider.attribute = parameter_spec + with self.hold_trait_notifications(): + # FIXME: figure out a better way to handle min/max/value checks + slider.value = 0.5 * (slider.max + slider.min) + slider.min = -9999999 + slider.max = 9999999 + for key, value in parameter_spec.items(): + setattr(slider, key, value) + + self.input_sliders.children = sliders + + def update_results(self, problem: om.Problem): + if not isinstance(problem, om.Problem): + return + outputs = { + spec["prom_name"]: dict( + value=spec["val"][0] if spec.get("shape") == (1,) else spec["val"], + units=spec.get("units"), + shape=spec.get("shape"), + ) + for _, spec in problem.model.list_outputs( + prom_name=True, shape=True, units=True, out_stream=False + ) + } + num_labels = len(outputs) + additional_labels_required = num_labels - len(self.result_labels) + if additional_labels_required > 0: + self.result_labels += tuple(ipyw.Label() for _ in range(additional_labels_required)) + labels = self.result_labels[:num_labels] + for label, (name, value) in zip(labels, sorted(outputs.items())): + label.value = ( + f"{name}: {value['value']:.5g}" + f" {value['units']}" if value["units"] else "" + ) + self.results.children = labels + + def _update_run_button(self, *_): + self.run_problem.disabled = False + for callback in self.on_run_callbacks: + callback(problem=self.problem) + + +class CircuitUI(DockPop): + """A user interface for interacting with the Circuit Builder example.""" + + DEFAULT_LAYOUT = { + "type": "split-area", + "orientation": "horizontal", + "children": [ + { + "type": "split-area", + "orientation": "vertical", + "children": [ + {"type": "tab-area", "widgets": [0], "currentIndex": 0}, + {"type": "tab-area", "widgets": [4], "currentIndex": 0}, + ], + "sizes": [0.28, 0.72], + }, + {"type": "tab-area", "widgets": [1, 3, 2], "currentIndex": 0}, + ], + "sizes": [0.2, 0.8], + } + + panels: DockBox = trt.Instance( + DockBox, kw=dict(description="Circuit Generator", dock_layout=DEFAULT_LAYOUT) + ) + + circuit_model: pm.Model = trt.Instance(pm.Model, allow_none=True) + interpretation: dict = trt.Dict() + + instance_generator: CircuitGenerator = trt.Instance(CircuitGenerator, args=()) + graph_plotter: CircuitPlotter = trt.Instance(CircuitPlotter, args=()) + connections: ipyw.Output = trt.Instance(ipyw.Output, kw=dict(layout=dict(width="100%"))) + n2: ipyw.Output = trt.Instance(ipyw.Output, kw=dict(layout=dict(width="100%"))) + parametric_executor: ParametricExecutor = trt.Instance( + ParametricExecutor, kw=dict(layout=dict(height="auto", width="100%")) + ) + + @trt.default("circuit_model") + def _get_circuit_model(self) -> pm.Model: + return self.instance_generator.circuit_model + + @property + def panel_layout(self): + return self.panels.dock_layout + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + generator = self.instance_generator + + if self.circuit_model is None: + self.circuit_model = self.instance_generator.circuit_model + + panels = { + "Generator": generator, + "Graph": self.graph_plotter, + "Connections": self.connections, + "N2": self.n2, + "Parametric Model": self.parametric_executor, + } + self.panels.children = tuple(panels.values()) + self.children = [self.panels] + + for name, widget in panels.items(): + widget.add_traits(description=trt.Unicode(default_value=name).tag(sync=True)) + + self.parametric_executor.on_run_callbacks += (self._update_connections,) + + trt.link((self, "circuit_model"), (generator, "circuit_model")) + trt.link((self, "interpretation"), (generator, "selected_interpretation")) + + @trt.observe("interpretation") + def _update_graph(self, *_): + graph = self.interpretation.get("cleaned_graph") + if isinstance(graph, nx.DiGraph): + self.graph_plotter.graph = graph + problem = self.interpretation.get("problem") + if isinstance(problem, om.Problem): + self.parametric_executor.problem = problem + + def _update_n2(self, problem: om.Problem, filename="n2.html", width="100%", height=700): + html_file = Path(filename).resolve().absolute() + if html_file.exists(): + html_file.unlink() + om.n2(problem, outfile=str(filename)) + self.n2.clear_output() + if html_file.is_file(): + with self.n2: + display(IFrame(str(filename), width=width, height=height)) + + def _update_connections(self, problem: om.Problem, filename="connections.html"): + html_file = Path(filename).resolve().absolute() + if html_file.exists(): + html_file.unlink() + om.view_connections(problem, outfile=str(filename)) + self.connections.clear_output() + if html_file.is_file(): + with self.connections: + display(HTML(html_file.read_text())) + + @trt.observe("interpretation") + def _update_openmdao_views(self, change: trt.Bunch = None): + if change: + problem = (change.new or {}).get("problem") + elif self.interpretation: + problem = self.interpretation.get("problem") + if problem: + self._update_n2(problem=problem) + self._update_connections(problem=problem) diff --git a/tests/interpretation/test_calculation_ordering.py b/tests/interpretation/test_calculation_ordering.py index 1632f10c..b8c3c18a 100644 --- a/tests/interpretation/test_calculation_ordering.py +++ b/tests/interpretation/test_calculation_ordering.py @@ -1,13 +1,12 @@ import pytest from pymbe.interpretation.calc_dependencies import generate_execution_order -from tests.conftest import kerbal_lpg, kerbal_random_stage_5_complete, kerbal_stable_names ROCKET_BUILDING = "Model::Kerbal::Rocket Building::" PARTS_LIBRARY = "Model::Kerbal::Parts Library::" -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_kerbal_calc_order(kerbal_lpg, kerbal_random_stage_5_complete, kerbal_stable_names): # check that the right number of values are created for their features diff --git a/tests/interpretation/test_graph_orderings.py b/tests/interpretation/test_graph_orderings.py index 6e509eac..04362566 100644 --- a/tests/interpretation/test_graph_orderings.py +++ b/tests/interpretation/test_graph_orderings.py @@ -1,7 +1,5 @@ import networkx as nx -from tests.conftest import kerbal_model_loaded_client - def test_part_def_graph_ordering(kerbal_lpg): diff --git a/tests/interpretation/test_graph_visit_orderings.py b/tests/interpretation/test_graph_visit_orderings.py index 5c1aea88..7559e42a 100644 --- a/tests/interpretation/test_graph_visit_orderings.py +++ b/tests/interpretation/test_graph_visit_orderings.py @@ -2,7 +2,7 @@ build_expression_sequence_templates, build_sequence_templates, ) -from pymbe.interpretation.results import * +from pymbe.interpretation.results import pprint_double_id_list ROCKET_BUILDING = "Model::Kerbal::Rocket Building::" PARTS_LIBRARY = "Model::Kerbal::Parts Library::" @@ -11,15 +11,15 @@ PARTS_LIBRARY = "Model::Simple Parts Model::Fake Library::" -def test_feature_sequence_templates1(kerbal_client, kerbal_lpg, kerbal_stable_names): +def test_feature_sequence_templates1(kerbal_lpg, kerbal_stable_names): *_, qualified_name_to_id = kerbal_stable_names seq_templates = build_sequence_templates(kerbal_lpg) assert len(seq_templates) == 39 - solid_booster_id = qualified_name_to_id[f"{ROCKET_BUILDING}Solid Booster <>"] - boosters_id = qualified_name_to_id[ + _solid_booster_id = qualified_name_to_id[f"{ROCKET_BUILDING}Solid Booster <>"] + _boosters_id = qualified_name_to_id[ f"{ROCKET_BUILDING}Solid Stage::boosters: Solid Booster <>" ] liquid_stage_id = qualified_name_to_id[f"{ROCKET_BUILDING}Liquid Stage <>"] @@ -37,11 +37,11 @@ def test_feature_sequence_templates1(kerbal_client, kerbal_lpg, kerbal_stable_na assert [liquid_stage_id, engines_id] in seq_templates - assert any([krp_mass_id in seq for seq in seq_templates]) + assert any(krp_mass_id in seq for seq in seq_templates) - assert any([(engines_id in seq) and (liquid_stage_id in seq) for seq in seq_templates]) + assert any((engines_id in seq) and (liquid_stage_id in seq) for seq in seq_templates) - assert any([(tanks_id in seq) and (liquid_stage_id in seq) for seq in seq_templates]) + assert any((tanks_id in seq) and (liquid_stage_id in seq) for seq in seq_templates) def test_feature_sequence_templates2(kerbal_lpg, kerbal_stable_names): diff --git a/tests/interpretation/test_playbook_phases.py b/tests/interpretation/test_playbook_phases.py index 92408817..be21b464 100644 --- a/tests/interpretation/test_playbook_phases.py +++ b/tests/interpretation/test_playbook_phases.py @@ -16,7 +16,7 @@ SIMPLE_MODEL = "Model::Simple Parts Model::" -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_type_multiplicity_dict_building(kerbal_lpg, kerbal_stable_names): *_, qualified_name_to_id = kerbal_stable_names @@ -44,7 +44,7 @@ def test_type_multiplicity_dict_building(kerbal_lpg, kerbal_stable_names): assert full_multiplicities[real_id] == 3038 -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_phase_1_instance_creation(kerbal_random_stage_1_instances, kerbal_stable_names): *_, qualified_name_to_id = kerbal_stable_names @@ -62,7 +62,7 @@ def test_phase_1_instance_creation(kerbal_random_stage_1_instances, kerbal_stabl assert solid_booster_id not in kerbal_random_stage_1_instances -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_phase_1_singleton_instances(kerbal_random_stage_1_complete, kerbal_stable_names): *_, qualified_name_to_id = kerbal_stable_names @@ -80,9 +80,9 @@ def test_phase_1_singleton_instances(kerbal_random_stage_1_complete, kerbal_stab assert solid_booster_id not in kerbal_random_stage_1_complete -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_phase_2_instance_creation( - kerbal_lpg, kerbal_random_stage_1_complete, kerbal_stable_names, kerbal_client + kerbal_lpg, kerbal_random_stage_1_complete, kerbal_stable_names ): *_, qualified_name_to_id = kerbal_stable_names @@ -154,7 +154,7 @@ def test_phase_3_instance_sampling(kerbal_random_stage_3_complete, kerbal_stable assert len(kerbal_random_stage_3_complete[booster_empty_mass_id]) > 0 -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_phase_4_instance_sampling( kerbal_random_stage_4_complete, kerbal_stable_names, kerbal_lpg ): @@ -208,7 +208,7 @@ def test_phase_4_instance_sampling( assert len(kerbal_random_stage_4_complete[booster_empty_mass_id]) > 0 -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_sp_phase_1_instance_creation( simple_parts_random_stage_1_instances, simple_parts_stable_names ): @@ -229,7 +229,7 @@ def test_sp_phase_1_instance_creation( assert power_user_id not in simple_parts_random_stage_1_instances -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_sp_phase_1_singleton_instances( simple_parts_random_stage_1_complete, simple_parts_stable_names ): @@ -250,7 +250,7 @@ def test_sp_phase_1_singleton_instances( assert power_user_id not in simple_parts_random_stage_1_complete -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_sp_phase_2_instance_creation( simple_parts_lpg, simple_parts_random_stage_1_complete, @@ -284,7 +284,7 @@ def test_sp_phase_2_instance_creation( assert len(simple_parts_random_stage_1_complete[port_id]) == 6 -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_sp_phase_3_instance_sampling( simple_parts_random_stage_3_complete, simple_parts_stable_names ): diff --git a/tests/jupyter/notebooks/Playbook Explorer Parts Test.ipynb b/tests/jupyter/notebooks/Playbook Explorer Parts Test.ipynb index e3b447e9..dea2b92f 100644 --- a/tests/jupyter/notebooks/Playbook Explorer Parts Test.ipynb +++ b/tests/jupyter/notebooks/Playbook Explorer Parts Test.ipynb @@ -360,7 +360,7 @@ "outputs": [], "source": [ "m0_interpretation = random_generator_playbook(\n", - " lpg=lpg,\n", + " m1=lpg,\n", " name_hints=name_hints,\n", " filtered_feat_packages=[lpg.model.ownedElement[\"Simple Parts Model\"]],\n", ")" diff --git a/tests/jupyter/test_notebooks.py b/tests/jupyter/test_notebooks.py index edc894b2..99f4d72f 100644 --- a/tests/jupyter/test_notebooks.py +++ b/tests/jupyter/test_notebooks.py @@ -2,7 +2,7 @@ from testbook import testbook -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") @testbook("docs/tutorials/01-Basic.ipynb", execute=True) def test_tutorial(tb): """Test the Tutorial notebook""" @@ -16,7 +16,7 @@ def test_tutorial(tb): assert not set(m1_diagram.model.elements).symmetric_difference(set(tree.model.elements)) -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") @testbook("tests/jupyter/notebooks/Playbook Explorer Parts Test.ipynb", execute=True) def test_simple_parts_explorer(tb): m0_interpretation = tb.ref("m0_interpretation") diff --git a/tests/model/test_instantiation.py b/tests/model/test_instantiation.py index 8b1a7b4b..e1feee4d 100644 --- a/tests/model/test_instantiation.py +++ b/tests/model/test_instantiation.py @@ -1,27 +1,26 @@ -import pytest - -from pymbe.model import VALUE_METATYPES, ValueHolder -from tests.conftest import kerbal_model - - -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") -def test_instantiation(kerbal_model): - model = kerbal_model - - for element in model.elements.values(): - label = element.label - if not label or element._metatype not in VALUE_METATYPES: - continue - - instance = element(value=1.5) - assert isinstance(instance, ValueHolder) - assert instance.value == 1.5 - repred = str(instance) - assert element.name in repred - assert "1.5" in repred - - unset_value_holder = element() - assert unset_value_holder.value is None - repred = str(unset_value_holder) - assert element.name in repred - assert "unset" in repred +import pytest + +from pymbe.model import VALUE_METATYPES, ValueHolder + + +@pytest.mark.skip("Need to refactor tests, after upgrades") +def test_instantiation(kerbal_model): + model = kerbal_model + + for element in model.elements.values(): + label = element.label + if not label or element._metatype not in VALUE_METATYPES: + continue + + instance = element(value=1.5) + assert isinstance(instance, ValueHolder) + assert instance.value == 1.5 + repred = str(instance) + assert element.name in repred + assert "1.5" in repred + + unset_value_holder = element() + assert unset_value_holder.value is None + repred = str(unset_value_holder) + assert element.name in repred + assert "unset" in repred diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 11cb9678..40e8e551 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1,6 +1,9 @@ -from copy import deepcopy +from pathlib import Path +from uuid import uuid4 -from tests.conftest import all_models, kerbal_model +import pytest + +from pymbe.model import Element, Model def test_respect_of_sysml(all_models): @@ -24,7 +27,7 @@ def test_kerbal_model(kerbal_model): kerbal = model.ownedElement["Kerbal"] assert kerbal.name == "Kerbal", f"Element name should be 'Kerbal', not '{kerbal.name}'" - assert kerbal == kerbal._id, f"Element should equate to its id" + assert kerbal == kerbal._id, "Element should equate to its id" my_rocket = kerbal(name="My Rocket") assert ( @@ -44,17 +47,85 @@ def test_kerbal_model(kerbal_model): def test_relationships(kerbal_model): model = kerbal_model + rocket_part = None for element in model.elements.values(): if element.name == "Kerbal Rocket Part": rocket_part = element + break + subclass = None for subclass in rocket_part.reverseSuperclassing: if subclass.name == "Parachute": break + assert subclass, "Did not find the `Parachute`" + assert rocket_part, "Did not find the `Kerbal Rocket Part`" assert subclass.throughSuperclassing[0].name == rocket_part.name +def test_edge_cases(): + name = f"model_{uuid4()}" + filename = f"{name}.json" + model = Model(elements={}, name=name) + cwd = Path(".") + + with pytest.warns(UserWarning): + model.save_to_file() + assert not list(cwd.glob(filename)), f"Model should NOT have been saved! {list(cwd.glob('*'))}" + + empty_element = Element(_data={}, _model=model) + assert empty_element._is_proxy, "Element should be marked as proxy!" + + model.save_to_file() + saved_file = list(cwd.glob(filename)) + assert saved_file, "Model should have been saved with .json extension added!" + + with pytest.warns(UserWarning): + model.save_to_file(filename) + saved_file[0].unlink() + + +def test_package_references(kerbal_model): + model = kerbal_model + packages = model.packages + + assert packages, "Kerbal should have some packages!" + + a_package, sub_package = None, None + for a_package in packages: + sub_packages = [ + pkg + for pkg in a_package.ownedElement + if pkg._metatype == "Package" and pkg.ownedElement + ] + if sub_packages: + sub_package = sub_packages[0] + break + + assert a_package, "Kerbal model should have a package that has another package" + assert sub_package, "Kerbal model should have a nested package" + + owned_element = sub_package.ownedElement[0] + assert sub_package.ownedElement[owned_element.name] == owned_element + + assert ( + owned_element.owning_package == sub_package + ), f"{owned_element} should know it is owned by {sub_package}" + assert owned_element.is_in_package(a_package), f"{owned_element} should be in {a_package}" + assert owned_element.is_in_package(sub_package), f"{owned_element} should be in {sub_package}" + assert not a_package.is_in_package( + owned_element + ), f"{a_package} should NOT be owned by {owned_element}" + assert not a_package.is_in_package( + sub_package + ), f"{a_package} should NOT be owned by {sub_package}" + assert not sub_package.is_in_package( + owned_element + ), f"{sub_package} should NOT be owned by {owned_element}" + + assert (a_package > sub_package) == (a_package.name > sub_package.name) + + def test_accessors(kerbal_model): model = kerbal_model for element in model.elements.values(): diff --git a/tests/query/test_feature_queries.py b/tests/query/test_feature_queries.py index f7131f0f..48a83420 100644 --- a/tests/query/test_feature_queries.py +++ b/tests/query/test_feature_queries.py @@ -9,14 +9,13 @@ roll_up_multiplicity_for_type, roll_up_upper_multiplicity, ) -from tests.conftest import kerbal_model_loaded_client ROCKET_BUILDING = "Model::Kerbal::Rocket Building::" PARTS_LIBRARY = "Model::Kerbal::Parts Library::" SIMPLE_MODEL = "Model::Simple Parts Model::" -def test_feature_to_type1(kerbal_client, kerbal_lpg, kerbal_stable_names): +def test_feature_to_type1(kerbal_lpg, kerbal_stable_names): *_, qualified_name_to_id = kerbal_stable_names engines_feat = qualified_name_to_id[ @@ -27,7 +26,7 @@ def test_feature_to_type1(kerbal_client, kerbal_lpg, kerbal_stable_names): assert get_types_for_feature(kerbal_lpg, engines_feat) == [engine_type_feat] -def test_type_to_feature1(kerbal_client, kerbal_lpg, kerbal_stable_names): +def test_type_to_feature1(kerbal_lpg, kerbal_stable_names): *_, qualified_name_to_id = kerbal_stable_names engines_feat = qualified_name_to_id[ @@ -38,11 +37,11 @@ def test_type_to_feature1(kerbal_client, kerbal_lpg, kerbal_stable_names): assert get_features_typed_by_type(kerbal_lpg, engine_type_feat) == [engines_feat] -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_type_to_feature2(simple_parts_client, simple_parts_lpg, simple_parts_stable_names): *_, qualified_name_to_id = simple_parts_stable_names - port_type_id = qualified_name_to_id[f"Model::Ports::Port <>"] + port_type_id = qualified_name_to_id["Model::Ports::Port <>"] power_in_id = qualified_name_to_id[ f"{SIMPLE_MODEL}Power Group: Part::Power User: Part::Power In: Port <>" ] @@ -197,7 +196,7 @@ def test_type_multiplicity_rollup1(kerbal_lpg, kerbal_stable_names): assert rocket_upper == 0 -@pytest.mark.skip("Need to refactor tests, after 0.19.0 upgrades") +@pytest.mark.skip("Need to refactor tests, after upgrades") def test_type_multiplicity_rollup2(simple_parts_lpg, simple_parts_stable_names): *_, qualified_name_to_id = simple_parts_stable_names