Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 43 additions & 22 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -63,23 +58,49 @@ 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
git config --local user.email "action@github.com"
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
- 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
2 changes: 1 addition & 1 deletion engforge/_testing_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions engforge/attr_plotting.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion engforge/attributes.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions engforge/component_collections.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
5 changes: 3 additions & 2 deletions engforge/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
49 changes: 32 additions & 17 deletions engforge/datastores/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,71 +14,84 @@ 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()
if not project_root:
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
else:
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:")
Expand All @@ -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
3 changes: 1 addition & 2 deletions engforge/dynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +29,6 @@
from collections import OrderedDict
import numpy as np
import pandas
import expiringdict
import attr, attrs


Expand Down
6 changes: 3 additions & 3 deletions engforge/eng/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion engforge/eng/geometry.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion engforge/eng/pipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions engforge/eng/solid_materials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions engforge/eng/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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']
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions engforge/eng/structure_beams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading