diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c43265e..35a1775 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,31 +19,26 @@ on: workflow_dispatch: workflow_call: jobs: - test: + format: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: [ "3.9", "3.10", "3.11" ] - + if: github.event_name == 'pull_request' + permissions: + contents: write steps: - uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - + ref: ${{ github.head_ref || github.ref }} + - name: Install Python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: "3.11" - - name: Install dependencies + - name: Install Black run: | python -m pip install --upgrade pip - pip install flake8 pytest black - pip install -e .[all] - - - name: Display Python Version - run: python -c "import sys; print(sys.version)" + pip install black - name: Check for existing release if: github.event_name == 'pull_request' @@ -63,11 +58,8 @@ jobs: else echo "Version v$VERSION is available" fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Auto-format with Black - if: github.event_name == 'pull_request' run: | black ./engforge if ! git diff --quiet; then @@ -75,11 +67,40 @@ jobs: git config --local user.name "GitHub Action" git add -A git commit -m "Auto-format code with Black [skip ci]" || exit 0 - git push + git push origin HEAD:${{ github.head_ref }} fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + test: + runs-on: ubuntu-latest + needs: [format] + if: always() && (needs.format.result == 'success' || needs.format.result == 'skipped') + strategy: + fail-fast: false + matrix: + python-version: [ "3.9", "3.10", "3.11" ] + + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.head_ref || github.ref }} + + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - - name: Run tests - run: python -m unittest discover -s engforge/test -p "test_*.py" -v + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest black + pip install -e .[all] - - name: Verify Black formatting - run: black --check --verbose ./engforge \ No newline at end of file + - name: Check Python & Run Tests + run: | + python -c "import sys; print(sys.version)" + python -m pip list | grep PyNiteFEA || echo "pynite-not-found" + python -m pip list | grep coolprop || echo "coolprop-not-found" + python -m unittest discover -s engforge/test -p "test_*.py" -v diff --git a/engforge/_testing_components.py b/engforge/_testing_components.py index cf43fa8..214a064 100644 --- a/engforge/_testing_components.py +++ b/engforge/_testing_components.py @@ -760,7 +760,7 @@ class Comp2(Norm, CostModel): comp1 = Slot.define(Comp1, none_ok=True, default_ok=False) -quarterly = lambda inst, term: True if (term + 1) % 3 == 0 else False +quarterly = lambda inst, term, econ: True if (term + 1) % 3 == 0 else False @forge diff --git a/engforge/attr_plotting.py b/engforge/attr_plotting.py index 8762cdd..3295189 100644 --- a/engforge/attr_plotting.py +++ b/engforge/attr_plotting.py @@ -1,5 +1,4 @@ -"""This module defines Plot and Trace methods that allow the plotting of Statistical & Transient relationships of data in each system -""" +"""This module defines Plot and Trace methods that allow the plotting of Statistical & Transient relationships of data in each system""" import attrs import uuid diff --git a/engforge/attributes.py b/engforge/attributes.py index 1853384..f35f7f8 100644 --- a/engforge/attributes.py +++ b/engforge/attributes.py @@ -1,6 +1,6 @@ """Defines a customizeable attrs attribute that is handled in configuration, -on init an instance of `Instance` type for any ATTR_BASE subclass is created """ +on init an instance of `Instance` type for any ATTR_BASE subclass is created""" import attrs, attr, uuid from engforge.logging import LoggingMixin, log diff --git a/engforge/component_collections.py b/engforge/component_collections.py index ff83bfa..bef2736 100644 --- a/engforge/component_collections.py +++ b/engforge/component_collections.py @@ -1,12 +1,12 @@ """define a collection of components that will propigate to its parents dataframe -When `wide` is set each component's references are reported to the system's table, otherwise only one component's references are reported, however the system will iterate over the components by calling `system.iterable_components` +When `wide` is set each component's references are reported to the system's table, otherwise only one component's references are reported, however the system will iterate over the components by calling `system.iterable_components` Define a Iterable Component slot in a system by calling `Slot.define_iterable(...,wide=True/False)` CostModel isonly supported in wide mode at this time. -Types: +Types: 1. ComponentList, ordered by index 2. ComponentDict, ordered by key 3. ComponentGraph, ?#TODO: diff --git a/engforge/configuration.py b/engforge/configuration.py index add80b4..2ec6441 100644 --- a/engforge/configuration.py +++ b/engforge/configuration.py @@ -331,11 +331,12 @@ def signals_slots_handler( f"{cls.__name__} overriding inherited attr: {o.name} as a system property overriding it" ) elif o.inherited and k in cls_dict: - log.debug( + log.warning( f"{cls.__name__} overriding inherited attr: {o.name} as {cls_dict[k]} in cls" ) # FIXME: should we deepcopy? - cls.__anony_store[k] = sscb = lambda: copy.copy(cls_dict[k]) + val = copy.copy(cls_dict[k]) + cls.__anony_store[k] = sscb = lambda: val out.append(o.evolve(default=attrs.Factory(sscb))) else: log.msg(f"{cls.__name__} adding attr: {o.name}") diff --git a/engforge/datastores/__init__.py b/engforge/datastores/__init__.py index 8867cd7..4c8b544 100644 --- a/engforge/datastores/__init__.py +++ b/engforge/datastores/__init__.py @@ -4,6 +4,7 @@ import importlib.util from pathlib import Path + def get_project_root(): """Find the project root containing pyproject.toml""" current = Path(__file__).parent @@ -13,6 +14,7 @@ def get_project_root(): current = current.parent return None + def install_optional_dependencies(group_name="database"): """Install optional dependencies for a specific group""" project_root = get_project_root() @@ -20,32 +22,34 @@ def install_optional_dependencies(group_name="database"): print("Could not find pyproject.toml. Manual installation required:") print(f"pip install engforge[{group_name}]") return False - + try: # Try to import tomllib (Python 3.11+) or tomli (fallback) try: import tomllib except ImportError: import tomli as tomllib - + # Read pyproject.toml with open(project_root / "pyproject.toml", "rb") as f: config = tomllib.load(f) - + optional_deps = config.get("project", {}).get("optional-dependencies", {}) if group_name not in optional_deps: print(f"No optional dependency group '{group_name}' found") return False - + # Install the optional dependencies package_name = config.get("project", {}).get("name", "engforge") install_spec = f"{package_name}[{group_name}]" - + print(f"Installing {install_spec}...") - result = subprocess.run([ - sys.executable, "-m", "pip", "install", install_spec - ], capture_output=True, text=True) - + result = subprocess.run( + [sys.executable, "-m", "pip", "install", install_spec], + capture_output=True, + text=True, + ) + if result.returncode == 0: print(f"✅ Successfully installed {install_spec}") return True @@ -53,31 +57,41 @@ def install_optional_dependencies(group_name="database"): print(f"❌ Failed to install {install_spec}") print(f"Error: {result.stderr}") return False - + except Exception as e: print(f"Error during auto-installation: {e}") print(f"Please install manually: pip install engforge[{group_name}]") return False + def check_and_install_datastores(): """Check if datastores dependencies are available, auto-install if needed""" try: import engforge.datastores.data + return True except ImportError as e: print(f"Datastores dependencies not available: {e}") print("") - + # Check if we're in development mode (editable install) and interactive project_root = get_project_root() - is_interactive = hasattr(sys, 'ps1') or sys.stdout.isatty() - - if project_root and (project_root / "pyproject.toml").exists() and is_interactive: + is_interactive = hasattr(sys, "ps1") or sys.stdout.isatty() + + if ( + project_root + and (project_root / "pyproject.toml").exists() + and is_interactive + ): try: - answer = input("Auto-install database dependencies? (y/N): ").strip().lower() - if answer in ['y', 'yes']: + answer = ( + input("Auto-install database dependencies? (y/N): ").strip().lower() + ) + if answer in ["y", "yes"]: if install_optional_dependencies("database"): - print("Please restart your Python session to use the newly installed dependencies.") + print( + "Please restart your Python session to use the newly installed dependencies." + ) return False else: print("Auto-installation failed. Install manually with:") @@ -96,6 +110,7 @@ def check_and_install_datastores(): print("pip install engforge[database]") return False + # Auto-check and install on import if not check_and_install_datastores(): pass # Dependencies not available, but user has been informed diff --git a/engforge/dynamics.py b/engforge/dynamics.py index 2b94abd..2b40058 100644 --- a/engforge/dynamics.py +++ b/engforge/dynamics.py @@ -8,7 +8,7 @@ #TODO: The top level system will collect the underlying dynamical systems and combine them to an index and overall state space model. This will allow for the creation of a system of systems, and the ability to create a system of systems with a single state space model. -#TODO: integration is done by the solver, where DynamicSystems have individual solver control, solver control is set for a smart default scipy +#TODO: integration is done by the solver, where DynamicSystems have individual solver control, solver control is set for a smart default scipy """ from engforge.configuration import Configuration, forge @@ -29,7 +29,6 @@ from collections import OrderedDict import numpy as np import pandas -import expiringdict import attr, attrs diff --git a/engforge/eng/costs.py b/engforge/eng/costs.py index c913d1c..7210b7b 100644 --- a/engforge/eng/costs.py +++ b/engforge/eng/costs.py @@ -4,14 +4,14 @@ CostModel's can have cost_property's which detail how and when a cost should be applied & grouped. By default each CostModel has a `cost_per_item` which is reflected in `item_cost` cost_property set on the `initial` term as a `unit` category. Multiple categories of cost are also able to be set on cost_properties as follows -Cost Models can represent multiple instances of a component, and can be set to have a `num_items` multiplier to account for multiple instances of a component. CostModels can have a `term_length` which will apply costs over the term, using the `cost_property.mode` to determine at which terms a cost should be applied. +Cost Models can represent multiple instances of a component, and can be set to have a `num_items` multiplier to account for multiple instances of a component. CostModels can have a `term_length` which will apply costs over the term, using the `cost_property.mode` to determine at which terms a cost should be applied. ``` @forge class Widget(Component,CostModel): - + #use num_items as a multiplier for costs, `cost_properties` can have their own custom num_items value. - num_items:float = 100 + num_items:float = 100 @cost_property(mode='initial',category='capex,manufacturing',num_items=1) def cost_of_XYZ(self) -> float: diff --git a/engforge/eng/geometry.py b/engforge/eng/geometry.py index f375ede..65aaf9f 100644 --- a/engforge/eng/geometry.py +++ b/engforge/eng/geometry.py @@ -1,4 +1,4 @@ -"""These exist as in interface to sectionproperties from PyNite""" +"""These exist as in interface to sectionproperties from Pynite""" from engforge.configuration import Configuration, forge, LoggingMixin from engforge.properties import ( diff --git a/engforge/eng/pipes.py b/engforge/eng/pipes.py index eac9ea4..4953441 100644 --- a/engforge/eng/pipes.py +++ b/engforge/eng/pipes.py @@ -2,7 +2,7 @@ start with single phase and move to others -1) Pumps Power = C x Q x P +1) Pumps Power = C x Q x P 2) Compressor = C x (QP) x (PR^C2 - 1) 3) Pipes = dP = C x fXL/D x V^2 / 2g 4) Pipe Fittings / Valves = dP = C x V^2 (fXL/D +K) | K is a constant, for valves it can be interpolated between closed and opened diff --git a/engforge/eng/solid_materials.py b/engforge/eng/solid_materials.py index 62f9f8d..aca4e79 100644 --- a/engforge/eng/solid_materials.py +++ b/engforge/eng/solid_materials.py @@ -11,7 +11,7 @@ import json, hashlib # One Material To Merge Them All -from PyNite import Material as PyNiteMat +from Pynite import Material as PyniteMat from sectionproperties.pre.pre import Material as SecMat SectionMaterial = SecMat.mro()[0] @@ -35,7 +35,7 @@ def ih(val): @forge(hash=False) -class SolidMaterial(SectionMaterial, PyNiteMat.Material, Configuration): +class SolidMaterial(SectionMaterial, PyniteMat.Material, Configuration): """A class to hold physical properties of solid structural materials and act as both a section property material and a pynite material""" __metaclass__ = SecMat diff --git a/engforge/eng/structure.py b/engforge/eng/structure.py index 62dc6d2..c2315f1 100644 --- a/engforge/eng/structure.py +++ b/engforge/eng/structure.py @@ -28,7 +28,7 @@ import sectionproperties import sectionproperties.pre.geometry as geometry import sectionproperties.pre.library.primitive_sections as sections -import PyNite as pynite +import Pynite as pynite import numpy as np from scipy import optimize as sciopt import pandas as pd @@ -208,7 +208,7 @@ class BeamDict(ComponentDict): # TODO: Make analysis, where each load case is a row, but how sytactically? @forge class Structure(System, CostModel, PredictionMixin): - """A integration between sectionproperties and PyNite, with a focus on ease of use + """A integration between sectionproperties and Pynite, with a focus on ease of use Right now we just need an integration between Sections+Materials and Members, to find, CG, and inertial components @@ -658,7 +658,7 @@ def quad_info(self) -> dict: return self._quad_info.copy() def add_mesh(self, meshtype, *args, **kwargs): - """maps to appropriate PyNite mesh generator but also applies loads + """maps to appropriate Pynite mesh generator but also applies loads Currently these types are supported ['rect','annulus','cylinder','triangle'] """ @@ -951,7 +951,7 @@ def costs(self) -> dict: return out def visulize(self, **kwargs): - from PyNite import Visualization + from Pynite import Visualization if "combo_name" not in kwargs: kwargs["combo_name"] = self.default_combo @@ -1054,7 +1054,7 @@ def merge_displacement_results(self, combo, D): node.RZ[combo] = D[node.ID * 6 + 5, 0] def prep_remote_analysis(self): - """copies PyNite analysis pre-functions""" + """copies Pynite analysis pre-functions""" # Generate all meshes for mesh in self.frame.Meshes.values(): if mesh.is_generated == False: diff --git a/engforge/eng/structure_beams.py b/engforge/eng/structure_beams.py index a6a7056..920229f 100644 --- a/engforge/eng/structure_beams.py +++ b/engforge/eng/structure_beams.py @@ -26,9 +26,9 @@ import sectionproperties.pre.geometry as geometry import sectionproperties.pre.library.primitive_sections as sections import sectionproperties.analysis.section as cross_section -import PyNite as pynite +import Pynite as pynite -# from PyNite import Visualization +# from Pynite import Visualization import copy nonetype = type(None) diff --git a/engforge/env_var.py b/engforge/env_var.py index 77212df..5519420 100644 --- a/engforge/env_var.py +++ b/engforge/env_var.py @@ -2,7 +2,7 @@ A global record of variables is kept for informational purposes in keeping track of progam variables -To prevent storage of env vars in program memory, access to the os env variables is provided on access of the `secret` variable. It is advisable to use the result of this as directly as possible when dealing with actual secrets. +To prevent storage of env vars in program memory, access to the os env variables is provided on access of the `secret` variable. It is advisable to use the result of this as directly as possible when dealing with actual secrets. For example add: `db_driver(DB_HOST.secret,DB_PASSWORD.secret,...) """ diff --git a/engforge/problem_context.py b/engforge/problem_context.py index a26de9d..6363586 100644 --- a/engforge/problem_context.py +++ b/engforge/problem_context.py @@ -1,6 +1,6 @@ -"""The ProblemExec provides a uniform set of options for managing the state of the system and its solvables, establishing the selection of combos or de/active attributes to Solvables. Once once created any further entracnces to ProblemExec will return the same instance until finally the last exit is called. +"""The ProblemExec provides a uniform set of options for managing the state of the system and its solvables, establishing the selection of combos or de/active attributes to Solvables. Once once created any further entracnces to ProblemExec will return the same instance until finally the last exit is called. -The ProblemExec class allows entrance to a its context to the same instance until finally the last exit is called. The first entrance to the context will create the instance, each subsequent entrance will return the same instance. The ProblemExec arguments are set the first time and remove keyword arguments from the input dictionary (passed as a dict ie stateful) to subsequent methods. +The ProblemExec class allows entrance to a its context to the same instance until finally the last exit is called. The first entrance to the context will create the instance, each subsequent entrance will return the same instance. The ProblemExec arguments are set the first time and remove keyword arguments from the input dictionary (passed as a dict ie stateful) to subsequent methods. This isn't technically a singleton pattern, but it does provide a similar interface. Instead mutliple problem instances will be clones of the first instance, with the optional difference of input/output/event criteria. The first instance will be returned by each context entry, so for that reason it may always appear to have same instance, however each instance is unique in a recusive setting so it may record its own state and be reverted to its own state as per the options defined. #TODO: allow update of kwargs on re-entrance @@ -13,9 +13,9 @@ pe._sys_refs #get the references and compiled problem for i in range(10): pe.solve_min(pe.Xref,pe.Yref,**other_args) - pe.set_checkpoint() #save the state of the system + pe.set_checkpoint() #save the state of the system pe.save_data() - + #Solver Module (can use without knowledge of the runtime system) with ProblemExec(sys,{},Xnew=Xnext,ctx_fail_new=True) as pe: @@ -31,7 +31,7 @@ # Active Mode Handiling The `only_active` argument can be used to select only active items. The `activate` and `deactivate` arguments can be used to activate or deactivate specific solvables. -`add_obj` can be used to add an objective to the solver. +`add_obj` can be used to add an objective to the solver. # Root Parameter Determination: Any session's may access root session parameters defined in the `root_parameters` dictionary like `converged`, or `data` can be accessed by calling `session.` this eventually calls `root._` via the __getattr__ method. @@ -62,8 +62,6 @@ from collections import OrderedDict import numpy as np import pandas as pd -import expiringdict -import attr, attrs import datetime diff --git a/engforge/solver.py b/engforge/solver.py index bb64337..812abff 100644 --- a/engforge/solver.py +++ b/engforge/solver.py @@ -4,12 +4,12 @@ ### A general Solver Run Will Look Like: 0. run pre execute (signals=pre,both) -1. add execution context with **kwargument for the signals. +1. add execution context with **kwargument for the signals. 2. parse signals here (through a new Signals.parse_rtkwargs(**kw)) which will non destructively parse the signals and return all the signal candiates which are put into an ProblemExec object that resets the signals after the run depending on the revert behavior 3. the execute method will recieve this ProblemExec object where it can update the solver references / signals so that it can handle them per the signals api -4. with self.execution_context(**kwargs) as ctx_exe: +4. with self.execution_context(**kwargs) as ctx_exe: 1. pre-update / signals - #self.execute(ctx_exe,**kwargs) + #self.execute(ctx_exe,**kwargs) 2. post-update / signals > signals will be reset after the execute per the api 5. run post update diff --git a/engforge/test/test_modules.py b/engforge/test/test_modules.py index 4f518e8..6b01692 100644 --- a/engforge/test/test_modules.py +++ b/engforge/test/test_modules.py @@ -21,7 +21,6 @@ import time - class ImportTest(unittest.TestCase): """We test the compilation of all included modules""" diff --git a/engforge/typing.py b/engforge/typing.py index 9109207..c536adb 100644 --- a/engforge/typing.py +++ b/engforge/typing.py @@ -1,6 +1,4 @@ -""" - -""" +""" """ import pandas, attr, numpy from engforge.properties import * diff --git a/pyproject.toml b/pyproject.toml index 4840542..0aca512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "engforge" -version = "0.2.0" +version = "0.3.0" description = "The Engineer's Framework" readme = "README.md" license = "MIT" authors = [ - {name = "Kevin Russell", email = "kevin@ottermatics.com"} + {name = "Kevin Russell", email = "olly@ottermatics.com"} ] classifiers = [ "Development Status :: 3 - Alpha", @@ -21,8 +21,6 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "virtualenv", - "wheel", "attrs~=23.2.0", "arrow", "deepdiff>=6.3.1", @@ -34,18 +32,6 @@ dependencies = [ "scikit-learn~=1.3.2", "seaborn", "tabulate", - "sectionproperties~=3.1.2", - "PyNiteFEA @ git+https://github.com/Ottermatics/PyNite.git@v2nept", - "control", - "fluids", - "CoolProp", - "expiringdict~=1.2.2", - "zipp==3.1.0", - "jinja2>=2.11.3", - "freezegun>=0.3.12", - "sphinx>=3.2.1", - "recommonmark>=0.6.0", - "sphinx-rtd-theme>=1.0.0", "networkx-query==1.0.1", "networkx>=2.5.1", "black~=24.8.0", @@ -62,6 +48,23 @@ Repository = "https://github.com/Ottermatics/engforge" Documentation = "https://ottermatics.github.io/engforge/" [project.optional-dependencies] + +dev = [ + "wheel", + "jinja2>=2.11.3", + "sphinx>=3.2.1", + "recommonmark>=0.6.0", + "sphinx-rtd-theme>=1.0.0", +] + +eng = [ + "control", + "fluids", + "CoolProp", + "sectionproperties~=3.1.2", + "PyniteFEA>=1.2.0" +] + database = [ "SQLAlchemy~=1.3.17", "SQLAlchemy-Utils", @@ -70,6 +73,7 @@ database = [ "sqlalchemy-batch-inserts", ] google = [ + "expiringdict~=1.2.2", "pygsheets==2.0.4", "pydrive2==1.8.1", ] @@ -90,7 +94,7 @@ docs = [ "toml~=0.10.2", ] all = [ - "engforge[database,google,cloud,distributed,docs]", + "engforge[database,google,cloud,distributed,docs,eng,dev]", ] [tool.setuptools]