diff --git a/doc/source/conf.py b/doc/source/conf.py index a43be1bd..f0b73be0 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -59,7 +59,7 @@ # Intersphinx mapping intersphinx_mapping = { "python": ("https://docs.python.org/dev", None), - "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), + "scipy": ("https://docs.scipy.org/doc/scipy", None), "numpy": ("https://numpy.org/devdocs", None), "matplotlib": ("https://matplotlib.org/stable", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), diff --git a/examples/00-parametric/parametric_static_mixer_1.py b/examples/00-parametric/parametric_static_mixer_1.py index 86f48936..a6ae2ddc 100644 --- a/examples/00-parametric/parametric_static_mixer_1.py +++ b/examples/00-parametric/parametric_static_mixer_1.py @@ -54,6 +54,7 @@ session.solver.tui.define.boundary_conditions.set.velocity_inlet( "inlet1", (), "vmag", "yes", "inlet1_vel", 1, "quit" ) + session.solver.tui.define.boundary_conditions.set.velocity_inlet( "inlet1", (), "temperature", "yes", "inlet1_temp", 300, "quit" ) @@ -61,6 +62,7 @@ session.solver.tui.define.boundary_conditions.set.velocity_inlet( "inlet2", (), "vmag", "yes", "no", "inlet2_vel", 1, "quit" ) + session.solver.tui.define.boundary_conditions.set.velocity_inlet( "inlet2", (), "temperature", "yes", "no", "inlet2_temp", 350, "quit" ) diff --git a/pyproject.toml b/pyproject.toml index b2f17cd0..7ff6516b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ packages = [ python = ">=3.7,<4.0" importlib-metadata = {version = "^4.0", python = "<3.8"} ansys-fluent-core = "~=0.10" +h5py = ">=3.7.0" [tool.black] line-length = 88 diff --git a/src/ansys/fluent/parametric/__init__.py b/src/ansys/fluent/parametric/__init__.py index 5500ada1..f03b98b9 100644 --- a/src/ansys/fluent/parametric/__init__.py +++ b/src/ansys/fluent/parametric/__init__.py @@ -185,25 +185,25 @@ class ParametricStudy: Update a list of design points. """ - _all_studies: Dict[int, "ParametricStudy"] = {} - current_study_name = None - def __init__( self, parametric_studies, + session=None, name: Optional[str] = None, design_points: Dict[str, DesignPoint] = None, ): self._parametric_studies = parametric_studies + self.session = ( + session if session is not None else (_shared_parametric_study_registry()) + ) self.name = name self.design_points = {} if design_points is not None: self.design_points = design_points self.project_filepath = None - ParametricStudy._all_studies[id(self)] = self + self.session.register_study(self) - @classmethod - def get_all_studies(cls) -> Dict[str, "ParametricStudy"]: + def get_all_studies(self) -> Dict[str, "ParametricStudy"]: """Get all currently active studies. Returns @@ -211,7 +211,7 @@ def get_all_studies(cls) -> Dict[str, "ParametricStudy"]: Dict[str, "ParametricStudy"] currently active studies """ - return {v.name: v for _, v in cls._all_studies.items()} + return {v.name: v for _, v in self.session._all_studies.items()} def initialize(self) -> "ParametricStudy": """Initialize parametric study.""" @@ -235,7 +235,7 @@ def initialize(self) -> "ParametricStudy": self._parametric_studies[self.name].design_points[BASE_DP_NAME], ) self.design_points = {BASE_DP_NAME: base_design_point} - ParametricStudy.current_study_name = self.name + self.session.current_study_name = self.name return self else: LOG.error("initialize is not available") @@ -258,13 +258,13 @@ def rename(self, new_name: str) -> None: @property def is_current(self) -> bool: """Whether the parametric study is the current parametric study.""" - return ParametricStudy.current_study_name == self.name + return self.session.current_study_name == self.name def set_as_current(self) -> None: """Set the parametric study as the current parametric study.""" if not self.is_current: self._parametric_studies.set_as_current(self.name) - ParametricStudy.current_study_name = self.name + self.session.current_study_name = self.name def duplicate(self, copy_design_points: bool = True) -> "ParametricStudy": """Duplicate the current study. @@ -283,9 +283,7 @@ def duplicate(self, copy_design_points: bool = True) -> "ParametricStudy": self._parametric_studies.duplicate(copy_design_points=copy_design_points) new_study_names = self._parametric_studies.get_object_names() clone_name = set(new_study_names).difference(set(old_study_names)).pop() - current_study = ParametricStudy.get_all_studies()[ - ParametricStudy.current_study_name - ] + current_study = self.get_all_studies()[self.session.current_study_name] if copy_design_points: clone_design_points = { k: DesignPoint(k, self._parametric_studies[clone_name].design_points[k]) @@ -298,9 +296,9 @@ def duplicate(self, copy_design_points: bool = True) -> "ParametricStudy": ) clone_design_points = {BASE_DP_NAME: base_design_point} clone = ParametricStudy( - self._parametric_studies, clone_name, clone_design_points + self._parametric_studies, self.session, clone_name, clone_design_points ) - ParametricStudy.current_study_name = clone.name + self.session.current_study_name = clone.name return clone def delete(self) -> None: @@ -309,7 +307,7 @@ def delete(self) -> None: LOG.error("Cannot delete the current study %s", self.name) else: del self._parametric_studies[self.name] - ParametricStudy._all_studies.pop(id(self)) + self.session._all_studies.pop(id(self)) del self def use_base_data(self) -> None: @@ -508,11 +506,15 @@ def __init__( parametric_project, parametric_studies, project_filepath: str, + session=None, open_project: bool = True, ): self._parametric_project = parametric_project self._parametric_studies = parametric_studies self.project_filepath = project_filepath + self.session = ( + session if session is not None else (_shared_parametric_study_registry()) + ) if open_project: self.open(project_filepath=project_filepath) @@ -534,7 +536,7 @@ def open( ) self.project_filepath = project_filepath for study_name in self._parametric_studies.get_object_names(): - study = ParametricStudy(self._parametric_studies, study_name) + study = ParametricStudy(self._parametric_studies, self.session, study_name) dps_settings = self._parametric_studies[study_name].design_points for dp_name in dps_settings.get_object_names(): study.design_points[dp_name] = DesignPoint( @@ -600,7 +602,16 @@ def __call__(self): return pyfluent.launch_fluent(*self._args, **self._kwargs) -class ParametricSession: +class ParametricStudyRegistry: + def __init__(self): + self._all_studies: Dict[int, "ParametricStudy"] = {} + self.current_study_name = None + + def register_study(self, study): + self._all_studies[id(study)] = study + + +class ParametricSession(ParametricStudyRegistry): """ParametricSession class which encapsulates studies and project. Attributes @@ -645,6 +656,7 @@ def __init__( Whether to start streaming of Fluent transcript, by default False. """ + super().__init__() self.studies = {} self.project = None self._session = launcher() @@ -652,35 +664,37 @@ def __init__( self.scheme_eval( "(set parametric-study-dependents-manager " "save-project-at-exit? #f)" ) - if start_transcript: - self.start_transcript() + if not start_transcript: + self.stop_transcript() self._root = self._session.solver.root if case_filepath is not None: self._root.file.read(file_name=case_filepath, file_type="case") - study = ParametricStudy(self._root.parametric_studies).initialize() + study = ParametricStudy(self._root.parametric_studies, self).initialize() self.studies[study.name] = study self.project = ParametricProject( parametric_project=self._root.file.parametric_project, parametric_studies=self._root.parametric_studies, project_filepath=str(study.project_filepath), open_project=False, + session=self._session, ) elif project_filepath is not None: self.project = ParametricProject( parametric_project=self._root.file.parametric_project, parametric_studies=self._root.parametric_studies, project_filepath=project_filepath, + session=self._session, ) studies_settings = self._root.parametric_studies for study_name in studies_settings.get_object_names(): - study = ParametricStudy(studies_settings, study_name) + study = ParametricStudy(studies_settings, self, study_name) dps_settings = studies_settings[study_name].design_points for dp_name in dps_settings.get_object_names(): study.design_points[dp_name] = DesignPoint( dp_name, dps_settings[dp_name] ) self.studies[study_name] = study - ParametricStudy.current_study_name = self._root.current_parametric_study() + self.current_study_name = self._root.current_parametric_study() def new_study(self) -> ParametricStudy: """Create new study. @@ -690,7 +704,7 @@ def new_study(self) -> ParametricStudy: ParametricStudy New study. """ - study = self.studies[ParametricStudy.current_study_name].duplicate() + study = self.studies[self.current_study_name].duplicate() self.studies[study.name] = study return study @@ -741,4 +755,13 @@ def stop_transcript(self) -> None: self._session.stop_transcript() +def _shared_parametric_study_registry(): + if _shared_parametric_study_registry.instance is None: + _shared_parametric_study_registry.instance = ParametricStudyRegistry() + return _shared_parametric_study_registry.instance + + +_shared_parametric_study_registry.instance = None + + from ansys.fluent.parametric.parameters import InputParameters, OutputParameters diff --git a/src/ansys/fluent/parametric/local/__init__.py b/src/ansys/fluent/parametric/local/__init__.py new file mode 100644 index 00000000..53e290be --- /dev/null +++ b/src/ansys/fluent/parametric/local/__init__.py @@ -0,0 +1,310 @@ +""" +Classes for locally defining a parametric study for Fluent without +running Fluent. The study can then be submitted to be executed in +parallel. + +Example +------- + +Set up a local study + +>>> from ansys.fluent.parametric.local.local import LocalParametricStudy +>>> local_study = LocalParametricStudy(case_filepath="E:/elbow1_param.cas.h5") +>>> design_point = local_study.design_point("Base DP") +>>> design_point.input_parameters['v1'] = 0.0 +>>> for idx in range(1, 20): +>>> design_point = local_study.add_design_point("dp_"+str(idx)) +>>> design_point.input_parameters['v1'] = float(idx)/10.0 + +Run in Fluent + +>>> local_study.run_in_fluent(5) + +Display results + +>>> for design_point in local_study.design_point_table: +>>> for k, v in design_point.input_parameters.items(): +>>> print("input parameter", k, v) +>>> for k, v in design_point.output_parameters.items(): +>>> print("output parameter", k, v) +>>> print(72 * "-") + +""" + +from math import ceil +from typing import Any, Dict, Union + +from ansys.fluent.core.utils.async_execution import asynchronous + +from ansys.fluent.parametric import ( + BASE_DP_NAME, + ParametricSession, + ParametricSessionLauncher, +) + +try: + from ansys.fluent.core.filereader.casereader import CaseReader +except ImportError: + from ansys.fluent.parametric.local.filereader.casereader import CaseReader + + +def convert_design_point_parameter_units( + value: Dict[str, Union[float, int, str]] +) -> Dict[str, Union[float, int]]: + def conv(val): + if type(val) in (float, int): + return val + if type(val) is not str: + raise RuntimeError("Invalid value type for input parameter", val, type(val)) + pos = val.find(" [") + if pos == -1: + return float(val) + return float(val[:pos]) + + return {k: conv(v) for k, v in value.items()} + + +class LocalDesignPoint: + """ + Purely local version of design point in a parametric study. + + Attributes + ---------- + name : str + Name of the design point as a str. + output_parameters : dict + Dict of output parameters + (name of parameter to value). + input_parameters : dict + Dict of input parameters + (name of parameter to value). + status : DesignPointStatus + Current status of the design point. + """ + + def __init__(self, design_point_name: str, base_design_point=None): + self.name = design_point_name + if base_design_point: + self.__inputs = base_design_point.input_parameters.copy() + self.__outputs = base_design_point.output_parameters.copy() + else: + self.__inputs = {} + self.__outputs = {} + + @property + def input_parameters(self) -> dict: + return self.__inputs + + @input_parameters.setter + def input_parameters(self, inputs: dict): + self.__inputs = inputs + + @property + def output_parameters(self) -> dict: + return self.__outputs + + @output_parameters.setter + def output_parameters(self, outputs: dict): + self.__outputs = outputs + + +class LocalDesignPointTable(list): + """ + Local version of Design point table in a parametric study + + Methods + ------- + add_design_point(design_point_name: str) -> DesignPoint + Add a new design point to the table with the provided name. + find_design_point(idx_or_name) + Get a design point, either by name (str) or an index + indicating the position in the table (by order of insertion). + Raises + ------ + RuntimeError + If the design point is not found. + remove_design_point(idx_or_name) + Remove a design point, either by name (str) or an index + indicating the position in the table (by order of insertion). + Raises + ------ + RuntimeError + If the design point is not found. + """ + + def __init__(self, base_design_point: LocalDesignPoint): + super().__init__() + self.append(base_design_point) + + def add_design_point(self, design_point_name: str) -> LocalDesignPoint: + self.append(LocalDesignPoint(design_point_name, self[0])) + return self[-1] + + def find_design_point(self, idx_or_name) -> LocalDesignPoint: + if isinstance(idx_or_name, int): + return self[idx_or_name] + for design_point in self: + if idx_or_name == design_point.name: + return design_point + raise RuntimeError(f"Design point not found: {idx_or_name}") + + def remove_design_point(self, idx_or_name): + design_point = self.find_design_point(idx_or_name) + if design_point is self[0]: + raise RuntimeError("Cannot remove base design point") + self.remove(self.find_design_point(idx_or_name)) + + +def _run_local_study_in_fluent( + local_study, + num_servers: int, + launcher: Any, + start_transcript: bool, + capture_report_data: bool, +): + + source_table_size = len(local_study.design_point_table) + + def make_input_for_study(design_point_range) -> None: + if design_point_range is None: + design_point_range = range(0, source_table_size) + study_input = [] + for idx in design_point_range: + design_point = local_study.design_point(idx_or_name=idx) + study_input.append(design_point.input_parameters.copy()) + return study_input + + def make_input_for_studies(num_servers) -> None: + study_inputs = [] + total_num_points = num_points = source_table_size + for i in range(num_servers): + count = ceil(num_points / num_servers) + range_base = total_num_points - num_points + num_points -= count + num_servers -= 1 + study_inputs.append( + make_input_for_study(range(range_base, range_base + count)) + ) + return study_inputs + + @asynchronous + def make_parametric_session(case_filepath): + return ParametricSession( + case_filepath=case_filepath, + launcher=launcher, + start_transcript=start_transcript, + ) + + @asynchronous + def apply_to_study(study, inputs): + first = True + for inpt in inputs: + if first: + design_point = study.design_points[BASE_DP_NAME] + design_point.capture_simulation_report_data_enabled = ( + capture_report_data + ) + first = False + else: + design_point = study.add_design_point( + capture_simulation_report_data=capture_report_data + ) + design_point.input_parameters = convert_design_point_parameter_units( + inpt.copy() + ) + + @asynchronous + def update_design_point(study): + study.update_all_design_points() + + def apply_to_studies(studies, inputs) -> None: + results = [] + for item in list(zip(studies, inputs)): + study, inpt = item + results.append(apply_to_study(study, inpt)) + for result in results: + result.result() + + study_inputs = make_input_for_studies(num_servers) + + sessions = [] + studies = [] + for i in range(num_servers): + sessions.append( + make_parametric_session(case_filepath=local_study.case_filepath) + ) + + for session in sessions: + studies.append(next(iter(session.result().studies.values()))) + + apply_to_studies(studies, study_inputs) + + updates = [] + for study in studies: + updates.append(update_design_point(study)) + + for update in updates: + update.result() + + it = iter(local_study.design_point_table) + + for study in studies: + for _, design_point in study.design_points.items(): + next(it).output_parameters = design_point.output_parameters.copy() + + +class LocalParametricStudy: + """ + Local version of parametric study that manages design points to parametrize a + Fluent solver set-up. + + Methods + ------- + add_design_point(design_point_name: str) -> LocalDesignPoint + Add a design point + design_point(idx_or_name) + Get a design point, either by name (str) or an index + indicating the position in the table (by order of insertion). + Raises + ------ + RuntimeError + If the design point is not found. + run_in_fluent + Run the study in Fluent + """ + + def __init__(self, case_filepath: str, base_design_point_name: str = "Base DP"): + self.case_filepath = case_filepath + base_design_point = LocalDesignPoint(base_design_point_name) + case_reader = CaseReader(hdf5_case_filepath=case_filepath) + + base_design_point.input_parameters = { + p.name: p.value for p in case_reader.input_parameters() + } + + base_design_point.output_parameters = { + p.name: None for p in case_reader.output_parameters() + } + + self.design_point_table = LocalDesignPointTable(base_design_point) + + def add_design_point(self, design_point_name: str) -> LocalDesignPoint: + return self.design_point_table.add_design_point(design_point_name) + + def design_point(self, idx_or_name) -> LocalDesignPoint: + return self.design_point_table.find_design_point(idx_or_name) + + def run_in_fluent( + self, + num_servers: int, + launcher: Any = ParametricSessionLauncher(), + start_transcript: bool = False, + capture_report_data: bool = False, + ): + _run_local_study_in_fluent( + local_study=self, + num_servers=num_servers, + launcher=launcher, + start_transcript=start_transcript, + capture_report_data=capture_report_data, + ) diff --git a/src/ansys/fluent/parametric/local/filereader/__init__.py b/src/ansys/fluent/parametric/local/filereader/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ansys/fluent/parametric/local/filereader/casereader.py b/src/ansys/fluent/parametric/local/filereader/casereader.py new file mode 100644 index 00000000..33053b29 --- /dev/null +++ b/src/ansys/fluent/parametric/local/filereader/casereader.py @@ -0,0 +1,137 @@ +""" +Reader for Fluent case files. + +Example +------- + +>>> from ansys.fluent.parametric.local.filereader.casereader import CaseReader + +Instantiate a case reader + +>>> reader = CaseReader(hdf5_case_filepath=case_filepath) + +Get lists of input and output parameters + +>>> input_parameters = reader.input_parameters() +>>> output_parameters = reader.output_parameters() + + +""" + +from pathlib import Path +from typing import List + +import h5py + +from . import lispy + + +class InputParameter: + """Class to represent an input parameter + + Attributes + ---------- + name : str + value + The value of this input parameter, usually + a string, qualified by units + """ + + def __init__(self, raw_data): + self.name, self.name = None, None + for k, v in raw_data: + if k == "name": + self.name = v + elif k == "definition": + self.value = v + + +class OutputParameter: + """Class to represent an output parameter + + Attributes + ---------- + name : str + """ + + def __init__(self, raw_data): + parameter = raw_data[1] + for elem in parameter: + if len(elem) and elem[0] == "name": + self.name = elem[1][1] + + +class CaseReader: + """Class to read a Fluent case file. + + Methods + ------- + input_parameters + Get a list of input parameter objects + output_parameters + Get a list of output parameter objects + num_dimensions + Get the dimensionality of the case (2 or 3) + precision + Get the precision (1 or 2 for 1D of 2D) + """ + + def __init__(self, hdf5_case_filepath: str): + try: + file = h5py.File(hdf5_case_filepath) + except FileNotFoundError: + raise RuntimeError(f"The case file {hdf5_case_filepath} cannot be found.") + except OSError: + error_message = ( + "Could not read case file. " "Only valid HDF5 files can be read. " + ) + if Path(hdf5_case_filepath).suffix != ".h5": + error_message += ( + f"The file {hdf5_case_filepath} does not have a .h5 extension." + ) + raise RuntimeError(error_message) + except BaseException: + raise RuntimeError(f"Could not read case file {hdf5_case_filepath}") + settings = file["settings"] + rpvars = settings["Rampant Variables"][0] + rp_vars_str = rpvars.decode() + self._rp_vars = lispy.parse(rp_vars_str)[1] + self._rp_var_cache = {} + + def input_parameters(self) -> List[InputParameter]: + exprs = self._named_expressions() + input_params = [] + for expr in exprs: + for attr in expr: + if attr[0] == "input-parameter" and attr[1] is True: + input_params.append(InputParameter(expr)) + return input_params + + def output_parameters(self) -> List[OutputParameter]: + parameters = self._find_rp_var("parameters/output-parameters") + return [OutputParameter(param) for param in parameters] + + def num_dimensions(self) -> int: + for attr in self._case_config(): + if attr[0] == "rp-3d?": + return 3 if attr[1] is True else 2 + + def precision(self) -> int: + for attr in self._case_config(): + if attr[0] == "rp-double?": + return 2 if attr[1] is True else 1 + + def _named_expressions(self): + return self._find_rp_var("named-expressions") + + def _case_config(self): + return self._find_rp_var("case-config") + + def _find_rp_var(self, name: str): + try: + return self._rp_var_cache[name] + except KeyError: + for var in self._rp_vars: + if type(var) == list and len(var) and var[0] == name: + self._rp_var_cache[name] = var[1] + return var[1] diff --git a/src/ansys/fluent/parametric/local/filereader/lispy.py b/src/ansys/fluent/parametric/local/filereader/lispy.py new file mode 100644 index 00000000..d9660dbf --- /dev/null +++ b/src/ansys/fluent/parametric/local/filereader/lispy.py @@ -0,0 +1,467 @@ +################ Scheme Interpreter in Python + +## (c) Peter Norvig, 2010; See http://norvig.com/lispy2.html + +################ Symbol, Procedure, classes + +## This code is copied from +## https://github.com/norvig/pytudes/blob/main/py/lispy.py +## and modified as necessary + + +import io +import re +import sys + + +class Symbol(str): + pass + + +def Sym(s, symbol_table={}): + "Find or create unique Symbol entry for str s in symbol table." + if s not in symbol_table: + symbol_table[s] = Symbol(s) + return symbol_table[s] + + +( + _quote, + _if, + _set, + _define, + _lambda, + _begin, + _definemacro, +) = map(Sym, "quote if set! define lambda begin define-macro".split()) + +_quasiquote, _unquote, _unquotesplicing = map( + Sym, "quasiquote unquote unquote-splicing".split() +) + + +class Procedure: + "A user-defined Scheme procedure." + + def __init__(self, params, exp, env): + self.params, self.exp, self.env = params, exp, env + + def __call__(self, *args): + return eval(self.exp, Env(self.params, args, self.env)) + + +################ parse, read, and user interaction + + +def parse(in_port): + "Parse a program: read and expand/error-check it." + # Backwards compatibility: given a str, convert it to an InputPort + if isinstance(in_port, str): + in_port = InputPort(io.StringIO(in_port)) + return expand(read(in_port), toplevel=True) + + +eof_object = Symbol("#") # Note: uninterned; can't be read + + +class InputPort: + "An input port. Retains a line of chars." + tokenizer = r"""\s*(,@|[('`,)]|"(?:[\\].|[^\\"])*"|;.*|[^\s('"`,;)]*)(.*)""" + + def __init__(self, file): + self.file = file + self.line = "" + + def next_token(self): + "Return the next token, reading new text into line buffer if needed." + while True: + if self.line == "": + self.line = self.file.readline() + if self.line == "": + return eof_object + token, self.line = re.match(InputPort.tokenizer, self.line).groups() + if token != "" and not token.startswith(";"): + return token + + +def readchar(in_port): + "Read the next character from an input port." + if in_port.line != "": + ch, in_port.line = in_port.line[0], in_port.line[1:] + return ch + else: + return in_port.file.read(1) or eof_object + + +def read(in_port): + "Read a Scheme expression from an input port." + + def read_ahead(token): + if "(" == token: + L = [] + cons = None + while True: + token = in_port.next_token() + if token == ")": + return L + if token == ".": + if len(L) > 1: + cons = [L.pop()] + else: + ahead = read_ahead(token) + if cons: + cons.append(ahead) + ahead = cons + cons = None + L.append(ahead) + elif ")" == token: + raise SyntaxError("unexpected )") + elif token in quotes: + return [quotes[token], read(in_port)] + elif token is eof_object: + raise SyntaxError("unexpected EOF in list") + else: + return atom(token) + + # body of read: + token1 = in_port.next_token() + return eof_object if token1 is eof_object else read_ahead(token1) + + +quotes = {"'": _quote, "`": _quasiquote, ",": _unquote, ",@": _unquotesplicing} + + +def atom(token): + 'Numbers become numbers; #t and #f are booleans; "..." string; otherwise Symbol.' + if token == "#t": + return True + elif token == "#f": + return False + elif token[0] == '"': + return token[1:-1] + try: + return int(token) + except ValueError: + try: + return float(token) + except ValueError: + try: + return complex(token.replace("i", "j", 1)) + except ValueError: + return Sym(token) + + +def to_string(x): + "Convert a Python object back into a Lisp-readable string." + if x is True: + return "#t" + elif x is False: + return "#f" + elif isa(x, Symbol): + return x + elif isa(x, str): + return repr(x) + elif isa(x, list): + return "(" + " ".join(map(to_string, x)) + ")" + elif isa(x, complex): + return str(x).replace("j", "i") + else: + return str(x) + + +def load(filename): + "Eval every expression from a file." + repl(None, InputPort(open(filename)), None) + + +def repl(prompt="lispy> ", in_port=InputPort(sys.stdin), out=sys.stdout): + "A prompt-read-eval-print loop." + sys.stderr.write("Lispy version 2.0\n") + while True: + try: + if prompt: + sys.stderr.write(prompt) + x = parse(in_port) + if x is eof_object: + return + val = eval(x) + if val is not None and out: + print(to_string(val), file=out) + except Exception as e: + print("%s: %s" % (type(e).__name__, e)) + + +################ Environment class + + +class Env(dict): + "An environment: a dict of {'var':val} pairs, with an outer Env." + + def __init__(self, params=(), args=(), outer=None): + # Bind paarm list to corresponding args, or single param to list of args + self.outer = outer + if isa(params, Symbol): + self.update({params: list(args)}) + else: + if len(args) != len(params): + raise TypeError( + "expected %s, given %s, " % (to_string(params), to_string(args)) + ) + self.update(zip(params, args)) + + def find(self, var): + "Find the innermost Env where var appears." + if var in self: + return self + elif self.outer is None: + raise LookupError(var) + else: + return self.outer.find(var) + + +def is_pair(x): + return x != [] and isa(x, list) + + +def cons(x, y): + return [x] + y + + +def callcc(proc): + "Call proc with current continuation; escape only" + ball = RuntimeWarning("Sorry, can't continue this continuation any longer.") + + def throw(retval): + ball.retval = retval + raise ball + + try: + return proc(throw) + except RuntimeWarning as w: + if w is ball: + return ball.retval + else: + raise w + + +def add_globals(self): + "Add some Scheme standard procedures." + import cmath + import math + import operator as op + + self.update(vars(math)) + self.update(vars(cmath)) + self.update( + { + "+": op.add, + "-": op.sub, + "*": op.mul, + "/": op.truediv, + "not": op.not_, + ">": op.gt, + "<": op.lt, + ">=": op.ge, + "<=": op.le, + "=": op.eq, + "equal?": op.eq, + "eq?": op.is_, + "length": len, + "cons": cons, + "car": lambda x: x[0], + "cdr": lambda x: x[1:], + "append": op.add, + "list": lambda *x: list(x), + "list?": lambda x: isa(x, list), + "null?": lambda x: x == [], + "symbol?": lambda x: isa(x, Symbol), + "boolean?": lambda x: isa(x, bool), + "pair?": is_pair, + "port?": lambda x: isa(x, file), + "apply": lambda proc, l: proc(*l), + "eval": lambda x: eval(expand(x)), + "load": lambda fn: load(fn), + "call/cc": callcc, + "open-input-file": open, + "close-input-port": lambda p: p.file.close(), + "open-output-file": lambda f: open(f, "w"), + "close-output-port": lambda p: p.close(), + "eof-object?": lambda x: x is eof_object, + "read-char": readchar, + "read": read, + "write": lambda x, port=sys.stdout: port.write(to_string(x)), + "display": lambda x, port=sys.stdout: port.write( + x if isa(x, str) else to_string(x) + ), + } + ) + return self + + +isa = isinstance + +global_env = add_globals(Env()) + +################ eval (tail recursive) + + +def eval(x, env=global_env): + "Evaluate an expression in an environment." + while True: + if isa(x, Symbol): # variable reference + return env.find(x)[x] + elif not isa(x, list): # constant literal + return x + elif x[0] is _quote: # (quote exp) + (_, exp) = x + return exp + elif x[0] is _if: # (if test conseq alt) + (_, test, conseq, alt) = x + x = conseq if eval(test, env) else alt + elif x[0] is _set: # (set! var exp) + (_, var, exp) = x + env.find(var)[var] = eval(exp, env) + return None + elif x[0] is _define: # (define var exp) + (_, var, exp) = x + env[var] = eval(exp, env) + return None + elif x[0] is _lambda: # (lambda (var*) exp) + (_, vars, exp) = x + return Procedure(vars, exp, env) + elif x[0] is _begin: # (begin exp+) + for exp in x[1:-1]: + eval(exp, env) + x = x[-1] + else: # (proc exp*) + exps = [eval(exp, env) for exp in x] + proc = exps.pop(0) + if isa(proc, Procedure): + x = proc.exp + env = Env(proc.params, exps, proc.env) + else: + return proc(*exps) + + +################ expand + + +def expand(x, toplevel=False): + "Walk tree of x, making optimizations/fixes, and signaling SyntaxError." + # require(x, x!=[]) # () => Error + if x == []: + return x + if not isa(x, list): # constant => unchanged + return x + elif x[0] is _quote: # (quote exp) + require(x, len(x) == 2) + return x + elif x[0] is _if: + if len(x) == 3: + x = x + [None] # (if t c) => (if t c None) + require(x, len(x) == 4) + return list(map(expand, x)) + elif x[0] is _set: + require(x, len(x) == 3) + var = x[1] # (set! non-var exp) => Error + require(x, isa(var, Symbol), "can set! only a symbol") + return [_set, var, expand(x[2])] + elif x[0] is _define or x[0] is _definemacro: + require(x, len(x) >= 3) + _def, v, body = x[0], x[1], x[2:] + if isa(v, list) and v: # (define (f args) body) + f, args = v[0], v[1:] # => (define f (lambda (args) body)) + return expand([_def, f, [_lambda, args] + body]) + else: + require(x, len(x) == 3) # (define non-var/list exp) => Error + require(x, isa(v, Symbol), "can define only a symbol") + exp = expand(x[2]) + if _def is _definemacro: + require(x, toplevel, "define-macro only allowed at top level") + proc = eval(exp) + require(x, callable(proc), "macro must be a procedure") + macro_table[v] = proc # (define-macro v proc) + return None # => None; add v:proc to macro_table + return [_define, v, exp] + elif x[0] is _begin: + if len(x) == 1: + return None # (begin) => None + else: + return [expand(xi, toplevel) for xi in x] + elif x[0] is _lambda: # (lambda (x) e1 e2) + require(x, len(x) >= 3) # => (lambda (x) (begin e1 e2)) + vars, body = x[1], x[2:] + require( + x, + (isa(vars, list) and all(isa(v, Symbol) for v in vars)) + or isa(vars, Symbol), + "illegal lambda argument list", + ) + exp = body[0] if len(body) == 1 else [_begin] + body + return [_lambda, vars, expand(exp)] + elif x[0] is _quasiquote: # `x => expand_quasiquote(x) + require(x, len(x) == 2) + return expand_quasiquote(x[1]) + elif isa(x[0], Symbol) and x[0] in macro_table: + return expand(macro_table[x[0]](*x[1:]), toplevel) # (m arg...) + else: # => macroexpand if m isa macro + return list(map(expand, x)) # (f arg...) => expand each + + +def require(x, predicate, msg="wrong length"): + "Signal a syntax error if predicate is false." + if not predicate: + raise SyntaxError(to_string(x) + ": " + msg) + + +_append, _cons, _let = map(Sym, "append cons let".split()) + + +def expand_quasiquote(x): + """Expand `x => 'x; `,x => x; `(,@x y) => (append x y)""" + if not is_pair(x): + return [_quote, x] + require(x, x[0] is not _unquotesplicing, "can't splice here") + if x[0] is _unquote: + require(x, len(x) == 2) + return x[1] + elif is_pair(x[0]) and x[0][0] is _unquotesplicing: + require(x[0], len(x[0]) == 2) + return [_append, x[0][1], expand_quasiquote(x[1:])] + else: + return [_cons, expand_quasiquote(x[0]), expand_quasiquote(x[1:])] + + +def let(*args): + args = list(args) + x = cons(_let, args) + require(x, len(args) > 1) + bindings, body = args[0], args[1:] + require( + x, + all(isa(b, list) and len(b) == 2 and isa(b[0], Symbol) for b in bindings), + "illegal binding list", + ) + vars, vals = zip(*bindings) + return [[_lambda, list(vars)] + list(map(expand, body))] + list(map(expand, vals)) + + +macro_table = {_let: let} ## More macros can go here + +eval( + parse( + """(begin + +(define-macro and (lambda args + (if (null? args) #t + (if (= (length args) 1) (car args) + `(if ,(car args) (and ,@(cdr args)) #f))))) + +;; More macros can also go here + +)""" + ) +) + +if __name__ == "__main__": + repl() diff --git a/tests/test_casereader.py b/tests/test_casereader.py new file mode 100644 index 00000000..172adeaf --- /dev/null +++ b/tests/test_casereader.py @@ -0,0 +1,47 @@ +from ansys.fluent.core import examples + +from ansys.fluent.parametric.local.filereader.casereader import CaseReader + + +def test_casereader(): + + case_filepath = examples.download_file( + "Static_Mixer_Parameters.cas.h5", "pyfluent/static_mixer" + ) + + reader = CaseReader(hdf5_case_filepath=case_filepath) + + input_parameters = reader.input_parameters() + + assert reader.precision() == 2 + + assert reader.num_dimensions() == 3 + + assert len(input_parameters) == 4 + + input_parameter_dict = {p.name: p.value for p in input_parameters} + + assert input_parameter_dict["inlet1_temp"] == "300 [K]" + + assert input_parameter_dict["inlet1_vel"] == "1 [m/s]" + + assert input_parameter_dict["inlet2_temp"] == "350 [K]" + + assert input_parameter_dict["inlet2_vel"] == "1 [m/s]" + + output_parameters = reader.output_parameters() + + assert len(output_parameters) == 2 + + assert {"outlet-temp-avg-op", "outlet-vel-avg-op"} == { + p.name for p in output_parameters + } + + +def test_casereader_no_file(): + throws = False + try: + reader = CaseReader(hdf5_case_filepath="no_file.cas.h5") + except BaseException: + throws = True + assert throws diff --git a/tests/test_lispy.py b/tests/test_lispy.py new file mode 100644 index 00000000..0346155b --- /dev/null +++ b/tests/test_lispy.py @@ -0,0 +1,19 @@ +from ansys.fluent.parametric.local.filereader import lispy + + +def test_read(): + in_outs = [ + ["1", 1], + ["(1)", [1]], + ["(1 2)", [1, 2]], + ["(1 . 2)", [1, 2]], + ["(1 2 3)", [1, 2, 3]], + ["(1 2 . 3)", [1, [2, 3]]], + ["((1 . 2) . 3)", [[1, 2], 3]], + ["(1 . (2 . 3))", [1, [2, 3]]], + ["(x 1)", ["x", 1]], + ['(x . "1.0 [m/s]")', ["x", "1.0 [m/s]"]], # should be "'1.0 [m/s]'" ? + ] + + for in_out in in_outs: + assert lispy.parse(in_out[0]) == in_out[1] diff --git a/tests/test_local_parametric_run.py b/tests/test_local_parametric_run.py new file mode 100644 index 00000000..c15fd050 --- /dev/null +++ b/tests/test_local_parametric_run.py @@ -0,0 +1,51 @@ +""" +This local parametric study workflow test performs these steps + +TODO +""" + +############################################################################ + +from math import inf + +from ansys.fluent.core import examples + +from ansys.fluent.parametric.local import ( + LocalParametricStudy, + convert_design_point_parameter_units, +) + + +def test_local_parametric_setup(): + + ############################################################################ + # Read the hopper/mixer case + + case_filename = examples.download_file( + "Static_Mixer_Parameters.cas.h5", "pyfluent/static_mixer" + ) + + local_study = LocalParametricStudy(case_filepath=case_filename) + + for idx in range(20): + design_point = local_study.add_design_point("dp_" + str(idx)) + design_point.input_parameters["inlet1_vel"] = float(2 + idx) + + local_study.run_in_fluent(num_servers=4) + + table = local_study.design_point_table + + assert len(table) == 21 + + outlet_velocity = -inf + inlet_velocity = 0.0 + + for point in table: + ins = convert_design_point_parameter_units(point.input_parameters) + outs = point.output_parameters + new_inlet_velocity = ins["inlet1_vel"] + new_outlet_velocity = outs["outlet-vel-avg-op"] + assert new_inlet_velocity - inlet_velocity == 1.0 + assert new_outlet_velocity > outlet_velocity + inlet_velocity = new_inlet_velocity + outlet_velocity = new_outlet_velocity diff --git a/tests/test_local_parametric_setup.py b/tests/test_local_parametric_setup.py new file mode 100644 index 00000000..598f51a6 --- /dev/null +++ b/tests/test_local_parametric_setup.py @@ -0,0 +1,45 @@ +""" +This local parametric study workflow test performs these steps + +TODO +""" + +############################################################################ + +from ansys.fluent.core import examples + +from ansys.fluent.parametric.local import LocalParametricStudy + + +def test_local_parametric_setup(): + + ############################################################################ + # Read the hopper/mixer case + + case_filename = examples.download_file( + "Static_Mixer_Parameters.cas.h5", "pyfluent/static_mixer" + ) + + local_study = LocalParametricStudy(case_filepath=case_filename) + + base_design_point = local_study.design_point("Base DP") + + input_parameters = base_design_point.input_parameters + + assert len(input_parameters) == 4 + + assert input_parameters["inlet1_temp"] == "300 [K]" + + assert input_parameters["inlet1_vel"] == "1 [m/s]" + + assert input_parameters["inlet2_temp"] == "350 [K]" + + assert input_parameters["inlet2_vel"] == "1 [m/s]" + + output_parameters = base_design_point.output_parameters + + assert len(output_parameters) == 2 + + assert output_parameters["outlet-temp-avg-op"] == None + + assert output_parameters["outlet-vel-avg-op"] == None diff --git a/tests/test_parametric_workflow.py b/tests/test_parametric_workflow.py index 17599dc0..5371b64d 100644 --- a/tests/test_parametric_workflow.py +++ b/tests/test_parametric_workflow.py @@ -119,7 +119,7 @@ study_1 = ParametricStudy(session.solver.root.parametric_studies).initialize() parametricStudies_exp = 1 -parametricStudies_test = len(ParametricStudy._all_studies.keys()) +parametricStudies_test = len(study_1.get_all_studies().keys()) assert parametricStudies_test == parametricStudies_exp ########################################################################### @@ -237,7 +237,7 @@ assert len(study_2.design_points) == 2 parametricStudies_exp = 2 -parametricStudies_test = len(ParametricStudy._all_studies.keys()) +parametricStudies_test = len(study_1.get_all_studies().keys()) assert parametricStudies_test == parametricStudies_exp ######################################################################### @@ -251,7 +251,7 @@ study_1.delete() parametricStudies_exp = 1 -parametricStudies_test = len(ParametricStudy._all_studies.keys()) +parametricStudies_test = len(study_1.get_all_studies().keys()) assert parametricStudies_test == parametricStudies_exp #########################################################################