diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8714727..343ba92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,5 +46,6 @@ jobs: - uses: psf/black@stable with: + version: "24.8.0" options: "--check --verbose" src: "./engforge" \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 3419e1a..fbc5966 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,22 +8,23 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) -#Source code dir relative to this file -project = 'engforge' -copyright = '2024, Kevin Russell' -author = 'Kevin Russell' +sys.path.insert(0, os.path.abspath("..")) +# Source code dir relative to this file -release = '1.0' +project = "engforge" +copyright = "2024, Kevin Russell" +author = "Kevin Russell" + +release = "1.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [] -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] extensions = [ "sphinx.ext.autodoc", @@ -31,26 +32,30 @@ "sphinx.ext.viewcode", "sphinx.ext.autosummary", "myst_parser", - "sphinx_autodoc_typehints" + "sphinx_autodoc_typehints", ] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' -html_static_path = ['_static'] +html_theme = "alabaster" +html_static_path = ["_static"] autosummary_generate = True # Turn on sphinx.ext.autosummary autoclass_content = "both" # Add __init__ doc (ie. params) to class summaries -html_show_sourcelink = False # Remove 'view source code' from top of page (for html, not python) +html_show_sourcelink = ( + False # Remove 'view source code' from top of page (for html, not python) +) autodoc_inherit_docstrings = True # If no docstring, inherit from base class set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints -#nbsphinx_allow_errors = True # Continue through Jupyter errors -autodoc_typehints = "description" # Sphinx-native method. Not as good as sphinx_autodoc_typehints -add_module_names = False # Remove namespaces from class/method signatures +# nbsphinx_allow_errors = True # Continue through Jupyter errors +autodoc_typehints = ( + "description" # Sphinx-native method. Not as good as sphinx_autodoc_typehints +) +add_module_names = False # Remove namespaces from class/method signatures html_theme = "sphinx_rtd_theme" -html_css_files = ["readthedocs-custom.css"] # Override some CSS settings +html_css_files = ["readthedocs-custom.css"] # Override some CSS settings -#Set Assertion Bypass -os.environ['SPHINX_BUILD']='true' \ No newline at end of file +# Set Assertion Bypass +os.environ["SPHINX_BUILD"] = "true" diff --git a/engforge.egg-info/PKG-INFO b/engforge.egg-info/PKG-INFO index ce69645..694e267 100644 --- a/engforge.egg-info/PKG-INFO +++ b/engforge.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: engforge -Version: 0.9.2 +Version: 0.0.9 Summary: The Engineer's Framework Home-page: https://github.com/SoundsSerious/engforge Author: Kevin russell diff --git a/engforge.egg-info/SOURCES.txt b/engforge.egg-info/SOURCES.txt index 90219a2..e8f97a7 100644 --- a/engforge.egg-info/SOURCES.txt +++ b/engforge.egg-info/SOURCES.txt @@ -1,7 +1,9 @@ LICENSE README.md +setup.cfg setup.py engforge/__init__.py +engforge/_testing_components.py engforge/analysis.py engforge/attr_dynamics.py engforge/attr_plotting.py @@ -33,7 +35,6 @@ engforge/typing.py engforge.egg-info/PKG-INFO engforge.egg-info/SOURCES.txt engforge.egg-info/dependency_links.txt -engforge.egg-info/entry_points.txt engforge.egg-info/not-zip-safe engforge.egg-info/requires.txt engforge.egg-info/top_level.txt @@ -52,12 +53,8 @@ engforge/eng/solid_materials.py engforge/eng/structure.py engforge/eng/structure_beams.py engforge/eng/thermodynamics.py -engforge/examples/__init__.py -engforge/examples/air_filter.py -engforge/examples/spring_mass.py engforge/test/__init__.py engforge/test/report_testing.py -engforge/test/solver_testing_components.py engforge/test/test_airfilter.py engforge/test/test_analysis.py engforge/test/test_comp_iter.py @@ -74,4 +71,9 @@ engforge/test/test_problem_deepscoping.py engforge/test/test_slider_crank.py engforge/test/test_solver.py engforge/test/test_structures.py -engforge/test/test_tabulation.py \ No newline at end of file +engforge/test/test_tabulation.py +examples/__init__.py +examples/air_filter.py +examples/field_area.py +examples/spring_mass.py +test/test_problem.py \ No newline at end of file diff --git a/engforge.egg-info/entry_points.txt b/engforge.egg-info/entry_points.txt deleted file mode 100644 index 62c5489..0000000 --- a/engforge.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -condaenvset = engforge.common:main_cli diff --git a/engforge.egg-info/top_level.txt b/engforge.egg-info/top_level.txt index fff77c8..50be3ae 100644 --- a/engforge.egg-info/top_level.txt +++ b/engforge.egg-info/top_level.txt @@ -1 +1,2 @@ engforge +examples diff --git a/engforge/__init__.py b/engforge/__init__.py index 497bafe..e588e13 100644 --- a/engforge/__init__.py +++ b/engforge/__init__.py @@ -18,7 +18,7 @@ from engforge.analysis import Analysis from engforge.env_var import EnvVariable from engforge.problem_context import ProblemExec - +from engforge.system_reference import Ref from engforge.env_var import EnvVariable from engforge.logging import LoggingMixin, change_all_log_levels diff --git a/engforge/attr_solver.py b/engforge/attr_solver.py index c6f5576..4da79f7 100644 --- a/engforge/attr_solver.py +++ b/engforge/attr_solver.py @@ -206,8 +206,6 @@ class Solver(ATTR_BASE): active: bool instance_class = SolverInstance - define = None - @classmethod def configure_for_system(cls, name, config_class, cb=None, **kwargs): """add the config class, and perform checks with `class_validate) diff --git a/engforge/configuration.py b/engforge/configuration.py index e9a4f2e..ec93767 100644 --- a/engforge/configuration.py +++ b/engforge/configuration.py @@ -166,25 +166,35 @@ def property_changed(instance, variable, value): if log.log_level <= 2: log.debug(f"already property changed {instance}{variable.name} {value}") return value + + # elif session: + # notify session that this variable has changed # log.info(f'property changed {variable.name} {value}') + # TODO: notify change input system here, perhaps with & without a session + # session.change_sys_var(variable,value,doset=False) + + attrs = attr.fields(instance.__class__) # check identity of variable + cur = getattr(instance, variable.name) + is_different = value != cur + is_var = variable in attrs + chgnw = instance._anything_changed if log.log_level <= 6: - log.debug(f"checking property changed {instance}{variable.name} {value}") + log.debug( + f"checking property changed {instance}{variable.name} {value}|invar: {is_var}| nteqval: {is_different}" + ) # Check if should be updated - cur = getattr(instance, variable.name) - attrs = attr.fields(instance.__class__) # check identity of variable - if not instance._anything_changed and variable in attrs and value != cur: + if not chgnw and is_var and is_different: if log.log_level < 5: log.debug(f"changing variables: {variable.name} {value}") instance._anything_changed = True - elif log.log_level < 4 and variable in attrs: - log.warning(f"didnt change variables {variable.name}| {value} == {cur}") + elif log.log_level < 4 and is_var and not is_different: + log.warning(f"variables same {variable.name}| {value} == {cur}") # If active session in dynamic mode and the component is dynamic, flag for matrix update # TODO: determine if dynamic matricies affected by this. - if session and session.dynamic_solve and instance.is_dynamic: # log.info(f'dynamics changed') session.dynamics_updated = True # flag for update @@ -209,6 +219,23 @@ def signals_slots_handler( log.debug(f"transforming signals and slots for {cls.__name__}") + # add attrs attributes from mro parent class + # TODO: work on this to allow attrs from non attrs classes + # fd = {f.name:f for f in fields} + # has_fields = set(list(fd.keys())) + # + # if cls._inherit_parent_attributres: + # non_dec_attrs = {} #get all attrs variables from non-decorated mixins + # for icls in cls.mro(): + # if attrs.has(icls): + # print(icls,str(list(attrs.fields_dict(icls).keys()))[:100]) + # atr_dict = {k:v.evolve(icls) for k,v in icls.__dict__.items() if isinstance(v,attrs.Attribute)} + # non_dec_attrs.update({k:v for k,v in atr_dict.items() if k not in non_dec_attrs and k not in has_fields}) + # if non_dec_attrs: + # log.info(f'adding non decorated attrs: {list(non_dec_attrs.keys())}') + # fields = fields + list(non_dec_attrs.values()) + + # Fields for t in fields: if t.name in PROTECTED_NAMES: raise Exception(f"cannot use {t.name} as a field name, its protected") @@ -353,6 +380,7 @@ class Configuration(AttributedBaseMixin): kw_only=True, ) + _inherit_parent_attributres = True log_fmt = "[%(name)-24s]%(message)s" log_silo = True @@ -516,7 +544,7 @@ def __attrs_post_init__(self): for compnm, comp in self.internal_configurations(False).items(): if isinstance(comp, Component): # TODO: allow multiple parents - if (not hasattr(comp, "parent")) and (comp.parent is not None): + if (hasattr(comp, "parent")) and (comp.parent is not None): self.warning( f"Component {compnm} already has a parent {comp.parent} copying, and assigning to {self}" ) @@ -528,14 +556,18 @@ def __attrs_post_init__(self): # subclass instance instance init causes conflicts in structures self.__on_init__() + runs = set((self.__class__.__on_init__,)) # keep track of unique functions if self._subclass_init: try: for comp in self.__class__.mro(): if ( hasattr(comp, "__on_init__") and comp.__on_init__ != Configuration.__on_init__ + and comp.__on_init__ not in runs ): comp.__on_init__(self) + runs.add(comp.__on_init__) + except Exception as e: self.error(e, f"error in __on_init__") diff --git a/engforge/dataframe.py b/engforge/dataframe.py index 8b688c0..a72bb28 100644 --- a/engforge/dataframe.py +++ b/engforge/dataframe.py @@ -98,7 +98,7 @@ def split_dataframe(df: pandas.DataFrame) -> tuple: for s in df: c = df[s] if is_uniform(c): - uniform[s] = c[0] + uniform[s] = c.iloc[0] df_unique = df.copy().drop(columns=list(uniform)) return uniform, df_unique if len(df_unique) > 0 else df_unique diff --git a/engforge/datastores/datastores_requirements.txt b/engforge/datastores/datastores_requirements.txt index f160d0e..686d697 100644 --- a/engforge/datastores/datastores_requirements.txt +++ b/engforge/datastores/datastores_requirements.txt @@ -29,6 +29,6 @@ pydrive2==1.8.1 boto3 #deco -ray[default]~=2.7.1 -ray[tune]~=2.7.1 +ray[default]~=2.35.0 +ray[tune]~=2.35.0 sqlalchemy-batch-inserts diff --git a/engforge/eng/costs.py b/engforge/eng/costs.py index 05b35a9..d116222 100644 --- a/engforge/eng/costs.py +++ b/engforge/eng/costs.py @@ -712,6 +712,8 @@ def _gather_cost_references(self, parent: "System"): child = comps[kbase] if ( isinstance(child, CostModel) + and hasattr(child.parent, "_slot_costs") + and child.parent._slot_costs and comp_key in child.parent._slot_costs ): self.debug(f"adding cost for {kbase}.{comp_key}") diff --git a/engforge/eng/structure.py b/engforge/eng/structure.py index 92a542b..440bc3b 100644 --- a/engforge/eng/structure.py +++ b/engforge/eng/structure.py @@ -32,7 +32,6 @@ import numpy as np from scipy import optimize as sciopt import pandas as pd -import ray import collections import numpy as np import itertools as itert @@ -43,6 +42,14 @@ from engforge.typing import Options from engforge.eng.structure_beams import Beam, rotation_matrix_from_vectors +ray_ok = False +try: + import ray + + ray_ok = True +except Exception as e: + pass + class StructureLog(LoggingMixin): pass @@ -118,7 +125,9 @@ def check_est_failures(failures: dict, top=True) -> bool: # Mesh Utils -def quad_stress_tensor(quad, r=0, s=0, combo="gravity"): +def quad_stress_tensor( + quad, r=0, s=0, combo="gravity", inf_val=1e24, filter_infeasable=True +): """determines stresses from a plate or quad""" q = quad S_xx, S_yy, Txy = q.membrane(r, s, combo) @@ -129,7 +138,18 @@ def quad_stress_tensor(quad, r=0, s=0, combo="gravity"): T_zx = float(Qx) / q.t T_yz = float(Qy) / q.t - return np.array([[S_xx, Txy, T_zx], [Txy, S_yy, T_yz], [T_zx, T_yz, 0.0]]) + out = np.array([[S_xx, Txy, T_zx], [Txy, S_yy, T_yz], [T_zx, T_yz, 0.0]]) + + if filter_infeasable: + # nan converted to zero + out[np.isnan(out)] = 0 + + # convert infite value to a maximum + infinx = np.isinf(out) + infs = out[infinx] + out[infinx] = np.sign(infs) * inf_val + + return out def node_pt(node): @@ -196,7 +216,8 @@ class Structure(System, CostModel, PredictionMixin): 1) Structure Motion (free body ie vehicle , multi-strucutre ie robots) """ - frame: pynite.FEModel3D = None + frame = None # pynite.FEModel3D + # _beams: dict = None _materials: dict = None @@ -251,14 +272,16 @@ class Structure(System, CostModel, PredictionMixin): _any_solved = False def __on_init__(self): + self.current_failure_summary = None + self._current_combo_failure_analysis = None self._materials = weakref.WeakValueDictionary() self._meshes = weakref.WeakValueDictionary() self.initalize_structure() self.frame.add_load_combo( self.default_combo, {self.gravity_name: 1.0}, "gravity" ) - self.create_structure() + # turn on prediction of failures with these models if self.prediction: d = { "fails": { @@ -271,6 +294,9 @@ def __on_init__(self): } self._prediction_models = d + # finally create the structure + self.create_structure() + def create_structure(self): """ Use this method to create a structure on init. @@ -626,7 +652,7 @@ def quad_info(self) -> dict: self._quad_info = {} for mn, mesh in self.Meshes.items(): for qn, q in mesh.elements.items(): - mat = self._materials[mesh.material] + mat = self._materials[mesh.material_name] self._quad_info[qn] = node_info(q, mat) return self._quad_info.copy() @@ -673,7 +699,7 @@ def apply_gravity_to_mesh(self, meshname): self.debug(f"applying gravity to {meshname}") mesh = self._meshes[meshname] - mat = self._materials[mesh.material] + mat = self._materials[mesh.material_name] rho = mat.rho for elid, elmt in mesh.elements.items(): @@ -815,34 +841,40 @@ def beam_dataframe(self, univ_parms: list = None, add_columns: list = None): """ df = self.dataframe - beam_col = set( - [c for c in df.columns if c.startswith("beams.") and len(c.split(".")) > 2] + + # determine the possible beam names + beams = set([f"beams_{k}" for k in self.members]) + beam_cols = df.columns[df.columns.str.startswith("beams_")].tolist() + beam_parms = set( + [ + col.replace(k + "_", "") + for col in beam_cols + for k in beams + if col.startswith(k + "_") + ] ) - beams = set([c.split(".")[1] for c in beam_col]) - parms = set([".".join(c.split(".")[2:]) for c in beam_col]) - # defaults if univ_parms is None: - univ_parms_ = df.columns[df.columns.str.startswith("beams.")].tolist() - univ_parms = [(c.split(".")[-1], c) for c in univ_parms_] - uniq_parms = set([c.split(".")[-1] for c in univ_parms_]) + univ_parms = beam_parms # do not filter parms if add_columns is None: add_columns = [] + # loop through parms and beams reorganizing the data: + # TODO: look up potential multi-index or pivot method for pandas beam_data = [] for i in range(len(df)): row = df.iloc[i] add_dat = {k: row[k] for k in add_columns} for beam in beams: bc = add_dat.copy() # this is the data entry - bc["name"] = bc - for parm in parms: - if parm not in uniq_parms: + bc["name"] = beam + for parm in beam_parms: + if parm not in univ_parms: continue # if parm not in row: # continue - k = f"beams.{beam}.{parm}" + k = f"{beam}_{parm}" if k in row: v = row[k] # if 'Z1' in k: @@ -1157,7 +1189,7 @@ def check_mesh_failures( quad_info = self.quad_info for meshname, mesh in self._meshes.items(): - matid = mesh.material + matid = mesh.material_name mat = self._materials[matid] allowable = mat.allowable_stress for quadname, quad in mesh.elements.items(): @@ -1432,7 +1464,9 @@ def current_failures( return summary # Failure Analysis Properties - @system_property + # FIXME: needs a complete rethink as far as caching and reporting properties, consider removing or replacing + # @system_property + @property def max_est_fail_frac(self) -> float: """estimated failure fraction for the current result""" fc = self._current_failures @@ -1441,7 +1475,8 @@ def max_est_fail_frac(self) -> float: self.warning(f"could not get estimated beam failure frac") return fc - @system_property + # @system_property + @property def max_beam_est_fail_frac(self) -> float: fc = self._current_failures if isinstance(fc, dict): @@ -1449,7 +1484,8 @@ def max_beam_est_fail_frac(self) -> float: self.warning(f"could not get estimated beam failure frac") return fc - @system_property + # @system_property + @property def max_mesh_est_fail_frac(self) -> float: fc = self._current_failures if isinstance(fc, dict): @@ -1457,7 +1493,8 @@ def max_mesh_est_fail_frac(self) -> float: self.warning(f"could not get estimated failure frac") return fc - @system_property + # @system_property + @property def estimated_failure_count(self) -> float: fc = self._current_failures if isinstance(fc, dict): @@ -1465,7 +1502,8 @@ def estimated_failure_count(self) -> float: self.warning("could not get estimated failure count") return fc - @system_property + # @system_property + @property def estimated_beam_failure_count(self) -> float: fc = self._current_failures if isinstance(fc, dict): @@ -1473,7 +1511,8 @@ def estimated_beam_failure_count(self) -> float: self.warning(f"could not get estimated beam failure count") return fc - @system_property + # @system_property + @property def estimated_mesh_failure_count(self) -> float: fc = self._current_failures if isinstance(fc, dict): @@ -1481,7 +1520,8 @@ def estimated_mesh_failure_count(self) -> float: self.warning(f"could not get estimated mesh failure count") return fc - @system_property + # @system_property + @property def actual_failure_count(self) -> float: if not self.calculate_actual_failure: return None @@ -1492,7 +1532,8 @@ def actual_failure_count(self) -> float: self.warning(f"could not get actual failure count") return afc - @system_property + # @system_property + @property def actual_beam_failure_count(self) -> float: if not self.calculate_actual_failure: return None @@ -1503,7 +1544,8 @@ def actual_beam_failure_count(self) -> float: self.warning(f"could not get actual beam failure count") return afc - @system_property + # @system_property + @property def actual_mesh_failure_count(self) -> float: if not self.calculate_actual_failure: return None @@ -1781,68 +1823,6 @@ def run_failure_sections( return secton_results -# Remote Capabilities -@ray.remote(max_calls=5) -def remote_run_combo(ref_structure, combo): - log = StructureLog() - try: - sync_ref = ref_structure["actor_ref"] - struct_ref = ref_structure["struct_ref"] - log.info(f"got {ref_structure}") - struct = ray.get(struct_ref) - # sync = ray.get(sync_ref) - sync = sync_ref - # try: - struct.resetSystemLogs() - log.info(f"start combo {combo}") - # log.info(struct.logger.__dict__) - struct.run(combos=combo) - log.info(f"combo done {combo}") - # assert len(struct.table) == 1, 'bad! sync only at beginning and end' - row = struct.table[1] - out = { - "row": row, - "D": struct._D[combo], - "combo": combo, - "st_id": struct.run_id, - } - log.info(f"putting results for {combo}") - d = sync.put_combo.remote(out) - ray.wait([d]) - return out - - except Exception as e: - log.error(e, "issue with remote run") - raise e - - -@ray.remote(max_retries=-1, retry_exceptions=True) -def remote_section( - beamsection, - beamname, - forces, - allowable_stress, - combo, - x, - SF=1.0, - return_section=False, -): - """optimized method to run 2d fea and return a failure determination""" - # TODO: allow post processing ops - s = beamsection.calculate_stress(**forces) - - sss = s.get_stress() - d_ = {"loads": forces, "beam": beamname, "combo": combo, "x": x} - if return_section: - d_["stress_analysis"] = s - d_["stress_results"] = sss - d_["stress_vm_max"] = max([max(ss["sig_vm"]) for ss in sss]) - d_["fail_frac"] = ff = max([max(ss["sig_vm"] / allowable_stress) for ss in sss]) - d_["fails"] = fail = ff > 1 / SF - - return d_ - - # Remote Analysis def parallel_run_failure_analysis(struct, SF=1.0, run_full=False, purge=False, **kw): """ @@ -1899,241 +1879,324 @@ def purge_combo(struct, combo): node.RZ.pop(combo, None) -@ray.remote -def parallel_combo_run(combo, ref_structure, run_full=False, SF=1.0): - # run and store data - # self.struct.run(combos=combo) - - log = StructureLog() - - sync = ref_structure["actor_ref"] - struct_ref = ref_structure["struct_ref"] - log.info(f"running structure {combo}") - d = remote_run_combo.remote(ref_structure, combo) - log.info(f"waiting on combo {combo}") - ray.wait([d]) - - # check failures - log.info(f"failure analysis {combo}") - fail = remote_combo_failure_analysis(ref_structure, combo, run_full, SF) - d = sync.put_failure.remote(combo, fail, run_full) - log.info(f"failure analysis done {combo}") - return ray.get(d) - - -# -# @ray.remote -# class RemoteStructuralAnalysis: -# """represents a stateful object in a ray cloud contetxt""" -# -# def __init__(self, structure, *kw): -# self.struct = structure -# self.struct.resetSystemLogs() -# self.struct.prep_remote_analysis() -# -# # reference tracking -# self.combos_run = {} -# self.failed = False -# self.report = {"fails": self.failed} -# self.report["failures"] = failures = {} -# self.failures = failures -# self.put_beams = {} -# -# # can run all combos from base -# self.struct_ref = ray.put(self.struct) -# -# # put beams sections for quick analysis -# for beamnm, beam in self.struct.beams.items(): -# beam_ref = ray.put(beam._section_properties) -# self.put_beams[beamnm] = beam_ref -# -# def get(self, run_id=None): -# self.struct.info(f"getting {len(self.table)}") -# if run_id is None: -# return self.struct._table -# else: -# return [v for v in self.table if v["st_id"] == run_id] -# -# def put_combo(self, inv): -# """add the data from remotely running the load combo""" -# -# self.struct.info(f"adding {len(self.struct.table)+1}") -# self.struct._table[len(self.struct.table) + 1] = inv["row"] -# self.struct.merge_displacement_results(inv["combo"], inv["D"]) -# self.combos_run[inv["combo"]] = inv -# self.struct.info(f'finished analysis of {inv["combo"]}') -# -# def put_failure(self, combo, fail, run_full=False): -# self.struct.info(f"finished failure calc of {combo}") -# self.failures[combo] = fail -# self.struct.failure_load_exithandler(combo, fail, self.report, run_full) -# -# async def wait_and_get_failure(self, combo, wait=1, timeout=None): -# if combo in self.failures: -# return self.failures[combo] -# -# timeelapsed = 0 -# -# # wait loop -# while combo not in self.failures: -# self.struct.info(f"waiting on {combo}") -# if timeout is not None and timeelapsed > timeout: -# raise TimeoutError(f"combo {combo} not found within timeout") -# await asyncio.sleep(wait) -# timeelapsed += wait -# -# return self.failures[combo] -# -# def get_beam_forces(self, beam_name, combo, x): -# beam = self.struct.beams[beam_name] -# return beam.get_forces_at(x, combo) -# -# def get_beam_info(self, beam_name): -# r = self.put_beams[beam_name] -# beam = self.struct.beams[beam_name] -# out = {"ref": r, "allowable": beam.material.allowable_stress} -# return out -# -# def estimated_failures(self, combos): -# return self.struct.estimated_failures_report(combos=combos) -# -# def return_structure(self): -# return self.struct -# -# async def ensure(self, combo, wait=1, timeout=None): -# if combo in self.combos_run: -# return # good to go -# -# timeelapsed = 0 -# -# # wait loop -# while combo not in self.combos_run: -# self.struct.info(f"waiting on {combo}") -# if timeout is not None and timeelapsed > timeout: -# raise TimeoutError(f"combo {combo} not found within timeout") -# await asyncio.sleep(wait) -# timeelapsed += wait -# -# return # good to go -# -# def run_failure_analysis(self, SF=1.0, run_full=False, **kw): -# """ -# Failure Determination: -# Beam Stress Estiates are used to determine if a 2D FEA beam/combo analysis should be run. The maximum beam vonmises stress is compared the the beam material allowable stress / saftey factor. -# -# Override: -# Depending on number of load cases, this analysis could take a while. Its encouraged to use a similar pattern of check and exit to reduce load combo size in an effort to fail early. Override this function if nessicary. -# -# Case / Beam Ordering: -# runs the gravity case first and checks for failure to exit early, -# then all other combos are run -# :returns: a dictionary with 2D fea failures and esimated failures as well for each beam -# """ -# ctx = ray.get_runtime_context() -# actor_handle = ctx.current_actor -# try: -# cases = [] -# ref = {"struct_ref": self.struct_ref, "actor_ref": actor_handle} -# # Run the rest of the load cases -# for combo in self.struct.failure_load_combos( -# self.report, run_full, remote_sync=actor_handle, **kw -# ): -# d = parallel_combo_run.remote( -# combo, ref, run_full=run_full, SF=SF -# ) -# cases.append(d) -# -# for coro in asyncio.as_completed(cases): -# if self.failed and not run_full: -# self.struct.warning(f"structure failed!") -# return self.report -# -# except KeyboardInterrupt: -# return self.report -# -# except Exception as e: -# self.struct.error(e, "issue in failur analysis") -# -# return self.report # tada! - - -# Remote Sync Util -def remote_combo_failure_analysis( - ref_structure, combo, run_full: bool = False, SF: float = 1.0 -): - """runs a single load combo and adds 2d section failures""" +if ray_ok: + # Remote Capabilities + @ray.remote(max_calls=5) + def remote_run_combo(ref_structure, combo): + log = StructureLog() + try: + sync_ref = ref_structure["actor_ref"] + struct_ref = ref_structure["struct_ref"] + log.info(f"got {ref_structure}") + struct = ray.get(struct_ref) + # sync = ray.get(sync_ref) + sync = sync_ref + # try: + struct.resetSystemLogs() + log.info(f"start combo {combo}") + # log.info(struct.logger.__dict__) + struct.run(combos=combo) + log.info(f"combo done {combo}") + # assert len(struct.table) == 1, 'bad! sync only at beginning and end' + row = struct.table[1] + out = { + "row": row, + "D": struct._D[combo], + "combo": combo, + "st_id": struct.run_id, + } + log.info(f"putting results for {combo}") + d = sync.put_combo.remote(out) + ray.wait([d]) + return out - sync = ref_structure["actor_ref"] - struct_ref = ref_structure["struct_ref"] + except Exception as e: + log.error(e, "issue with remote run") + raise e + + @ray.remote(max_retries=-1, retry_exceptions=True) + def remote_section( + beamsection, + beamname, + forces, + allowable_stress, + combo, + x, + SF=1.0, + return_section=False, + ): + """optimized method to run 2d fea and return a failure determination""" + # TODO: allow post processing ops + s = beamsection.calculate_stress(**forces) - log = StructureLog() + sss = s.get_stress() + d_ = {"loads": forces, "beam": beamname, "combo": combo, "x": x} + if return_section: + d_["stress_analysis"] = s + d_["stress_results"] = sss + d_["stress_vm_max"] = max([max(ss["sig_vm"]) for ss in sss]) + d_["fail_frac"] = ff = max([max(ss["sig_vm"] / allowable_stress) for ss in sss]) + d_["fails"] = fail = ff > 1 / SF - log.info(f"determine combo {combo} failures...") + return d_ - out = {} - d = sync.estimated_failures_report.remote(combos=combo) - failures = ray.get(d) + @ray.remote + def parallel_combo_run(combo, ref_structure, run_full=False, SF=1.0): + # run and store data + # self.struct.run(combos=combo) - out["estimate"] = failures - out["actual"] = check_fail = {} - # perform 2d analysis if any estimated - if check_est_failures(failures): - log.info(f"testing estimated failures...") + log = StructureLog() - dfail = remote_failure_sections(sync, failures, run_full, SF) - out["actual"] = dfail + sync = ref_structure["actor_ref"] + struct_ref = ref_structure["struct_ref"] + log.info(f"running structure {combo}") + d = remote_run_combo.remote(ref_structure, combo) + log.info(f"waiting on combo {combo}") + ray.wait([d]) - if dfail and not run_full: - log.info(f"found actual failure...") - return out - else: - log.info(f"no estimated failures found") + # check failures + log.info(f"failure analysis {combo}") + fail = remote_combo_failure_analysis(ref_structure, combo, run_full, SF) + d = sync.put_failure.remote(combo, fail, run_full) + log.info(f"failure analysis done {combo}") + return ray.get(d) + + # + # @ray.remote + # class RemoteStructuralAnalysis: + # """represents a stateful object in a ray cloud contetxt""" + # + # def __init__(self, structure, *kw): + # self.struct = structure + # self.struct.resetSystemLogs() + # self.struct.prep_remote_analysis() + # + # # reference tracking + # self.combos_run = {} + # self.failed = False + # self.report = {"fails": self.failed} + # self.report["failures"] = failures = {} + # self.failures = failures + # self.put_beams = {} + # + # # can run all combos from base + # self.struct_ref = ray.put(self.struct) + # + # # put beams sections for quick analysis + # for beamnm, beam in self.struct.beams.items(): + # beam_ref = ray.put(beam._section_properties) + # self.put_beams[beamnm] = beam_ref + # + # def get(self, run_id=None): + # self.struct.info(f"getting {len(self.table)}") + # if run_id is None: + # return self.struct._table + # else: + # return [v for v in self.table if v["st_id"] == run_id] + # + # def put_combo(self, inv): + # """add the data from remotely running the load combo""" + # + # self.struct.info(f"adding {len(self.struct.table)+1}") + # self.struct._table[len(self.struct.table) + 1] = inv["row"] + # self.struct.merge_displacement_results(inv["combo"], inv["D"]) + # self.combos_run[inv["combo"]] = inv + # self.struct.info(f'finished analysis of {inv["combo"]}') + # + # def put_failure(self, combo, fail, run_full=False): + # self.struct.info(f"finished failure calc of {combo}") + # self.failures[combo] = fail + # self.struct.failure_load_exithandler(combo, fail, self.report, run_full) + # + # async def wait_and_get_failure(self, combo, wait=1, timeout=None): + # if combo in self.failures: + # return self.failures[combo] + # + # timeelapsed = 0 + # + # # wait loop + # while combo not in self.failures: + # self.struct.info(f"waiting on {combo}") + # if timeout is not None and timeelapsed > timeout: + # raise TimeoutError(f"combo {combo} not found within timeout") + # await asyncio.sleep(wait) + # timeelapsed += wait + # + # return self.failures[combo] + # + # def get_beam_forces(self, beam_name, combo, x): + # beam = self.struct.beams[beam_name] + # return beam.get_forces_at(x, combo) + # + # def get_beam_info(self, beam_name): + # r = self.put_beams[beam_name] + # beam = self.struct.beams[beam_name] + # out = {"ref": r, "allowable": beam.material.allowable_stress} + # return out + # + # def estimated_failures(self, combos): + # return self.struct.estimated_failures_report(combos=combos) + # + # def return_structure(self): + # return self.struct + # + # async def ensure(self, combo, wait=1, timeout=None): + # if combo in self.combos_run: + # return # good to go + # + # timeelapsed = 0 + # + # # wait loop + # while combo not in self.combos_run: + # self.struct.info(f"waiting on {combo}") + # if timeout is not None and timeelapsed > timeout: + # raise TimeoutError(f"combo {combo} not found within timeout") + # await asyncio.sleep(wait) + # timeelapsed += wait + # + # return # good to go + # + # def run_failure_analysis(self, SF=1.0, run_full=False, **kw): + # """ + # Failure Determination: + # Beam Stress Estiates are used to determine if a 2D FEA beam/combo analysis should be run. The maximum beam vonmises stress is compared the the beam material allowable stress / saftey factor. + # + # Override: + # Depending on number of load cases, this analysis could take a while. Its encouraged to use a similar pattern of check and exit to reduce load combo size in an effort to fail early. Override this function if nessicary. + # + # Case / Beam Ordering: + # runs the gravity case first and checks for failure to exit early, + # then all other combos are run + # :returns: a dictionary with 2D fea failures and esimated failures as well for each beam + # """ + # ctx = ray.get_runtime_context() + # actor_handle = ctx.current_actor + # try: + # cases = [] + # ref = {"struct_ref": self.struct_ref, "actor_ref": actor_handle} + # # Run the rest of the load cases + # for combo in self.struct.failure_load_combos( + # self.report, run_full, remote_sync=actor_handle, **kw + # ): + # d = parallel_combo_run.remote( + # combo, ref, run_full=run_full, SF=SF + # ) + # cases.append(d) + # + # for coro in asyncio.as_completed(cases): + # if self.failed and not run_full: + # self.struct.warning(f"structure failed!") + # return self.report + # + # except KeyboardInterrupt: + # return self.report + # + # except Exception as e: + # self.struct.error(e, "issue in failur analysis") + # + # return self.report # tada! + + # Remote Sync Util + def remote_combo_failure_analysis( + ref_structure, combo, run_full: bool = False, SF: float = 1.0 + ): + """runs a single load combo and adds 2d section failures""" - return out + sync = ref_structure["actor_ref"] + struct_ref = ref_structure["struct_ref"] + log = StructureLog() -def remote_failure_sections( - sync, - failures, - run_full: bool = False, - SF: float = 1.0, - fail_fast=True, - group_size=12, -): - """takes estimated failures and runs 2D section FEA on those cases""" - log = StructureLog() + log.info(f"determine combo {combo} failures...") - secton_results = {} - skip_beams = set() - cur = [] + out = {} + d = sync.estimated_failures_report.remote(combos=combo) + failures = ray.get(d) - for (beamnm, c, x), dat in sort_max_estimated_failures(failures): - log.info( - f'run remote beam sections: {beamnm},{c} @ {x} {dat["fail_frac"]*100.}%' - ) - if beamnm in skip_beams: - continue + out["estimate"] = failures + out["actual"] = check_fail = {} + # perform 2d analysis if any estimated + if check_est_failures(failures): + log.info(f"testing estimated failures...") - # prep for output - if beamnm not in secton_results: - secton_results[beamnm] = {} + dfail = remote_failure_sections(sync, failures, run_full, SF) + out["actual"] = dfail - # get from sync - ray.get(sync.ensure.remote(c)) - d = sync.get_beam_forces.remote(beamnm, c, x) - r = sync.get_beam_info.remote(beamnm) - forces, beam_ref = ray.get([d, r]) - beam_ref = r["ref"] - allowable = r["allowable"] + if dfail and not run_full: + log.info(f"found actual failure...") + return out + else: + log.info(f"no estimated failures found") - cur.append( - remote_section.remote(beam_ref, beamnm, forces, allowable, c, x, SF=SF) - ) + return out - # run the damn thing - if len(cur) >= group_size: - log.info(f"waiting on {group_size}") + def remote_failure_sections( + sync, + failures, + run_full: bool = False, + SF: float = 1.0, + fail_fast=True, + group_size=12, + ): + """takes estimated failures and runs 2D section FEA on those cases""" + log = StructureLog() + + secton_results = {} + skip_beams = set() + cur = [] + + for (beamnm, c, x), dat in sort_max_estimated_failures(failures): + log.info( + f'run remote beam sections: {beamnm},{c} @ {x} {dat["fail_frac"]*100.}%' + ) + if beamnm in skip_beams: + continue + + # prep for output + if beamnm not in secton_results: + secton_results[beamnm] = {} + + # get from sync + ray.get(sync.ensure.remote(c)) + d = sync.get_beam_forces.remote(beamnm, c, x) + r = sync.get_beam_info.remote(beamnm) + forces, beam_ref = ray.get([d, r]) + beam_ref = r["ref"] + allowable = r["allowable"] + + cur.append( + remote_section.remote(beam_ref, beamnm, forces, allowable, c, x, SF=SF) + ) + + # run the damn thing + if len(cur) >= group_size: + log.info(f"waiting on {group_size}") + ray.wait(cur) + + for res in ray.get(cur): + fail = res["fails"] + beamnm = res["beam"] + combo = res["combo"] + x = res["x"] + secton_results[res["beam"]][(combo, x)] = res + + if fail: + log.warning(f"beam {beamnm} failed @ {x*100:3.0f}%| {c}") + if fail_fast: + return secton_results + if not run_full: + skip_beams.add(beamnm) + # else: + # log.info(f'beam {beamnm} ok @ {x*100:3.0f}%| {c}') + + cur = [] + log.info(f"done waiting, continue...") + + # finish them! + if cur: + log.info(f"waiting on {len(cur)}") ray.wait(cur) + log.info(f"done, continue...") for res in ray.get(cur): fail = res["fails"] @@ -2151,29 +2214,4 @@ def remote_failure_sections( # else: # log.info(f'beam {beamnm} ok @ {x*100:3.0f}%| {c}') - cur = [] - log.info(f"done waiting, continue...") - - # finish them! - if cur: - log.info(f"waiting on {len(cur)}") - ray.wait(cur) - log.info(f"done, continue...") - - for res in ray.get(cur): - fail = res["fails"] - beamnm = res["beam"] - combo = res["combo"] - x = res["x"] - secton_results[res["beam"]][(combo, x)] = res - - if fail: - log.warning(f"beam {beamnm} failed @ {x*100:3.0f}%| {c}") - if fail_fast: - return secton_results - if not run_full: - skip_beams.add(beamnm) - # else: - # log.info(f'beam {beamnm} ok @ {x*100:3.0f}%| {c}') - - return secton_results + return secton_results diff --git a/engforge/engforge_attributes.py b/engforge/engforge_attributes.py index c605ad0..f743879 100644 --- a/engforge/engforge_attributes.py +++ b/engforge/engforge_attributes.py @@ -260,6 +260,7 @@ def input_as_dict(self): @property def numeric_as_dict(self): + """recursively gets internal components numeric_as_dict as well as its own numeric values""" from engforge.configuration import Configuration o = {k: getattr(self, k, None) for k in self.numeric_fields()} @@ -270,20 +271,31 @@ def numeric_as_dict(self): return o # Hashes + # TODO: issue with logging sub-items + def hash_with(self, **input_kw): + d = self.as_dict + d.update(input_kw) + return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d] + + def hash_numeric_with(self, **input_kw): + d = self.numeric_as_dict + d.update(input_kw) + return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d] + @property def unique_hash(self): d = self.as_dict - return deepdiff.DeepHash(d)[d] + return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d] @property - def numeric_hash(self): + def input_hash(self): d = self.input_as_dict - return deepdiff.DeepHash(d)[d] + return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d] @property def numeric_hash(self): d = self.numeric_as_dict - return deepdiff.DeepHash(d)[d] + return deepdiff.DeepHash(d, ignore_encoding_errors=True)[d] # Configuration Push/Pop methods def setattrs(self, dict): diff --git a/engforge/locations.py b/engforge/locations.py index 8bb5b4c..a36e1cc 100644 --- a/engforge/locations.py +++ b/engforge/locations.py @@ -1,14 +1,17 @@ from engforge.env_var import EnvVariable +import os -FORGE_PATH_VAR = EnvVariable("FORGE_REPORT_PATH", default=None, dontovrride=True) +FORGE_ROOT_VAR = EnvVariable( + "FORGE_ROOT", default=None, dontovrride=True, type_conv=os.path.expanduser +) def client_path(alternate_path=None, **kw): - path = FORGE_PATH_VAR.secret + path = FORGE_ROOT_VAR.secret if path is None: if alternate_path is None: raise KeyError( - f"no `FORGE_REPORT_PATH` set and no alternate path in client_path call " + f"no `FORGE_ROOT` set and no alternate path in client_path call" ) return alternate_path else: diff --git a/engforge/logging.py b/engforge/logging.py index 4d1dbf5..e1eab61 100644 --- a/engforge/logging.py +++ b/engforge/logging.py @@ -75,7 +75,7 @@ def logger(self): LoggingMixin.slack_webhook_url = SLACK_WEBHOOK if not hasattr(self, "_f_change_log"): - + # change log level listener def _change_log(new_level, check_function=None): if new_level != self.log_level: if check_function is None or check_function(self): @@ -86,6 +86,7 @@ def _change_log(new_level, check_function=None): self.log_level = new_level self.resetLog() + # our emitter gets set with a callback log_change_emitter.add_listener("change_level", _change_log) self._f_change_log = _change_log diff --git a/engforge/problem_context.py b/engforge/problem_context.py index 1bf18ae..93b2d2f 100644 --- a/engforge/problem_context.py +++ b/engforge/problem_context.py @@ -199,6 +199,8 @@ def __str__(self) -> str: # TODO: develop subproblem strategy (multiple root cache problem in class cache) # TODO: determine when components are updated, and refresh the system references accordingly. # TODO: Map attributes/properties by component key and then autofix refs! (this is a big one), no refresh required. Min work +# TODO: component graph with pyee anything changed system to make a lazy observer system. +# TODO: plot levels to manage report output (with pdf graph publishing) class ProblemExec: """ Represents the execution context for a problem in the system. The ProblemExec class 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. @@ -547,6 +549,43 @@ def get_sesh(self, sesh=None): self.inst_sesh = out return out + # @classmethod + # def cls_get_sesh(cls, sesh=None): + # """get the session""" + # out = sesh + # if not sesh: + # if hasattr(cls.class_cache, "session"): + # out = self.class_cache.session + # elif self._problem_id == True: + # out = self + # if out: + # self.inst_sesh = out + # return out + + # @property + # def index(self): + # sesh = self.get_sesh() + # if not sesh._data: + # return 0 + # else: + # return max(list(sesh._data.keys())) + # + # @property + # def last_index(self): + # sesh = self.index + # if sesh == 0: + # return None + # else: + # return sesh - 1 + # + # @property + # def next_index(self): + # sesh = self.index + # if sesh == 0: + # return 1 + # else: + # return sesh + 1 + # Update Methods def refresh_references(self, sesh=None): """refresh the system references""" @@ -593,6 +632,7 @@ def min_refresh(self, sesh=None): sesh._all_refs = sesh.system.system_references( recache=True, check_config=False, ignore_none_comp=False ) + # sesh._attr_sys_key_map = sesh.attribute_sys_key_map # Problem Variable Definitions sesh.Xref = sesh.all_problem_vars @@ -660,7 +700,7 @@ def __enter__(self): f"creating execution context for {self.system}| {self._slv_kw}| {refs}" ) - return self + return self # return the local problem context, use self.sesh to get top values def __exit__(self, exc_type, exc_value, traceback): # define exit action, to handle the error here return True. Otherwise error propigates up to top level @@ -1095,7 +1135,7 @@ def solve_min(self, Xref=None, Yref=None, output=None, **kw): output = dflt if len(Xref) == 0: - self.info(f"no variables found for solver: {kw}") + self.debug(f"no variables found for solver: {kw}") # None for `ans` will not trigger optimization failure return output @@ -1582,7 +1622,12 @@ def record_state(self) -> dict: """records the state of the system using session""" # refs = self.all_variable_refs sesh = self.sesh - refs = sesh.all_comps_and_vars # no need for properties + # only get used refs modified no need for properties + # TODO: more elegant solution + chk = self.temp_state if self.temp_state else {} + # FIXME: only record state as it changes + # refs = {k:v for k,v in sesh.all_comps_and_vars.items()} # if k in chk + refs = {k: v for k, v in sesh.all_comps_and_vars.items()} return Ref.refset_get(refs, sys=sesh.system, prob=self) @property @@ -1618,11 +1663,41 @@ def get_ref_values(self, refs=None): def set_ref_values(self, values, refs=None): """returns the values of the refs""" # TODO: add checks for the refs - sesh = self.sesh if refs is None: + sesh = self.sesh refs = sesh.all_comps_and_vars return Ref.refset_input(refs, values) + def change_sys_var(self, key, value, refs=None, doset=True, attr_key_map=None): + """use this function to change the value of a system var and update the start state, multiple uses in the same context will not change the record preserving the start value + + :param key: a string corresponding to a ref, or an `attrs.Attribute` of one of the system or its component's. + """ + if self.log_level < 5: + self.msg(f"setting var: {key} <= {value}") + + # if isinstance(key,attrs.Attribute): + # if attr_key_map is None: + # attr_key_map = self.attribute_sys_key_map + # key = attr_key_map[key] #change to system key format + + if refs is None: + refs = self.sesh.all_comps_and_vars + if key in refs: + ref = refs[key] + if key not in self.x_start: + cur_value = ref.value() + self.x_start[key] = cur_value + if self.log_level < 5: + self.msg(f"setting var: {key} <= {value} from {cur_value}") + if doset: + ref.set_value(value) + elif isinstance(key, Ref): + ref = key + self.x_start[key] = key.value() + if doset: + ref.set_value(value) + def set_checkpoint(self): """sets the checkpoint""" self.x_start = self.record_state @@ -1934,12 +2009,21 @@ def integrator_var_refs(self): # Dataframe support @property - def dataframe(self) -> pd.DataFrame: - """returns the dataframe of the system""" + def numeric_data(self): + """return a list of sorted data rows by item and filter each row to remove invalid data""" sesh = self.sesh - res = pd.DataFrame( - [kv[-1] for kv in sorted(sesh.data.items(), key=lambda kv: kv[0])] + filter_non_numeric = lambda kv: ( + False if isinstance(kv[1], (list, dict, tuple)) else True ) + f_numrow = lambda in_dict: dict(filter(filter_non_numeric, in_dict.items())) + return [ + f_numrow(kv[-1]) for kv in sorted(sesh.data.items(), key=lambda kv: kv[0]) + ] + + @property + def dataframe(self) -> pd.DataFrame: + """returns the dataframe of the system""" + res = pd.DataFrame(self.numeric_data) self.system.format_columns(res) return res @@ -2031,6 +2115,17 @@ def all_comps_and_vars(self) -> dict: attrs.update(comps) return attrs + @property + def attribute_sys_key_map(self) -> dict: + """returns an attribute:key mapping to lookup the key from the attribute""" + sesh = self.sesh + attrvars = sesh.all_refs["attributes"] + comps = {c: k for k, c in self.all_components.items()} + return { + refget_attr(v): refget_key(v, sesh.system, comps) + for k, v in attrvars.items() + } + @property def all_system_references(self) -> dict: sesh = self.sesh @@ -2045,6 +2140,12 @@ def __str__(self): return f"ProblemContext[{self.level_name:^12}][{str(self.session_id)[0:8]}-{str(self._problem_id)[0:8]}][{self.system.identity}]" +refget_attr = lambda ref: getattr(ref.comp.__class__.__attrs_attrs__, ref.key) +###FIXME: ref.comp needs to be reliablly in comps +refget_key = ( + lambda ref, slf, comps: f'{comps[ref.comp]+"." if slf != ref.comp else ""}{ref.key}' +) + # TODO: move all system_reference concept inside problem context, remove from system/tabulation ect. # TODO: use prob.register/change(comp,key='') to add components to the problem context, mapping subcomponents to the problem context # TODO: make a graph of all problem dependencies and use that to determine the order of operations, and subsequent updates. diff --git a/engforge/properties.py b/engforge/properties.py index 3256b81..ed4dcd4 100644 --- a/engforge/properties.py +++ b/engforge/properties.py @@ -23,12 +23,19 @@ class PropertyLog(LoggingMixin): class engforge_prop: - """an interface for extension and identification and class return support""" + """ + an interface for extension and identification and class return support + + Use as follows: + @engforge_prop + def our_custom_function(self): + pass + """ must_return = False def __init__(self, fget=None, fset=None, fdel=None, *args, **kwargs): - """ """ + """call with the function you want to wrap in a decorator""" self.fget = fget if fget: diff --git a/engforge/solver.py b/engforge/solver.py index 6ba4f9f..c192579 100644 --- a/engforge/solver.py +++ b/engforge/solver.py @@ -216,7 +216,8 @@ def eval(self, Xo=None, eval_kw: dict = None, sys_kw: dict = None, cb=None, **kw Xnew=Xo, ) as pbx: out = self.execute(**kw) - pbx.save_data() # context handles checking if anything changed + # pbx.save_data(force=True) # context handles checking if anything changed + pbx.save_data() pbx.exit_to_level(level="eval", revert=False) if self.log_level >= 20: diff --git a/engforge/system_reference.py b/engforge/system_reference.py index daea179..ec17780 100644 --- a/engforge/system_reference.py +++ b/engforge/system_reference.py @@ -25,6 +25,10 @@ class RefLog(LoggingMixin): def refset_input(refs, delta_dict, chk=True, fail=True, warn=True): """change a set of refs with a dictionary of values. If chk is True k will be checked for membership in refs""" for k, v in delta_dict.items(): + if isinstance(k, Ref): + k.set_value(v) + continue + memb = k in refs if not chk or memb: refs[k].set_value(v) @@ -131,6 +135,7 @@ class Ref: "_log_func", "hxd", "_name", + "attr_type", ] comp: "TabulationMixin" key: str @@ -141,12 +146,15 @@ class Ref: key_override: bool _value_eval: callable _log_func: callable + attr_type: type def __init__(self, comp, key, use_call=True, allow_set=True, eval_f=None): self.set(comp, key, use_call, allow_set, eval_f) def set(self, comp, key, use_call=True, allow_set=True, eval_f=None): # key can be a ref, in which case this ref will be identical to the other ref except for the component provided if it is not None + from engforge.engforge_attributes import AttributedBaseMixin + if isinstance(key, Ref): self.__dict__.update(key.__dict__) if comp is not None: @@ -176,6 +184,13 @@ def set(self, comp, key, use_call=True, allow_set=True, eval_f=None): if not self.use_dict: self._name = self.comp.classname + self.attr_type = None + if self.allow_set and isinstance(self.comp, AttributedBaseMixin): + fieldz = self.comp.__class__.cls_all_attrs_fields() + atr_canidate = fieldz.get(self.key, None) + if isinstance(atr_canidate, attrs.Attribute): + self.attr_type = atr_canidate.type + if not hasattr(self, "_name"): self._name = "NULL" @@ -241,24 +256,31 @@ def value(self, *args, **kw): def set_value(self, val): if self.allow_set: - if self.value() != val: # this increases perf. by reducing writes + if self.attr_type not in (str, float, int, bool): + if self.comp and self.comp.log_level < 10: + self.comp.msg(f"REF[set] {self} <- {val}") + return setattr(self.comp, self.key, val) + elif self.value() != val: # this increases perf. by reducing writes if self.comp and self.comp.log_level < 10: self.comp.msg(f"REF[set] {self} <- {val}") return setattr(self.comp, self.key, val) else: raise Exception(f"not allowed to set value on {self.key}") + @property + def full_key(self): + if self.key_override: + return f"{self._name}.{self.key.__name__}" + else: + return f"{self._name}.{self.key}" + def __str__(self) -> str: if self.use_dict: return f"REF[{self.hxd}][DICT.{self.key}]" - if self.key_override: - return f"REF[{self.hxd}][{self._name}.{self.key.__name__}]" - return f"REF[{self.hxd}][{self._name}.{self.key}]" + return f"REF[{self.hxd}][{self.full_key}]" def __repr__(self) -> str: - if self.key_override: - return f"REF[{self.hxd}][{self._name}.{self.key.__name__}]" - return f"REF[{self.hxd}][{self._name}.{self.key}]" + return f"REF[{self.hxd}][{self.full_key}]" # Utilty Methods refset_get = refset_get diff --git a/engforge/tabulation.py b/engforge/tabulation.py index a320476..5c08f98 100644 --- a/engforge/tabulation.py +++ b/engforge/tabulation.py @@ -50,6 +50,15 @@ class TabulationMixin(SolveableMixin, DataframeMixin): _anything_changed: bool = True _always_save_data = False + def __getstate__(self): + """remove references and storage of properties, be sure to call super if overwriting this function in your subclass""" + vs = super().__getstate__() + pir = vs.pop("_prv_internal_references", None) + pir = vs.pop("_system_properties_def", None) + pir = vs.pop("parent", None) + + return vs + @property def anything_changed(self): """use the on_setattr method to determine if anything changed, @@ -70,7 +79,9 @@ def last_context(self): """Returns the last context""" raise NotImplemented("this should be implemented in the solvable class") - @solver_cached + # TODO: create an intelligent graph informed anything_changed alerting system (pyee?) and trigger solver_cache expirations appropriately + # @solver_cached #FIXME: not caching correctly + @property # FIXME: this is slow def dataframe(self): if hasattr(self, "last_context") and hasattr(self.last_context, "dataframe"): return self.last_context.dataframe diff --git a/test/__init__.py b/engforge/test/__init__.py similarity index 51% rename from test/__init__.py rename to engforge/test/__init__.py index 30b372e..1e47b5d 100644 --- a/test/__init__.py +++ b/engforge/test/__init__.py @@ -1,4 +1,5 @@ import sys -import os,pathlib +import os, pathlib + test_dir = pathlib.Path(__file__).parent -sys.path.insert(0,test_dir) \ No newline at end of file +sys.path.insert(0, test_dir) diff --git a/test/report_testing.py b/engforge/test/report_testing.py similarity index 100% rename from test/report_testing.py rename to engforge/test/report_testing.py diff --git a/test/test_airfilter.py b/engforge/test/test_airfilter.py similarity index 84% rename from test/test_airfilter.py rename to engforge/test/test_airfilter.py index 08407ec..f09f354 100644 --- a/test/test_airfilter.py +++ b/engforge/test/test_airfilter.py @@ -28,23 +28,23 @@ def setUp(self): def test_plot(self): N = 10 - self.af.run(throttle=np.linspace(0, 1, N),combos='*',slv_vars='*') + self.af.run(throttle=np.linspace(0, 1, N), combos="*", slv_vars="*") fig = self.af.flow_curve() self.assertIsNotNone(fig) df = self.af.dataframe - self.assertEqual(df.shape[0],N) + self.assertEqual(df.shape[0], N) dfv = self.af.dataframe_variants - self.assertEqual(dfv.shape[0],N) - self.assertIn('w',dfv.columns) - self.assertIn('throttle',dfv.columns) - + self.assertEqual(dfv.shape[0], N) + self.assertIn("w", dfv.columns) + self.assertIn("throttle", dfv.columns) dfc = self.af.dataframe_constants - self.assertIsInstance(dfc,dict) + self.assertIsInstance(dfc, dict) + + self.assertEqual(df.shape[1], len(dfc) + dfv.shape[1]) - self.assertEqual(df.shape[1],len(dfc)+dfv.shape[1]) class TestAnalysis(unittest.TestCase): def setUp(self): @@ -61,22 +61,21 @@ def test_plot(self): self.assertIsNotNone(ofig) - # #Run the system # from matplotlib.pylab import * -# -# -# +# +# +# # fan = Fan() # filt = Filter() # af = Airfilter(fan=fan,filt=filt) -# +# # change_all_log_levels(af,20) #info -# +# # af.run(throttle=list(np.arange(0.1,1.1,0.1)),combos='*') -# +# # df = af.dataframe -# +# # fig,(ax,ax2) = subplots(2,1) # ax.plot(df.throttle*100,df.w,'k--',label='flow') # ax2.plot(df.throttle*100,df.filt_dp_filter,label='filter') @@ -89,4 +88,4 @@ def test_plot(self): # ax2.grid() # ax2.set_title(f'pressure') # ax2.set_xlabel(f'throttle%') dfv = self.af.dataframe_variants -# self.assertEqual(dfv.shape[0],N) \ No newline at end of file +# self.assertEqual(dfv.shape[0],N) diff --git a/test/test_analysis.py b/engforge/test/test_analysis.py similarity index 63% rename from test/test_analysis.py rename to engforge/test/test_analysis.py index 32465e8..ace0c29 100644 --- a/test/test_analysis.py +++ b/engforge/test/test_analysis.py @@ -1,3 +1,3 @@ -from test_airfilter import * +from engforge.test.test_airfilter import * from engforge.analysis import Analysis from engforge.attr_slots import Slot diff --git a/test/test_comp_iter.py b/engforge/test/test_comp_iter.py similarity index 100% rename from test/test_comp_iter.py rename to engforge/test/test_comp_iter.py diff --git a/test/test_composition.py b/engforge/test/test_composition.py similarity index 100% rename from test/test_composition.py rename to engforge/test/test_composition.py diff --git a/test/test_costs.py b/engforge/test/test_costs.py similarity index 100% rename from test/test_costs.py rename to engforge/test/test_costs.py diff --git a/test/test_dynamics.py b/engforge/test/test_dynamics.py similarity index 100% rename from test/test_dynamics.py rename to engforge/test/test_dynamics.py diff --git a/test/test_dynamics_spaces.py b/engforge/test/test_dynamics_spaces.py similarity index 100% rename from test/test_dynamics_spaces.py rename to engforge/test/test_dynamics_spaces.py diff --git a/test/test_four_bar.py b/engforge/test/test_four_bar.py similarity index 87% rename from test/test_four_bar.py rename to engforge/test/test_four_bar.py index e705af2..3bdefef 100644 --- a/test/test_four_bar.py +++ b/engforge/test/test_four_bar.py @@ -76,17 +76,22 @@ def l3_zero(self) -> float: return self.r3_x_zero**2 + self.y_zero**2 @system_prop - def l3_gamma(self)-> float: - return (self.r3_x**2 + self.y**2) - + def l3_gamma(self) -> float: + return self.r3_x**2 + self.y**2 -if __name__ == '__main__': - #%run -i ~/engforge/engforge/test/test_four_bar.py + +if __name__ == "__main__": + # %run -i ~/engforge/engforge/test/test_four_bar.py import numpy as np fb = FourBar() - fb.run(combos='*',revert_last=False,revert_every=False) + fb.run(combos="*", revert_last=False, revert_every=False) df = fb.last_context.dataframe - fb.run(combos='goal,gamma,theta,lim',theta=np.linspace(0,3.14159),revert_last=False,revert_every=False) + fb.run( + combos="goal,gamma,theta,lim", + theta=np.linspace(0, 3.14159), + revert_last=False, + revert_every=False, + ) diff --git a/test/test_modules.py b/engforge/test/test_modules.py similarity index 100% rename from test/test_modules.py rename to engforge/test/test_modules.py diff --git a/test/test_performance.py b/engforge/test/test_performance.py similarity index 100% rename from test/test_performance.py rename to engforge/test/test_performance.py diff --git a/test/test_pipes.py b/engforge/test/test_pipes.py similarity index 100% rename from test/test_pipes.py rename to engforge/test/test_pipes.py diff --git a/engforge/test/test_problem.py b/engforge/test/test_problem.py new file mode 100644 index 0000000..5c57223 --- /dev/null +++ b/engforge/test/test_problem.py @@ -0,0 +1,411 @@ +import unittest +from engforge.problem_context import * +from engforge.system import System, forge +from engforge._testing_components import * +import random + + +@forge(auto_attribs=True) +class SimpleContext(System): + one: int = 1 + two: int = 2 + + def set_rand(self): + self.one = random.random() + self.two = random.random() + + def __str__(self): + return f"{self.one}_{self.two}" + + +chk = lambda d, k: set(d.get(k)) if k in d else set() + + +class TestSession(unittest.TestCase): + """Test lifecycle of a problem and IO of the context""" + + def test_system_last_context(self): + sm = SpringMass(Fa=0, u=5) + sm.run(dxdt=0, combos="time") + ssid = sm.last_context.session_id + sm.run(dxdt=0, combos="time") + trid = sm.last_context.session_id + self.assertNotEqual(ssid, trid, "Session ID should change after a run") + + def test_system_change_context(self): + sm = SpringMass(Fa=0, u=5) + sm.run(dxdt=0, combos="time") + ssid = sm.last_context.session_id + trsm, df = sm.simulate(dt=0.01, endtime=0.1, return_all=True) + trid = trsm.last_context.session_id + self.assertNotEqual(ssid, trid, "Session ID should change after a run") + + def test_slide_crank_empty(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec(sm, {"combos": "", "slv_vars": "", "dxdt": None}) + atx = pbx.ref_attrs + self.assertEqual(chk(atx, "solver.var"), set()) + self.assertEqual(chk(atx, "solver.obj"), set()) + self.assertEqual(chk(atx, "solver.ineq"), set()) + self.assertEqual(chk(atx, "solver.eq"), set()) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set()) + self.assertEqual(chk(atx, "dynamics.state"), set()) + self.assertEqual(chk(atx, "dynamics.input"), set()) + + cons = pbx.constraints + self.assertEqual(cons["constraints"], []) + self.assertEqual(len(cons["bounds"]), len(atx["solver.var"])) + + def test_slide_crank_dflt(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec(sm, {"dxdt": None}) + atx = pbx.ref_attrs + self.assertEqual(chk(atx, "solver.var"), set()) + self.assertEqual(chk(atx, "solver.obj"), set()) + self.assertEqual(chk(atx, "solver.ineq"), set()) + self.assertEqual(chk(atx, "solver.eq"), set()) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set()) + self.assertEqual(chk(atx, "dynamics.state"), set()) + self.assertEqual(chk(atx, "dynamics.input"), set()) + + cons = pbx.constraints + self.assertEqual(len(cons["constraints"]), 0) + self.assertEqual(len(cons["bounds"]), 0) + + def test_slide_crank_dxdt_true(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec(sm, {"dxdt": True}) + atx = pbx.ref_attrs + self.assertEqual(chk(atx, "solver.var"), set()) + self.assertEqual(chk(atx, "solver.obj"), set()) + self.assertEqual(chk(atx, "solver.ineq"), set()) + self.assertEqual(chk(atx, "solver.eq"), set()) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set(("omega", "theta"))) + self.assertEqual(chk(atx, "dynamics.state"), set(("omega", "theta"))) + self.assertEqual(chk(atx, "dynamics.input"), set(("Tg",))) + + cons = pbx.constraints + self.assertEqual(cons["constraints"], []) + self.assertEqual(len(cons["bounds"]), len(atx["solver.var"])) + + def test_slide_crank_dxdt_zero(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec(sm, {"dxdt": 0}) + atx = pbx.ref_attrs + self.assertEqual(chk(atx, "solver.var"), set()) + self.assertEqual(chk(atx, "solver.obj"), set()) + self.assertEqual(chk(atx, "solver.ineq"), set()) + self.assertEqual(chk(atx, "solver.eq"), set()) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set(("omega", "theta"))) + self.assertEqual(chk(atx, "dynamics.state"), set(("omega", "theta"))) + self.assertEqual(chk(atx, "dynamics.input"), set(("Tg",))) + + cons = pbx.constraints + self.assertEqual(len(cons["constraints"]), 2) + self.assertEqual(len(cons["bounds"]), 2) + + def test_slide_crank_design(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec(sm, {"combos": "design,cost", "dxdt": None}) + atx = pbx.ref_attrs + self.assertEqual( + chk(atx, "solver.var"), + set(("Lo", "Rc", "Ro", "x_offset", "y_offset")), + ) + self.assertEqual(chk(atx, "solver.obj"), set(("cost_slv",))) + self.assertEqual( + chk(atx, "solver.ineq"), + set(("crank_pos_slv", "gear_pos_slv", "motor_pos_slv")), + ) + self.assertEqual(chk(atx, "solver.eq"), set(())) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set()) + self.assertEqual(chk(atx, "dynamics.state"), set()) + self.assertEqual(chk(atx, "dynamics.input"), set()) + + cons = pbx.constraints + self.assertEqual(len(cons["constraints"]), 4) + self.assertEqual(len(cons["bounds"]), 5) + + def test_slide_crank_design_one_match(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec(sm, {"combos": "design", "dxdt": None, "both_match": False}) + atx = pbx.ref_attrs + self.assertEqual( + chk(atx, "solver.var"), + set(("Lo", "Rc", "Ro", "x_offset", "y_offset", "X_spring_center")), + ) + self.assertEqual(chk(atx, "solver.obj"), set(("cost_slv", "sym_slv"))) + self.assertEqual( + chk(atx, "solver.ineq"), + set(("crank_pos_slv", "gear_pos_slv", "motor_pos_slv", "size")), + ) + self.assertEqual(chk(atx, "solver.eq"), set(("gear_speed_slv", "range_slv"))) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set()) + self.assertEqual(chk(atx, "dynamics.state"), set()) + self.assertEqual(chk(atx, "dynamics.input"), set()) + + cons = pbx.constraints + self.assertEqual(len(cons["constraints"]), 7) + self.assertEqual(len(cons["bounds"]), 6) + + def test_slide_crank_design_slv_design(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec(sm, {"combos": "design", "slv_vars": "*slv", "dxdt": None}) + atx = pbx.ref_attrs + self.assertEqual(chk(atx, "solver.var"), set()) + self.assertEqual(chk(atx, "solver.obj"), set(())) + self.assertEqual( + chk(atx, "solver.ineq"), + set(("crank_pos_slv", "gear_pos_slv", "motor_pos_slv")), + ) + self.assertEqual(chk(atx, "solver.eq"), set(())) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set()) + self.assertEqual(chk(atx, "dynamics.state"), set()) + self.assertEqual(chk(atx, "dynamics.input"), set()) + + cons = pbx.constraints + self.assertEqual(len(cons["constraints"]), 3) + self.assertEqual(len(cons["bounds"]), 0) + + def test_slide_crank_design_slv(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec( + sm, + { + "combos": "design,cost,speed_goal,range_goal", + "ign_combos": "max*", + "slv_vars": "*slv", + "dxdt": None, + }, + ) + atx = pbx.ref_attrs + self.assertEqual(chk(atx, "solver.var"), set()) + self.assertEqual(chk(atx, "solver.obj"), set(("cost_slv",))) + self.assertEqual( + chk(atx, "solver.ineq"), + set(("crank_pos_slv", "gear_pos_slv", "motor_pos_slv")), + ) + self.assertEqual(chk(atx, "solver.eq"), set(("gear_speed_slv", "range_slv"))) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set()) + self.assertEqual(chk(atx, "dynamics.state"), set()) + self.assertEqual(chk(atx, "dynamics.input"), set()) + + cons = pbx.constraints + self.assertEqual(len(cons["constraints"]), 5) + self.assertEqual(len(cons["bounds"]), len(atx["solver.var"])) + + def test_slide_crank_design_slv_one_match(self): + sm = SliderCrank(Tg=0) + pbx = ProblemExec( + sm, + { + "combos": "design", + "ign_combos": "max*", + "slv_vars": "*slv", + "dxdt": None, + "both_match": False, + }, + ) + atx = pbx.ref_attrs + self.assertEqual( + chk(atx, "solver.var"), + set(("Lo", "Rc", "Ro", "x_offset", "y_offset")), + ) + self.assertEqual(chk(atx, "solver.obj"), set(("cost_slv", "sym_slv"))) + self.assertEqual( + chk(atx, "solver.ineq"), + set(("crank_pos_slv", "gear_pos_slv", "motor_pos_slv")), + ) + self.assertEqual(chk(atx, "solver.eq"), set(("gear_speed_slv", "range_slv"))) + self.assertEqual(chk(atx, "dynamics.output"), set()) + self.assertEqual(chk(atx, "dynamics.rate"), set()) + self.assertEqual(chk(atx, "dynamics.state"), set()) + self.assertEqual(chk(atx, "dynamics.input"), set()) + + cons = pbx.constraints + self.assertEqual(len(cons["constraints"]), 6) + self.assertEqual(len(cons["bounds"]), len(atx["solver.var"])) + + def test_slide_crank_add_var(self): + sc = SliderCrank() + out = sc.run( + combos="design,speed_goal,range_goal", + slv_vars="*", + revert_last=False, + add_vars="*gear*", + both_match=False, + ) + # self.assertEqual(len(out['gear_speed_slv']),0) + print(out) + + +class TestContextExits(unittest.TestCase): + def tearDown(self) -> None: + self.assertFalse( + hasattr(ProblemExec.class_cache, "session"), msg="not cleaned!" + ) + + def test_error_exit(self): + tst = SimpleContext() + + class TestError(Exception): + pass + + with self.assertRaises(TestError): # we should get an error + with ProblemExec(tst) as pb1: + tst.set_rand() + with ProblemExec(tst, level_name="2") as pb2: + tst.set_rand() + with ProblemExec(tst) as pb3: + tst.set_rand() + raise TestError( + "pocket-sand" + ) # TODO: INSERT KING OF THE HILL REFERENCE + self.assertFalse(hasattr(ProblemExec.class_cache, "session")) + + def test_context_singularity(self): + tst = SimpleContext() + + with ProblemExec(tst) as pb1: + self.assertEqual(pb1, tst.last_context) + self.assertTrue(pb1.entered) + self.assertFalse(pb1.exited) + with ProblemExec(tst, level_name="2") as pb2: + self.assertIs(pb1, pb2) + self.assertTrue(pb1.entered) + self.assertFalse(pb1.exited) + with ProblemExec(tst) as pb3: + self.assertIs(pb1, pb3) + self.assertTrue(pb3.entered) + self.assertFalse(pb3.exited) + pb3.exit_to_level("top", revert=True) + raise Exception("Wrong Level") + raise Exception("Wrong Level") + raise Exception("Wrong Level") + self.assertEqual(pb1, tst.last_context) + self.assertTrue(pb1.entered) + self.assertTrue(pb1.exited) + + def test_exit_top(self): + tst = SimpleContext() + + with ProblemExec(tst) as pb1: + tst.set_rand() + with ProblemExec(tst, level_name="2") as pb2: + tst.set_rand() + with ProblemExec(tst) as pb3: + tst.set_rand() + pb3.exit_to_level("top", revert=True) + raise Exception("Wrong Level") + raise Exception("Wrong Level") + raise Exception("Wrong Level") + self.assertEqual(tst.one, 1) + self.assertEqual(tst.two, 2) + + def test_exit_top_with_state(self): + tst = SimpleContext() + + with ProblemExec(tst, level_name="super") as pb1: + tst.set_rand() + with ProblemExec(tst, level_name="2") as pb2: + tst.set_rand() + with ProblemExec(tst) as pb3: + tst.set_rand() + final_one = tst.one + final_two = tst.two + pb3.exit_to_level("top", revert=False) + raise Exception("Wrong Level") + raise Exception("Wrong Level") + raise Exception("Wrong Level") + self.assertEqual(tst.one, final_one) + self.assertEqual(tst.two, final_two) + + def test_exit_2_with_state(self): + tst = SimpleContext() + + with ProblemExec(tst) as pb1: + tst.set_rand() + with ProblemExec(tst, level_name="2") as pb2: + tst.set_rand() + with ProblemExec(tst) as pb3: + tst.set_rand() + final_one = tst.one + final_two = tst.two + pb3.exit_to_level("2", revert=False) + raise Exception("Wrong Level") + self.assertEqual(tst.one, final_one) + self.assertEqual(tst.two, final_two) + self.assertEqual(tst.one, 1) + self.assertEqual(tst.two, 2) + + def test_exit_2_wo_state(self): + tst = SimpleContext() + with ProblemExec(tst) as pb1: + tst.set_rand() + final_one = tst.one + final_two = tst.two + with ProblemExec(tst, level_name="2") as pb2: + tst.set_rand() + with ProblemExec(tst) as pb3: + tst.set_rand() + pb3.exit_to_level("2", revert=True) + raise Exception("Wrong Level") + self.assertEqual(tst.one, final_one) + self.assertEqual(tst.two, final_two) + self.assertEqual(tst.one, 1) + self.assertEqual(tst.two, 2) + + def test_exit_with_state(self): + tst = SimpleContext() + + with ProblemExec(tst) as pb1: + tst.set_rand() + mid_one = tst.one + mid_two = tst.two + with ProblemExec(tst, level_name="2") as pb2: + tst.set_rand() + final_one = tst.one + final_two = tst.two + with ProblemExec(tst) as pb3: + tst.set_rand() + final_one = tst.one + final_two = tst.two + pb3.exit_with_state() + raise Exception("Wrong Level") + self.assertEqual(tst.one, final_one) + self.assertEqual(tst.two, final_two) + self.assertEqual(tst.one, mid_one) + self.assertEqual(tst.two, mid_two) + self.assertEqual(tst.one, 1) + self.assertEqual(tst.two, 2) + + def test_exit_and_revert(self): + tst = SimpleContext() + + with ProblemExec(tst) as pb1: + tst.set_rand() + mid_one = tst.one + mid_two = tst.two + with ProblemExec(tst, level_name="2") as pb2: + tst.set_rand() + final_one = tst.one + final_two = tst.two + with ProblemExec(tst) as pb3: + tst.set_rand() + + pb3.exit_and_revert() + raise Exception("Wrong Level") + self.assertEqual(tst.one, final_one) + self.assertEqual(tst.two, final_two) + self.assertEqual(tst.one, mid_one) + self.assertEqual(tst.two, mid_two) + self.assertEqual(tst.one, 1) + self.assertEqual(tst.two, 2) diff --git a/test/test_problem_deepscoping.py b/engforge/test/test_problem_deepscoping.py similarity index 99% rename from test/test_problem_deepscoping.py rename to engforge/test/test_problem_deepscoping.py index 26a23ad..d58d259 100644 --- a/test/test_problem_deepscoping.py +++ b/engforge/test/test_problem_deepscoping.py @@ -62,7 +62,6 @@ def test_one_deep(self): if __name__ == "__main__": - comp = DeepComp(comp=None) sys = DeepSys(comp=comp) # sys.change_all_log_lvl(1) diff --git a/test/test_slider_crank.py b/engforge/test/test_slider_crank.py similarity index 100% rename from test/test_slider_crank.py rename to engforge/test/test_slider_crank.py diff --git a/test/test_solver.py b/engforge/test/test_solver.py similarity index 100% rename from test/test_solver.py rename to engforge/test/test_solver.py diff --git a/test/test_structures.py b/engforge/test/test_structures.py similarity index 100% rename from test/test_structures.py rename to engforge/test/test_structures.py diff --git a/test/test_tabulation.py b/engforge/test/test_tabulation.py similarity index 100% rename from test/test_tabulation.py rename to engforge/test/test_tabulation.py diff --git a/examples/air_filter.py b/examples/air_filter.py index d5fee1f..c28d030 100644 --- a/examples/air_filter.py +++ b/examples/air_filter.py @@ -1,60 +1,54 @@ from engforge.analysis import Analysis -from engforge.reporting import CSVReporter,DiskPlotReporter +from engforge.reporting import CSVReporter, DiskPlotReporter from engforge.properties import system_property from engforge import * from matplotlib.pylab import * import numpy as np -import os,pathlib +import os, pathlib import attrs - @forge class Fan(Component): - - n_frac:float = field(default=1) - dp_design:float= field(default=100) - w_design:float = field(default=2) - + n_frac: float = field(default=1) + dp_design: float = field(default=100) + w_design: float = field(default=2) @system_property def dP_fan(self) -> float: - return self.dp_design*(self.n_frac*self.w_design)**2.0 + return self.dp_design * (self.n_frac * self.w_design) ** 2.0 + @forge class Filter(Component): - - w:float = field(default=0) - k_loss:float = field(default=50) + w: float = field(default=0) + k_loss: float = field(default=50) @system_property def dP_filter(self) -> float: - return self.k_loss*self.w + return self.k_loss * self.w + @forge class Airfilter(System): - - throttle:float = field(default=1) - w:float = field(default=1) - k_parasitic:float = field(default=0.1) + throttle: float = field(default=1) + w: float = field(default=1) + k_parasitic: float = field(default=0.1) fan: Fan = Slot.define(Fan) filt: Filter = Slot.define(Filter) - set_fan_n = Signal.define('fan.n_frac','throttle',mode='both') - set_filter_w = Signal.define('filt.w','w',mode='both') + set_fan_n = Signal.define("fan.n_frac", "throttle", mode="both") + set_filter_w = Signal.define("filt.w", "w", mode="both") + + flow_var = Solver.declare_var("w", combos="flow") + flow_var.add_var_constraint(0, "min", combos="flow") - flow_var = Solver.declare_var('w',combos='flow') - flow_var.add_var_constraint(0,'min',combos='flow') - - pr_eq = Solver.constraint_equality('sum_dP',0,combos='flow') - + pr_eq = Solver.constraint_equality("sum_dP", 0, combos="flow") - flow_curve = Plot.define( - "throttle", "w", kind="lineplot", title="Flow Curve" - ) + flow_curve = Plot.define("throttle", "w", kind="lineplot", title="Flow Curve") @system_property def dP_parasitic(self) -> float: @@ -68,61 +62,59 @@ def sum_dP(self) -> float: @forge class AirfilterAnalysis(Analysis): """Does post processing on a system""" - + efficiency = attrs.field(default=0.95) @system_property def clean_air_delivery_rate(self) -> float: - return self.system.w*self.efficiency + return self.system.w * self.efficiency - def post_process(self,*run_args,**run_kwargs): + def post_process(self, *run_args, **run_kwargs): pass - #TODO: something custom! + # TODO: something custom! -if __name__ == '__main__': +if __name__ == "__main__": this_dir = str(pathlib.Path(__file__).parent) - this_dir = os.path.join(this_dir,'airfilter_report') + this_dir = os.path.join(this_dir, "airfilter_report") if not os.path.exists(this_dir): - os.makedirs(this_dir,754,exist_ok=True) + os.makedirs(this_dir, 754, exist_ok=True) - csv = CSVReporter(path=this_dir,report_mode='daily') - csv_latest = CSVReporter(path=this_dir,report_mode='single') - - plots = DiskPlotReporter(path=this_dir,report_mode='monthly') - plots_latest = DiskPlotReporter(path=this_dir,report_mode='single') + csv = CSVReporter(path=this_dir, report_mode="daily") + csv_latest = CSVReporter(path=this_dir, report_mode="single") + plots = DiskPlotReporter(path=this_dir, report_mode="monthly") + plots_latest = DiskPlotReporter(path=this_dir, report_mode="single") fan = Fan() filt = Filter() - af = Airfilter(fan=fan,filt=filt) - change_all_log_levels(af,20) #info - + af = Airfilter(fan=fan, filt=filt) + change_all_log_levels(af, 20) # info - #Make The Analysis + # Make The Analysis sa = AirfilterAnalysis( - system = af, - table_reporters = [csv,csv_latest], - plot_reporters = [plots,plots_latest] - ) + system=af, + table_reporters=[csv, csv_latest], + plot_reporters=[plots, plots_latest], + ) - #Run the analysis! Input passed to system - sa.run(throttle=list(np.arange(0.1,1.1,0.1)),combos='*') + # Run the analysis! Input passed to system + sa.run(throttle=list(np.arange(0.1, 1.1, 0.1)), combos="*") - #CSV's & Plots available in ./airfilter_report! - af.run(throttle=list(np.arange(0.1,1.1,0.1)),combos='*') + # CSV's & Plots available in ./airfilter_report! + af.run(throttle=list(np.arange(0.1, 1.1, 0.1)), combos="*") df = af.dataframe - fig,(ax,ax2) = subplots(2,1) - ax.plot(df.throttle*100,df.w,'k--',label='flow') - ax2.plot(df.throttle*100,df.filt_dp_filter,label='filter') - ax2.plot(df.throttle*100,df.dp_parasitic,label='parasitic') - ax2.plot(df.throttle*100,df.fan_dp_fan,label='fan') - ax.legend(loc='upper right') - ax.set_title('flow') + fig, (ax, ax2) = subplots(2, 1) + ax.plot(df.throttle * 100, df.w, "k--", label="flow") + ax2.plot(df.throttle * 100, df.filt_dp_filter, label="filter") + ax2.plot(df.throttle * 100, df.dp_parasitic, label="parasitic") + ax2.plot(df.throttle * 100, df.fan_dp_fan, label="fan") + ax.legend(loc="upper right") + ax.set_title("flow") ax.grid() ax2.legend() ax2.grid() - ax2.set_title(f'pressure') - ax2.set_xlabel(f'throttle%') \ No newline at end of file + ax2.set_title(f"pressure") + ax2.set_xlabel(f"throttle%") diff --git a/examples/field_area.py b/examples/field_area.py new file mode 100644 index 0000000..f8d9082 --- /dev/null +++ b/examples/field_area.py @@ -0,0 +1,46 @@ +from engforge import * + + +@forge +class SimpleField(System): + x: float = field(default=1) + y: float = field(default=1) + budget: float = field(default=100) + + cost_per_x: float = field(default=2) + cost_per_y: float = field(default=1) + + xvar = Solver.declare_var("x") + xvar.add_var_constraint(0) + yvar = Solver.declare_var("y") + yvar.add_var_constraint(0) + + cost = Solver.constraint_inequality("remaining_budget", 0) + + obj = Solver.objective("area", kind="max") + perim = Solver.objective("perimeter", kind="max", combos="fence") + + @system_property + def perimeter(self) -> float: + return 2 * self.x + 2 * self.y + + @system_property + def area(self) -> float: + return self.x * self.y + + @system_property + def remaining_budget(self) -> float: + return self.budget - self.cost_per_x * self.x - self.cost_per_y * self.y + + +if __name__ == "__main__": + sf = SimpleField() + sf.run(slv_vars="*") + assert sf.x == 1 # keep original + assert sf.y == 1 # keep original + + ans = sf.dataframe.iloc[0] + assert abs(ans.x - 25) < 0.001 + assert abs(ans.y - 50) < 0.001 + assert abs(ans.area - 1250) < 0.001 + assert abs(ans.remaining_budget) < 0.001 diff --git a/examples/spring_mass.py b/examples/spring_mass.py index 99caebe..42a1660 100644 --- a/examples/spring_mass.py +++ b/examples/spring_mass.py @@ -6,7 +6,6 @@ @forge class SpringMass(System): - k: float = attrs.field(default=50) m: float = attrs.field(default=1) g: float = attrs.field(default=9.81) @@ -21,15 +20,15 @@ class SpringMass(System): x_neutral: float = attrs.field(default=0.5) - res =Solver.constraint_equality("sumF") - var_a = Solver.declare_var("a",combos='a',active=False) - var_b = Solver.declare_var("u",combos='u',active=False) - var_b.add_var_constraint(0.0,kind="min") - var_b.add_var_constraint(1.0,kind="max") + res = Solver.constraint_equality("sumF") + var_a = Solver.declare_var("a", combos="a", active=False) + var_b = Solver.declare_var("u", combos="u", active=False) + var_b.add_var_constraint(0.0, kind="min") + var_b.add_var_constraint(1.0, kind="max") vtx = Time.integrate("v", "accl") xtx = Time.integrate("x", "v") - xtx.add_var_constraint(0,kind="min") + xtx.add_var_constraint(0, kind="min") pos = Trace.define(y="x", y2=["v", "a"]) @@ -56,19 +55,18 @@ def Ffric(self) -> float: @system_property def sumF(self) -> float: return self.Fspring - self.Fgrav - self.Faccel - self.Ffric + self.Fext - + @system_property def Fext(self) -> float: - return self.Fa * np.cos( self.time * self.wo_f ) + return self.Fa * np.cos(self.time * self.wo_f) @system_property def accl(self) -> float: return self.sumF / self.m - -if __name__ == '__main__': - #Run The System, Compare damping `u`=0 & 0.1 +if __name__ == "__main__": + # Run The System, Compare damping `u`=0 & 0.1 sm = SpringMass(x=0.0) - trdf = sm.simulate(dt=0.01,endtime=10,u=[0.0,0.1],combos='*',slv_vars='*') - trdf.groupby('run_id').plot('time','x') \ No newline at end of file + trdf = sm.simulate(dt=0.01, endtime=10, u=[0.0, 0.1], combos="*", slv_vars="*") + trdf.groupby("run_id").plot("time", "x") diff --git a/requirements.txt b/requirements.txt index 84884f6..89727e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ virtualenv wheel #methodtools==0.4.2 -attrs>=23.1.0 +attrs~=23.2.0 arrow deepdiff>=6.3.1 @@ -57,7 +57,7 @@ recommonmark>=0.6.0 sphinx-rtd-theme>=1.0.0 networkx-query==1.0.1 networkx>=2.5.1 - +black~=24.8.0 randomname~=0.2.1 termcolor pyee diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2c6f44c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[semantic_release] +version_variable = setup.py:__version__,docs/source/conf.py:release +branch = main +upload_to_pypi = false diff --git a/setup.py b/setup.py index 69f9652..a58fc6f 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,8 @@ from setuptools import setup import setuptools +__version__ = "0.0.9" + def parse_requirements(filename): """load requirements from a pip requirements file""" @@ -20,7 +22,7 @@ def parse_requirements(filename): setup( name="engforge", - version="0.9.2", + version=__version__, description="The Engineer's Framework", url="https://github.com/SoundsSerious/engforge", author="Kevin russell", @@ -31,12 +33,12 @@ def parse_requirements(filename): entry_points={ "console_scripts": [ # command = package.module:function - "condaenvset=engforge.common:main_cli", - #"ollymakes=engforge.locations:main_cli", - #"engforgedrive=engforge.gdocs:main_cli", - #TODO: inspect folders / list / display components + solver options - #TODO: test adhoc modules with - #TODO: run analysis adhoc on components with cli options + # "condaenvset=engforge.common:main_cli", + # "ollymakes=engforge.locations:main_cli", + # "engforgedrive=engforge.gdocs:main_cli", + # TODO: inspect folders / list / display components + solver options + # TODO: test adhoc modules with + # TODO: run analysis adhoc on components with cli options ] }, install_requires=install_reqs, diff --git a/test/test_problem.py b/test/test_problem.py index 5c57223..44ac653 100644 --- a/test/test_problem.py +++ b/test/test_problem.py @@ -287,9 +287,9 @@ def test_context_singularity(self): self.assertTrue(pb3.entered) self.assertFalse(pb3.exited) pb3.exit_to_level("top", revert=True) - raise Exception("Wrong Level") - raise Exception("Wrong Level") - raise Exception("Wrong Level") + raise Exception("Wrong Level 1") + raise Exception("Wrong Level 2") + raise Exception("Wrong Level 3") self.assertEqual(pb1, tst.last_context) self.assertTrue(pb1.entered) self.assertTrue(pb1.exited) @@ -304,9 +304,9 @@ def test_exit_top(self): with ProblemExec(tst) as pb3: tst.set_rand() pb3.exit_to_level("top", revert=True) - raise Exception("Wrong Level") - raise Exception("Wrong Level") - raise Exception("Wrong Level") + raise Exception("Wrong Level 1") + raise Exception("Wrong Level 2") + raise Exception("Wrong Level 3") self.assertEqual(tst.one, 1) self.assertEqual(tst.two, 2) @@ -322,9 +322,9 @@ def test_exit_top_with_state(self): final_one = tst.one final_two = tst.two pb3.exit_to_level("top", revert=False) - raise Exception("Wrong Level") - raise Exception("Wrong Level") - raise Exception("Wrong Level") + raise Exception("Wrong Level 1") + raise Exception("Wrong Level 2") + raise Exception("Wrong Level 3") self.assertEqual(tst.one, final_one) self.assertEqual(tst.two, final_two)