diff --git a/doc/changelog/910.added.md b/doc/changelog/910.added.md new file mode 100644 index 000000000..279a11141 --- /dev/null +++ b/doc/changelog/910.added.md @@ -0,0 +1 @@ +Manually generate \`\`autoapi\`\` docs diff --git a/doc/source/conf.py b/doc/source/conf.py index d10f9f1da..395541594 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -31,7 +31,6 @@ "sphinx_jinja", "pyvista.ext.plot_directive", "sphinx_design", - "ansys_sphinx_theme.extension.autoapi", ] # Intersphinx mapping @@ -144,10 +143,6 @@ # -- Declare the Jinja context ----------------------------------------------- BUILD_API = True if os.environ.get("BUILD_API", "true") == "true" else False -if not BUILD_API: - exclude_patterns.append("api") - html_theme_options.pop("ansys_sphinx_theme_autoapi") - extensions.remove("ansys_sphinx_theme.extension.autoapi") suppress_warnings = ["autoapi.python_import_resolution", "config.cache", "docutils"] @@ -194,6 +189,45 @@ }, } +import sphinx +from sphinx.util import logging +import pathlib +import sys + +def run_autoapi(app): + """ + Run the autoapi script to generate API documentation. + + Parameters + ---------- + app : sphinx.application.Sphinx + Sphinx application instance containing the all the doc build configuration. + + """ + logger = logging.getLogger(__name__) + logger.info("\nWriting reST files for API documentation...", color="green") + + scritps_dir = pathlib.Path(app.srcdir).parent.parent / "scripts" + sys.path.append(str(scritps_dir.resolve())) + + print(scritps_dir) + + from autoapi import autodoc_extensions + autodoc_extensions() + + logger.info("Done!\n") + +autodoc_default_options = { + #'members': 'var1, var2', + "member-order": "alphabetical", + #'special-members': '__init__', + "show-inheritance": True, + "undoc-members": True, + #'exclude-members': '__weakref__' +} +autodoc_class_signature = "separated" + + def skip_run_subpackage(app, what, name, obj, skip, options): """Skip specific members of the 'run' subpackage during documentation generation. @@ -215,4 +249,9 @@ def skip_run_subpackage(app, what, name, obj, skip, options): def setup(sphinx): """Add custom extensions to Sphinx.""" - sphinx.connect("autoapi-skip-member", skip_run_subpackage) \ No newline at end of file + if BUILD_API: + # exclude_patterns.append("api") + # html_theme_options.pop("ansys_sphinx_theme_autoapi") + # extensions.remove("ansys_sphinx_theme.extension.autoapi") + sphinx.connect("builder-inited", run_autoapi) + # sphinx.connect("autoapi-skip-member", skip_run_subpackage) \ No newline at end of file diff --git a/scripts/autoapi.py b/scripts/autoapi.py new file mode 100644 index 000000000..ef2a121e3 --- /dev/null +++ b/scripts/autoapi.py @@ -0,0 +1,812 @@ +"""Automatically generate reStructuredText files for Python modules and classes.""" + +import ast +from pathlib import Path +import textwrap +from typing import Union + +from numpydoc.docscrape import NumpyDocString + + +class ManualRSTGenerator: + @staticmethod + def get_base_name(base): + if hasattr(base, "id"): + return base.id + elif hasattr(base, "attr"): + parts = [] + while isinstance(base, ast.Attribute): + parts.append(base.attr) + base = base.value + if hasattr(base, "id"): + parts.append(base.id) + return ".".join(reversed(parts)) + return None + """Generates reStructuredText files for Python modules and classes.""" + + def __init__(self, core_namespace, module_dir, doc_dir): + """Initialize the generator. + + Parameters + ---------- + core_namespace : str + The namespace for the modules. + module_dir : pathlib.Path + Path to the directory containing the Python modules. + doc_dir : pathlib.Path + Path to the directory where to save the generated RST files. + + """ + self.core_namespace = core_namespace + self.module_dir = module_dir + self.doc_dir = doc_dir + + def generate_rst_for_manual_modules(self, auto_files): + """Generate RST files for Python modules. + + Parameters + ---------- + auto_files : list + List of files to be excluded from RST generation. + + """ + auto_file_paths = [Path(f).resolve() for f in auto_files] + + for path in Path(self.module_dir).rglob("*.py"): + path_resolved = path.resolve() + is_autofile = any( + auto_path in path_resolved.parents or auto_path == path_resolved for auto_path in auto_file_paths + ) + is_private_file = path.name.startswith("_") and ("__init__") not in path.name + is_internal_file = "internal" in path.parts + if not is_autofile and not is_internal_file and not is_private_file: + self._generate_rst_for_pymodule(str(path)) + + def _wrap_python_code_snippets(self, unformatted_docstring: str): + """Wrap Python code snippets in the docstring with code-block directive. + + Parameters + ---------- + unformatted_docstring : str + The unformatted docstring. + + """ + lines = unformatted_docstring.splitlines() + formatted_lines = [] + + in_snippet = False + for line in lines: + stripped = line.lstrip() + indent_level = len(line) - len(stripped) + + if not in_snippet and indent_level > 0 and stripped: + in_snippet = True + formatted_lines.append(".. code-block:: python\n") + formatted_lines.append("") + + elif in_snippet and indent_level == 0 and stripped: + in_snippet = False + + formatted_lines.append(line) + + return "\n".join(formatted_lines).rstrip() + + def _generate_rst_for_pymodule(self, path_to_src_file: Union[str, Path]): + """Generate RST file for a Python module. + + Parameters + ---------- + path_to_src_file : str or pathlib.Path + Path to the source file. + """ + path_to_src_file = Path(path_to_src_file).resolve() + module_dir_path = Path(self.module_dir).resolve() + relative_path = path_to_src_file.relative_to(module_dir_path) + + if path_to_src_file.name == "__init__.py" and path_to_src_file.parent == module_dir_path: + return + + relative_namespace = ".".join(relative_path.with_suffix("").parts).replace(".__init__", "") + full_namespace = self.core_namespace + "." + relative_namespace + containing_namespace = ".".join(full_namespace.split(".")[:-1]) + module_name = full_namespace.split(".")[-1] + + if path_to_src_file.name == "__init__.py": + out_file_path = Path(self.doc_dir) / relative_path.parent.with_suffix(".rst") + else: + out_file_path = Path(self.doc_dir) / relative_path.with_suffix(".rst") + + out_file_path.parent.mkdir(parents=True, exist_ok=True) + + with ( + path_to_src_file.open("r", encoding="utf-8") as in_file, + out_file_path.open("w", encoding="utf-8") as out_file, + ): + tree = ast.parse(in_file.read()) + + submodules = [] + subpackages = [] + functions = [] + + if path_to_src_file.name == "__init__.py": + for entry in path_to_src_file.parent.iterdir(): + is_private_entry = entry.name.startswith("_") + if ( + entry.is_file() + and entry.suffix == ".py" + and entry.name != "__init__.py" + and not is_private_entry + ): + submodules.append(entry.stem) + elif entry.is_dir() and entry.name != "__pycache__": + subpackages.append(entry.name) + + type_definitions = [node for node in tree.body if isinstance(node, ast.ClassDef)] + enums = sorted( + [ + td + for td in type_definitions + if any(getattr(base, "id", "") in ("IntEnum", "IntFlag") for base in td.bases) + ], + key=lambda x: x.name, + ) + interfaces = sorted( + [td for td in type_definitions if td not in enums and td.name.startswith("I")], key=lambda x: x.name + ) + classes = sorted( + [ + td + for td in type_definitions + if td not in enums and not td.name.startswith("_") and td not in interfaces + ], + key=lambda x: x.name, + ) + + function_definitions = [node for node in tree.body if isinstance(node, ast.FunctionDef)] + functions = sorted( + [func_def for func_def in function_definitions if not func_def.name.startswith("_")], + key=lambda x: x.name, + ) + + def write_line(lines): + out_file.writelines([line + "\n" for line in lines]) + + write_line( + [ + f"The ``{module_name}`` module", + "=" * (len(module_name) + 20), + "", + f".. py:module:: {containing_namespace}.{module_name}", + "", + ] + ) + + if any([subpackages, submodules, interfaces, classes, enums, functions]): + write_line(["Summary", "-------", "", ".. tab-set::", ""]) + + def write_list_tab(title, items, formatter): + write_line( + [ + f" .. tab-item:: {title}", + "", + " .. list-table::", + " :header-rows: 0", + " :widths: auto", + "", + ] + ) + for item in items: + write_line([f" * - {formatter(item)}", ""]) + + if subpackages: + write_list_tab( + "Subpackages", subpackages, lambda sp: f":py:obj:`~{containing_namespace}.{module_name}.{sp}`" + ) + + if submodules: + write_list_tab( + "Submodules", submodules, lambda sm: f":py:obj:`~{containing_namespace}.{module_name}.{sm}`" + ) + + for def_type, defs in [ + ("Interfaces", interfaces), + ("Classes", classes), + ("Enums", enums), + ("Functions", functions), + ]: + if defs: + tag = ":py:class:" if def_type != "Functions" else ":py:func:" + write_list_tab( + def_type, + defs, + lambda d: f"{tag}`~{containing_namespace}.{module_name}.{d.name}`" + + ( + f"\n - {ast.get_docstring(d).splitlines()[0]}" + if ast.get_docstring(d) + else "" + ), + ) + + write_line(["Description", "-----------", ""]) + if ( + len(tree.body) > 0 + and isinstance(tree.body[0], ast.Expr) + and isinstance(tree.body[0].value, ast.Constant) + ): + write_line([tree.body[0].value.value.strip(), ""]) + + write_line([f".. py:currentmodule:: {containing_namespace}.{module_name}", "", ".. TABLE OF CONTENTS", ""]) + + def write_toc_block(items, symbol, folder_name): + if items: + write_line([".. toctree::", " :titlesonly:", " :maxdepth: 1", " :hidden:", ""]) + for item in items: + write_line([f" {symbol} {item}<{module_name}/{item}>"]) + + write_toc_block(subpackages, "🖿", "subpackage") + write_toc_block(submodules, "🗎", "submodule") + + if any([classes, interfaces, enums, functions]): + for def_type, defs in [ + ("Interfaces", interfaces), + ("Classes", classes), + ("Enums", enums), + ("Functions", functions), + ]: + if defs: + symbol = {"Interfaces": "", "Classes": "", "Enums": "≔ ", "Functions": ""}[def_type] + write_line([".. toctree::", " :titlesonly:", " :maxdepth: 1", " :hidden:", ""]) + for d in defs: + write_line([f" {symbol}{d.name}<{module_name}/{d.name}>"]) + + for obj in classes + interfaces: + self._generate_rst_for_pyobj(obj, containing_namespace, module_name, str(out_file_path)) + + for enum in enums: + self._generate_rst_for_pyenum(enum, containing_namespace, module_name, str(out_file_path)) + + + for func in functions: + self._generate_rst_for_pyfunc(func, containing_namespace, module_name, str(out_file_path)) + + @staticmethod + def _parse_args(method): + args = [] + defaults = list(method.args.defaults or []) + default_offset = len(method.args.args) - len(defaults) + + for i, arg in enumerate(method.args.args): + arg_str = arg.arg + annotation = getattr(arg, "annotation", None) + if ( + isinstance(annotation, ast.Subscript) + and hasattr(annotation.value, "attr") + and annotation.value.attr == "Callable" + ): + # Defensive: handle ast.Name and ast.Attribute in callable arg types + callable_args = [] + for elt in getattr(annotation.slice.dims[0], "elts", []): + if hasattr(elt, "id"): + callable_args.append(elt.id) + elif hasattr(elt, "attr"): + # ast.Attribute: get full name + parts = [] + while isinstance(elt, ast.Attribute): + parts.append(elt.attr) + elt = elt.value + if hasattr(elt, "id"): + parts.append(elt.id) + callable_args.append(".".join(reversed(parts))) + formatted_callable_arg_types = ", ".join(callable_args) + callable_return_type = getattr(annotation.slice.dims[1], "id", None) + arg_str += f": collections.abc.Callable[[{formatted_callable_arg_types}], {callable_return_type}]" + elif isinstance(annotation, ast.Subscript): + # Defensive: handle ast.Name and ast.Attribute + type_id = getattr(annotation.value, "id", None) + if not type_id and hasattr(annotation.value, "attr"): + parts = [] + val = annotation.value + while isinstance(val, ast.Attribute): + parts.append(val.attr) + val = val.value + if hasattr(val, "id"): + parts.append(val.id) + type_id = ".".join(reversed(parts)) + arg_str += f": {type_id.lower() if type_id else ''}[{ManualRSTGenerator._parse_nested_type(annotation.slice)}]" + elif annotation is not None: + arg_str += f": {ManualRSTGenerator._parse_nested_type(annotation)}" + if i >= default_offset and defaults: + default = defaults[i - default_offset] + if isinstance(default, ast.Constant): + arg_str += f" = {default.value!r}" + args.append(arg_str) + return ", ".join(args) + + @staticmethod + def _parse_nested_type(annotation): + type_string = "" + while hasattr(annotation, "attr"): + type_string = f"{annotation.attr}.{type_string}" + if hasattr(annotation, "value"): + annotation = annotation.value + else: + break + if hasattr(annotation, "id"): + type_string = f"{annotation.id}.{type_string}" + return "~" + type_string.strip(".") + + @staticmethod + def _parse_return_type(node): + if isinstance(node, ast.Constant): + return ["None"] + if isinstance(node, ast.Subscript): + elts = getattr(node.slice, "elts", None) + if elts: + return [ManualRSTGenerator._parse_nested_type(elt) for elt in elts] + if node: + return [ManualRSTGenerator._parse_nested_type(node)] + return [] + + def _generate_rst_for_pyobj(self, obj_definition, containing_namespace, module_name, module_rst_file_path): + """Generate RST file for a Python object (class or interface). + + Parameters + ---------- + obj_definition : ast.ClassDef + Object definition in the AST. + containing_namespace : str + The namespace containing the object. + module_name : str + Name of the module. + module_rst_file_path : str + Path to the module RST file. + + Raises + ------ + RuntimeError + If a type hint does not have the proper structure. + + """ + out_dir = Path(module_rst_file_path).parent.resolve() / module_name + out_path = out_dir / f"{obj_definition.name}.rst" + out_dir.mkdir(parents=True, exist_ok=True) + + fq_name = f"{containing_namespace}.{module_name}.{obj_definition.name}" + base_classes = ", ".join(filter(None, (ManualRSTGenerator.get_base_name(base) for base in obj_definition.bases))) + + def write_docstring(f, docstring, indent=" "): + if docstring: + formatted = self._wrap_python_code_snippets(docstring) + f.write(textwrap.indent(formatted, indent) + "\n\n") + + def write_summary_table(f, title, items, ref_type): + if not items: + return + f.writelines( + [ + ".. tab-set::\n\n", + f" .. tab-item:: {title}\n\n", + " .. list-table::\n", + " :header-rows: 0\n", + " :widths: auto\n\n", + ] + ) + for item in items: + f.write(f" * - :py:{ref_type}:`~{fq_name}.{item.name}`\n") + doc = ast.get_docstring(item) + if doc: + f.write(f" - {doc.splitlines()[0]}\n") + f.write("\n") + + with out_path.open("w", encoding="utf-8") as f: + f.writelines( + [ + f"{obj_definition.name}\n", + "=" * len(obj_definition.name) + "\n\n", + f".. py:class:: {fq_name}\n\n", + f" {base_classes}\n\n", + ] + ) + write_docstring(f, ast.get_docstring(obj_definition)) + f.write(f".. py:currentmodule:: {obj_definition.name}\n\n\n") + + methods = [m for m in obj_definition.body if isinstance(m, ast.FunctionDef) and not m.name.startswith("_")] + props = [m for m in methods if any(getattr(d, "id", None) == "property" for d in m.decorator_list)] + setters = [m for m in methods if any(getattr(d, "attr", None) == "setter" for d in m.decorator_list)] + methods = [m for m in methods if m not in props and m not in setters] + + if props or methods: + f.write("Overview\n--------\n\n") + write_summary_table(f, "Methods", methods, "attr") + write_summary_table(f, "Properties", props, "attr") + + f.writelines( + [ + "Import detail\n-------------\n\n", + ".. code-block:: python\n\n", + f" from {containing_namespace}.{module_name} import {obj_definition.name}\n\n\n", + ] + ) + + if props: + f.write("Property detail\n---------------\n\n") + for p in props: + try: + ret_type = ManualRSTGenerator._parse_return_type(p.returns) + except RuntimeError as e: + ret_type = "Unknown" + f.writelines( + [ + f".. py:property:: {p.name}\n", + f" :canonical: {fq_name}.{p.name}\n", + f" :type: {ret_type}\n\n", + ] + ) + write_docstring(f, ast.get_docstring(p), " ") + + + + if methods: + f.write("Method detail\n-------------\n\n") + for m in methods: + arg_str = ManualRSTGenerator._parse_args(m) + ret_type = ManualRSTGenerator._parse_return_type(m.returns) + f.writelines( + [ + f".. py:method:: {m.name}({arg_str})", + f"{' -> ' + ', '.join(ret_type) if ret_type else ''}\n", + f" :canonical: {fq_name}.{m.name}\n\n", + ] + ) + rawdocstring = ast.get_docstring(m) + docstring = None + if rawdocstring: + docstring = NumpyDocString(rawdocstring) + + if docstring: + if "Summary" in docstring: + f.write(textwrap.indent("\n".join(docstring["Summary"]), " ") + "\n\n") + if "Extended Summary" in docstring: + f.write(textwrap.indent("\n".join(docstring["Extended Summary"]), " ") + "\n\n") + + if m.args.args: + f.write(" :Parameters:\n\n") + if docstring and "Parameters" in docstring: + for param in docstring["Parameters"]: + if len(param.desc) > 0: + if "of" in param.type: + param_types = param.type.split() + if len(param_types) == 3: + f.write( + f" **{param.name}** : :obj:`~{param_types[0]}` of :obj:`~{param_types[2]}`\n" + ) + else: + raise RuntimeError( + "Improper format for parameter containing 'of'- expecting `type` 'of' `type`." + ) + else: + f.write(f" **{param.name}** : :obj:`~{param.type}`\n") + f.write(textwrap.indent("\n".join(param.desc), " ") + "\n") + f.write("\n") + f.write("\n") + f.write("\n") + + if ret_type: + f.write(" :Returns:\n\n") + # If multiple return types, output each separately + for i in range(len(ret_type)): + if docstring and "Returns" in docstring and len(docstring["Returns"]) > i: + ret = docstring["Returns"][i] + # Output each return value as a separate entry + f.write(f" {ret.name} : :obj:`~{ret.type}`\n") + f.write(textwrap.indent("\n".join(ret.desc), " ") + "\n") + f.write("\n") + else: + # Fallback if no name, just type + f.write(f" :obj:`~{ret_type[i]}`\n\n") + f.write("\n") + + def _generate_rst_for_pyfunc(self, func_def, namespace, module_name, module_rst_path): + """Generate RST file for a Python function. + + Parameters + ---------- + func_def : ast.FunctionDef + Function definition in the AST. + namespace : str + Namespace containing the function. + module_name : str + Name of the module. + module_rst_path : str + Path to the module RST file. + + Raises + ------ + RuntimeError + If a type hint does not have the proper structure. + """ + output_dir = Path(module_rst_path).parent.resolve() / module_name + output_dir.mkdir(parents=True, exist_ok=True) + + out_path = output_dir / f"{func_def.name}.rst" + with out_path.open("w", encoding="utf-8") as f: + # Header and class declaration + f.writelines( + [ + f"{func_def.name}\n", + f"{'=' * len(func_def.name)}\n\n", + ] + ) + + # # For graphs, insert test image + # graph_module_list = [ + # "access_graphs", + # "aircraft_graphs", + # "antenna_graphs", + # "area_target_graphs", + # "chain_graphs", + # "comm_system_graphs", + # "coverage_definition_graphs", + # "facility_graphs", + # "figure_of_merit_graphs", + # "ground_vehicle_graphs", + # "launch_vehicle_graphs", + # "line_target_graphs", + # "missile_graphs", + # "place_graphs", + # "radar_graphs", + # "receiver_graphs", + # "satellite_graphs", + # "sensor_graphs", + # "ship_graphs", + # "target_graphs", + # "transmitter_graphs", + # "scenario_graphs", + # ] + # # Exclude images for untested graphs to avoid broken links + # exclude_image_functions = [ + # "tle_teme_residuals_line_chart", + # "radar_propagation_loss_line_chart", + # "flight_profile_by_downrange_line_chart", + # "flight_profile_by_time_line_chart", + # "angle_between_line_chart", + # "bentpipe_link_cno_line_chart", + # ] + # # Images generated once, but not generated as part of testing + # shared_image_functions = [ + # "model_area_line_chart", + # "solar_panel_area_line_chart", + # "solar_panel_power_line_chart", + # "obscuration_line_chart", + # ] + # # Substitute missing graphs of the same type + # substitute_graph_key = { + # "sunlight_intervals_interval_pie_chart_launchvehicle": "sunlight_intervals_interval_pie_chart_satellite", + # "sunlight_intervals_interval_pie_chart_missile": "sunlight_intervals_interval_pie_chart_satellite", + # } + # class_name = module_name.replace("_graphs", "").replace("_", "") + # if module_name in graph_module_list and func_def.name not in exclude_image_functions: + # func_module_path = f"{func_def.name}_{class_name}" + # if func_def.name in shared_image_functions: + # graph_image_path = f"/graph_images_temp/{func_def.name}.png" + # elif func_module_path in substitute_graph_key: + # substitute_path = substitute_graph_key[func_module_path] + # graph_image_path = f"/graph_images_temp/test_{substitute_path}.png" + # else: + # graph_image_path = f"/graph_images_temp/test_{func_module_path}.png" + # f.writelines( + # [ + # f".. image:: {graph_image_path}\n", + # " :width: 600\n", + # f" :alt: image of output from {func_def.name}\n\n", + # ] + # ) + + arg_str = ManualRSTGenerator._parse_args(func_def) + ret_type = ManualRSTGenerator._parse_return_type(func_def.returns) + fq_name = f"{namespace}.{module_name}.{func_def.name}" + f.writelines( + [ + f".. py:function:: {fq_name}({arg_str})", + f"{' -> ' + ', '.join(ret_type) if ret_type else ''}\n", + f" :canonical: {fq_name}\n\n", + ] + ) + + # Function docstring + rawdocstring = ast.get_docstring(func_def) + docstring = None + if rawdocstring: + docstring = NumpyDocString(rawdocstring) + + if docstring: + if "Summary" in docstring: + f.write(textwrap.indent("\n".join(docstring["Summary"]), " ") + "\n\n") + if "Extended Summary" in docstring: + f.write(textwrap.indent("\n".join(docstring["Extended Summary"]), " ") + "\n\n") + + if func_def.args.args: + f.write(" :Parameters:\n\n") + if docstring and "Parameters" in docstring: + for param in docstring["Parameters"]: + if len(param.desc) > 0: + if "Callable" in param.type: + callable_return_type = param.type.rsplit(", ")[-1].strip("]") + callable_parameter_types = param.type.split("[[")[-1].split("]")[0].split(", ") + formatted_callable_parameter_types = ", ".join( + [f":obj:`~{type}`" for type in callable_parameter_types] + ) + f.write( + f" **{param.name}** : :obj:`~collections.abc.Callable` [[{formatted_callable_parameter_types}], :obj:`~{callable_return_type}`]\n" + ) + elif "of" in param.type: + param_types = param.type.split() + if len(param_types) == 3: + f.write( + f" **{param.name}** : :obj:`~{param_types[0]}` of :obj:`~{param_types[2]}`\n" + ) + else: + raise RuntimeError( + "Improper format for parameter containing 'of'- expecting `type` 'of' `type`." + ) + else: + f.write(f" **{param.name}** : :obj:`~{param.type}`\n") + f.write(textwrap.indent("\n".join(param.desc), " ") + "\n") + f.write("\n") + f.write("\n") + f.write("\n") + + if ret_type: + f.write(" :Returns:\n\n") + for i in range(len(ret_type)): + ret = ret_type[i] + if docstring and "Returns" in docstring and len(docstring["Returns"]) >= i: + ret = docstring["Returns"][i] + f.write(f" :obj:`~{ret.type}`\n") + f.write(textwrap.indent("\n".join(ret.desc), " ") + "\n") + f.write("\n") + + if docstring and "Raises" in docstring and len(docstring["Raises"]) == 1: + ret = docstring["Raises"][0] + f.writelines( + [ + " :Raises:\n\n", + f" :obj:`~{ret.type}`\n", + textwrap.indent("\n".join(ret.desc), " ") + "\n", + ] + ) + f.write("\n") + + if docstring and "Examples" in docstring and len(docstring["Examples"]) > 0: + f.write(" :Examples:\n\n") + in_code_block = False + for example_line in docstring["Examples"]: + if in_code_block and not example_line.startswith(">>>"): + in_code_block = False + f.write("\n\n") + if not in_code_block and example_line.startswith(">>> "): + in_code_block = True + f.write("\n .. code-block:: python\n\n") + if in_code_block: + f.write(f"{textwrap.indent(example_line[len('>>> ') :], ' ')}\n") + else: + f.write(f"{textwrap.indent(example_line, ' ')}\n") + f.write("\n") + + # Set current module context + f.writelines([f".. py:currentmodule:: {func_def.name}\n\n\n"]) + + # Import statement + f.writelines( + [ + "Import detail\n", + "-------------\n\n", + ".. code-block:: python\n\n", + f" from {namespace}.{module_name} import {func_def.name}\n\n\n", + ] + ) + + def _generate_rst_for_pyenum(self, enum_def, namespace, module_name, module_rst_path): + """Generate RST file for a Python enum. + + Parameters + ---------- + enum_def : ast.ClassDef + Enum definition in the AST. + namespace : str + Namespace containing the enum. + module_name : str + Name of the module. + module_rst_path : str + Path to the module RST file. + + """ + output_dir = module_rst_path.parent.resolve() / module_name + output_dir.mkdir(parents=True, exist_ok=True) + + out_path = output_dir / f"{enum_def.name}.rst" + with out_path.open("w", encoding="utf-8") as f: + # Header and class declaration + f.writelines( + [ + f"{enum_def.name}\n", + f"{'=' * len(enum_def.name)}\n\n", + f".. py:class:: {namespace}.{module_name}.{enum_def.name}\n\n", + f" {', '.join(base.id for base in enum_def.bases)}\n\n", + ] + ) + + # Class docstring + docstring = ast.get_docstring(enum_def) + if docstring: + f.write(f"{textwrap.indent(docstring, ' ')}\n\n") + + # Set current module context + f.writelines([f".. py:currentmodule:: {enum_def.name}\n\n\n"]) + + # Extract enum members and their docstrings + members = [] + for i, node in enumerate(enum_def.body): + if isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name): + name = node.targets[0].id + value = node.value.value if isinstance(node.value, ast.Constant) else None + doc = None + if i + 1 < len(enum_def.body): + next_node = enum_def.body[i + 1] + if isinstance(next_node, ast.Expr) and isinstance(next_node.value, ast.Constant): + doc = next_node.value.value + members.append({"name": name, "value": value, "doc": doc}) + + # Render enum member table if present + if members: + f.writelines( + [ + "Overview\n", + "--------\n\n", + ".. tab-set::\n\n", + " .. tab-item:: Members\n\n", + " .. list-table::\n", + " :header-rows: 0\n", + " :widths: auto\n\n", + ] + ) + for m in members: + f.write(f" * - :py:attr:~{m['name']}\n") + if m["doc"]: + lines = m["doc"].splitlines() + f.write(f" - {lines[0]}\n") + for line in lines[1:]: + f.write(f" {line}\n") + f.write("\n") + + # Import statement + f.writelines( + [ + "Import detail\n", + "-------------\n\n", + ".. code-block:: python\n\n", + f" from {namespace}.{module_name} import {enum_def.name}\n\n\n", + ] + ) + + +def autodoc_extensions(): + """Automatically generate RST files for the extensions package.""" + namespace = "ansys.dyna.core" + module_path = Path(__file__).resolve().parent.parent / "src" / "ansys" / "dyna" / "core" + doc_path = Path(__file__).resolve().parent.parent / "doc" / "source" / "api" / "ansys" / "dyna" / "core" + + autoapi = ManualRSTGenerator(namespace, module_path, doc_path) + print(f"Generating RST files in {doc_path} from modules in {module_path}" + ) + # exit(1) + autoapi.generate_rst_for_manual_modules(auto_files=[]) + + +def main(): + """Entry point for the script.""" + autodoc_extensions() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/ansys/dyna/core/lib/card.py b/src/ansys/dyna/core/lib/card.py index da6bf0742..005930eeb 100644 --- a/src/ansys/dyna/core/lib/card.py +++ b/src/ansys/dyna/core/lib/card.py @@ -120,7 +120,13 @@ def _get_field_by_name(self, prop: str) -> Field: # not needed by subclasses - only used by methods on keyword classes def get_value(self, prop: str) -> typing.Any: - """gets the value of the field in the card""" + """gets the value of the field in the card. + + Returns + ------- + typing.Any + The value of the field. + """ field = self._get_field_by_name(prop) return field.value diff --git a/src/ansys/dyna/core/lib/card_interface.py b/src/ansys/dyna/core/lib/card_interface.py index 6fc39cf15..15cc45316 100644 --- a/src/ansys/dyna/core/lib/card_interface.py +++ b/src/ansys/dyna/core/lib/card_interface.py @@ -43,7 +43,20 @@ def __subclasshook__(cls, subclass): @abc.abstractmethod def read(self, buf: typing.TextIO, parameter_set: typing.Optional[ParameterSet]) -> None: - """Reads the card data from an input text buffer.""" + """Reads the card data from an input text buffer. + + Parameters + ---------- + buf : typing.TextIO + Input text buffer to read from. + parameter_set : typing.Optional[ParameterSet] + Parameter set to use for reading the card. Can be None. + + Raises + ------ + NotImplementedError + If the method is not implemented in the subclass. + """ raise NotImplementedError @abc.abstractmethod diff --git a/src/ansys/dyna/core/lib/card_set.py b/src/ansys/dyna/core/lib/card_set.py index fb784f46b..b9500489f 100644 --- a/src/ansys/dyna/core/lib/card_set.py +++ b/src/ansys/dyna/core/lib/card_set.py @@ -95,7 +95,13 @@ def _add_item_simple(self) -> int: self._items.append(self._set_type(parent=self._parent, keyword=self._keyword)) def add_item(self, **kwargs) -> int: - """Add a card to the set. Return the index of the added card.""" + """Add a card to the set. Return the index of the added card. + + Returns + ------- + int + The index of the added card. + """ self._items.append(self._set_type(**kwargs, parent=self._parent, keyword=self._keyword)) return len(self._items) - 1 diff --git a/src/ansys/dyna/core/lib/card_writer.py b/src/ansys/dyna/core/lib/card_writer.py index beb828c5d..c1f996e3c 100644 --- a/src/ansys/dyna/core/lib/card_writer.py +++ b/src/ansys/dyna/core/lib/card_writer.py @@ -34,7 +34,25 @@ def write_cards( write_format: format_type, comment: typing.Optional[bool] = True, ) -> bool: - """Write the cards. Return whether a superfluous trailing newline was added.""" + """Write the cards. Return whether a superfluous trailing newline was added. + + Parameters + ---------- + cards : typing.List[CardInterface] + List of cards to write. + buf : typing.TextIO + The buffer to write to. + write_format : format_type + The format to write the cards in. + comment : typing.Optional[bool], optional + Whether to include comments in the output. The default is ``True``. + + + Returns + ------- + bool + True if a superfluous trailing newline was added, False otherwise. + """ # this code tries its best to avoid adding superfluous trailing newlines, but # is not always successful. If one or more empty cards exist at the end of the diff --git a/src/ansys/dyna/core/lib/deck.py b/src/ansys/dyna/core/lib/deck.py index 806dd27e4..8d9da0ed8 100644 --- a/src/ansys/dyna/core/lib/deck.py +++ b/src/ansys/dyna/core/lib/deck.py @@ -71,7 +71,17 @@ def transform_handler(self) -> TransformHandler: return self._transform_handler def register_import_handler(self, import_handler: ImportHandler) -> None: - """Registers an ImportHandler object""" + """Registers an ImportHandler object + + Parameters + ---------- + import_handler : ImportHandler + The import handler to register. + + Returns + ------- + None + """ self._import_handlers.append(import_handler) @property @@ -98,6 +108,15 @@ def append(self, keyword: Union[KeywordBase, str], check=False) -> None: or a string. check : bool, optional The default is ``False``. + + Raises + ------ + TypeError + If the keyword is not a ``KeywordBase``, ``EncryptedKeyword``, or a string. + + Returns + ------- + None """ if not (isinstance(keyword, KeywordBase) or isinstance(keyword, str) or isinstance(keyword, EncryptedKeyword)): raise TypeError("Only keywords, encrypted keywords, or strings can be included in a deck.") @@ -374,9 +393,15 @@ def loads( Parameters ---------- value : str + Keyword file as a string. context: ImportContext the context + Returns + ------- + DeckLoaderResult + The result of loading the deck. + """ # import deck_loader only when loading to avoid circular imports @@ -497,6 +522,11 @@ def get(self, **kwargs) -> typing.List[KeywordBase]: * *filter* (``callable``) -- The filter to apply to the result. Only keywords which pass the filter will be returned. + Returns + ------- + list + List of keywords that match the criteria. + """ if "type" in kwargs: kwds = list(self.get_kwds_by_type(kwargs["type"])) diff --git a/src/ansys/dyna/core/lib/deck_plotter.py b/src/ansys/dyna/core/lib/deck_plotter.py index 9ba05ec77..3b0358109 100644 --- a/src/ansys/dyna/core/lib/deck_plotter.py +++ b/src/ansys/dyna/core/lib/deck_plotter.py @@ -29,7 +29,19 @@ def get_nid_to_index_mapping(nodes) -> typing.Dict: - """Given a node id, output the node index as a dict""" + """ + Given a node id, output the node index as a dict. + + Parameters + ---------- + nodes : pd.DataFrame + The nodes DataFrame. + + Returns + ------- + dict + Mapping from node id to index. + """ mapping = {} for idx, node in nodes.iterrows(): mapping[node["nid"]] = idx @@ -43,7 +55,19 @@ def merge_keywords( Merge mesh keywords. Given a deck, merges specific keywords (NODE, ELEMENT_SHELL, ELEMENT_BEAM, ELEMENT_SOLID) - and returns tham as data frames. + and returns them as data frames. + + Parameters + ---------- + deck : Deck + The deck object containing mesh keywords. + + Returns + ------- + nodes : pd.DataFrame + DataFrame of nodes. + df_list : dict + Dictionary of element type to DataFrame. """ nodes_temp = [kwd.nodes for kwd in deck.get_kwds_by_type("NODE")] nodes = pd.concat(nodes_temp) if len(nodes_temp) else pd.DataFrame() @@ -60,6 +84,19 @@ def merge_keywords( def process_nodes(nodes_df): + """ + Extract xyz coordinates from nodes DataFrame. + + Parameters + ---------- + nodes_df : pd.DataFrame + DataFrame of nodes. + + Returns + ------- + np.ndarray + Array of xyz coordinates. + """ nodes_xyz = nodes_df[["x", "y", "z"]] return nodes_xyz.to_numpy() @@ -68,16 +105,15 @@ def shell_facet_array(facets: pd.DataFrame) -> np.array: """ Get the shell facet array from the DataFrame. - Facets are a pandas frame that is a sequence of integers - or NAs with max length of 8. - valid rows contain 3,4,6, or 8 items consecutive from the - left. we don't plot quadratic edges so 6/8 collapse to 3/4 - invalid rows are ignored, meaning they return an empty array - return an array of length 4 or 5 using the pyvista spec - for facets which includes a length prefix - [1,2,3]=>[3,1,2,3] - [1,2,3,0]=>[3,1,2,3] - [1,2,3,NA]=>[3,1,2,3] + Parameters + ---------- + facets : pd.DataFrame + DataFrame row of facet node ids. + + Returns + ------- + np.ndarray + Array of facet node ids with length prefix for PyVista. """ facet_array = np.empty(5, dtype=np.int32) @@ -104,16 +140,15 @@ def solid_array(solids: pd.DataFrame): """ Get the solid array from the DataFrame. - Solids are a pandas frame that is a sequence of integers - or NAs with max length of 28. - valid rows contain 3, 4, 6, or 8 items consecutive from the - left. We don't plot quadratic edges so 6/8 collapse to 3/4 - invalid rows are ignored, meaning they return an empty array - return an array of length 4 or 5 using the pyvista spec - for facets which includes a length prefix - [1,2,3]=>[3,1,2,3] - [1,2,3,0]=>[3,1,2,3] - [1,2,3,NA]=>[3,1,2,3] + Parameters + ---------- + solids : pd.DataFrame + DataFrame row of solid node ids. + + Returns + ------- + np.ndarray + Array of solid node ids with length prefix for PyVista. """ # FACES CREATED BY THE SOLIDS BASED ON MANUAL @@ -160,16 +195,15 @@ def line_array(lines: pd.DataFrame) -> np.array: """ Convert DataFrame to lines array. - `lines` is a pandas frame that is a sequence of integers - or NAs with max length of 2. - valid rows contain 2 items consecutive from the - left. - invalid rows are ignored, meaning they return an empty array - return an array of length 3 using the pyvista spec - for facets which includes a length prefix - [1,2,]=>[2,1,2] - [1,2,3,0]=>[] - [1,2,3,NA]=>[] + Parameters + ---------- + lines : pd.DataFrame + DataFrame row of line node ids. + + Returns + ------- + np.ndarray + Array of line node ids with length prefix for PyVista. """ line_array = np.empty(3, dtype=np.int32) @@ -187,10 +221,23 @@ def line_array(lines: pd.DataFrame) -> np.array: def map_facet_nid_to_index(flat_facets: np.array, mapping: typing.Dict) -> np.array: - """Convert mapping to numpy array. + """ + Convert mapping to numpy array. Given a flat list of facets or lines, use the mapping from nid to python index - to output the numbering system for pyvista from the numbering from dyna + to output the numbering system for PyVista from the numbering from dyna. + + Parameters + ---------- + flat_facets : np.ndarray + Flat array of facet or line node ids with length prefix. + mapping : dict + Mapping from node id to index. + + Returns + ------- + np.ndarray + Array of node indices for PyVista. """ # Map the indexes but skip the prefix flat_facets_indexed = np.empty(len(flat_facets), dtype=np.int32) @@ -208,19 +255,25 @@ def map_facet_nid_to_index(flat_facets: np.array, mapping: typing.Dict) -> np.ar def extract_shell_facets(shells: pd.DataFrame, mapping): - """Extract shell faces from DataFrame. - - Shells table comes in with the form - | eid | nid1 | nid2 | nid3 | nid4 - | 1 | 10 | 11 | 12 | - | 20 | 21 | 22 | 23 | 24 - - but the array needed for pyvista polydata is - of the form where each element is prefixed by the length of the element node list - [3,10,11,12,4,21,22,23,24] - - Take individual rows, extract the appropriate nid's and output a flat list of - facets for pyvista + """ + Extract shell faces from DataFrame. + + Parameters + ---------- + shells : pd.DataFrame + DataFrame of shell elements. + mapping : dict + Mapping from node id to index. + + Returns + ------- + tuple + facets : np.ndarray + Flat array of shell facet node indices for PyVista. + eid : np.ndarray + Array of element ids. + pid : np.ndarray + Array of part ids. """ if len(shells) == 0: @@ -247,21 +300,25 @@ def extract_shell_facets(shells: pd.DataFrame, mapping): def extract_lines(beams: pd.DataFrame, mapping: typing.Dict[int, int]) -> np.ndarray: - """Extract lines from DataFrame. - - Beams table comes in with the form with extra information not supported, - | eid | nid1 | nid2 - | 1 | 10 | 11 - | 20 | 21 | 22 - - we only care about nid 1 and 2 - - but the array needed for pyvista polydata is the same as in extract facets - of the form where each element is prefixed by the length of the element node list - [2,10,11,2,21,22] - - Take individual rows, extract the appropriate nid's and output a flat list of - facets for pyvista + """ + Extract lines from DataFrame. + + Parameters + ---------- + beams : pd.DataFrame + DataFrame of beam elements. + mapping : dict + Mapping from node id to index. + + Returns + ------- + tuple + lines : np.ndarray + Flat array of line node indices for PyVista. + eid : np.ndarray + Array of element ids. + pid : np.ndarray + Array of part ids. """ # dont need to do this if there is no beams if len(beams) == 0: @@ -285,6 +342,28 @@ def extract_lines(beams: pd.DataFrame, mapping: typing.Dict[int, int]) -> np.nda def extract_solids(solids: pd.DataFrame, mapping: typing.Dict[int, int]): + """ + Extract solid elements from DataFrame. + + Parameters + ---------- + solids : pd.DataFrame + DataFrame of solid elements. + mapping : dict + Mapping from node id to index. + + Returns + ------- + dict + Dictionary keyed by number of nodes (4, 5, 6, 8) with values: + [connectivity, element_ids, part_ids] + connectivity : np.ndarray + Flat array of solid node indices for PyVista. + element_ids : np.ndarray + Array of element ids. + part_ids : np.ndarray + Array of part ids. + """ if len(solids) == 0: return {} @@ -325,6 +404,14 @@ def extract_solids(solids: pd.DataFrame, mapping: typing.Dict[int, int]): def get_pyvista(): + """ + Import pyvista if available. + + Returns + ------- + pyvista module + The pyvista module. + """ try: import pyvista as pv except ImportError: @@ -333,7 +420,21 @@ def get_pyvista(): def get_polydata(deck: Deck, cwd=None): - """Create the PolyData Object for plotting from a given deck with nodes and elements.""" + """ + Create the PolyData Object for plotting from a given deck with nodes and elements. + + Parameters + ---------- + deck : Deck + The deck object containing mesh keywords. + cwd : str, optional + Current working directory for deck expansion. + + Returns + ------- + pyvista.UnstructuredGrid + The PyVista UnstructuredGrid object for plotting. + """ # import this lazily (otherwise this adds over a second to the import time of pyDyna) pv = get_pyvista() @@ -411,7 +512,21 @@ def get_polydata(deck: Deck, cwd=None): def plot_deck(deck, **args): - """Plot the deck.""" + """ + Plot the deck. + + Parameters + ---------- + deck : Deck + The deck object containing mesh keywords. + **args + Additional arguments for PyVista plot. + + Returns + ------- + Any + PyVista plot output. + """ # import this lazily (otherwise this adds over a second to the import time of pyDyna) pv = get_pyvista() diff --git a/src/ansys/dyna/core/lib/encrypted_keyword.py b/src/ansys/dyna/core/lib/encrypted_keyword.py index f02053d1f..7ff680fdd 100644 --- a/src/ansys/dyna/core/lib/encrypted_keyword.py +++ b/src/ansys/dyna/core/lib/encrypted_keyword.py @@ -24,4 +24,6 @@ class EncryptedKeyword: + """Encrypted keyword representation.""" + data: str = None diff --git a/src/ansys/dyna/core/lib/field.py b/src/ansys/dyna/core/lib/field.py index fd612f5dd..9cb19cefc 100644 --- a/src/ansys/dyna/core/lib/field.py +++ b/src/ansys/dyna/core/lib/field.py @@ -106,7 +106,15 @@ def value(self, value: typing.Any) -> None: self._value = value def io_info(self) -> typing.Tuple[str, typing.Type]: - """Return the value and type used for io.""" + """Return the value and type used for io. + + Returns + ------- + str + The value to use for io. + type + The type to use for io. + """ if self._is_flag(): if self._value.value: return self._value.true_value, str diff --git a/src/ansys/dyna/core/lib/field_writer.py b/src/ansys/dyna/core/lib/field_writer.py index 818f735fd..392715730 100644 --- a/src/ansys/dyna/core/lib/field_writer.py +++ b/src/ansys/dyna/core/lib/field_writer.py @@ -156,13 +156,19 @@ def write_fields( format: format_type optional - format to write - >>> s=io.String() - >>> fields = [ - ... Field("a", int, 0, 10, 1), - ... Field("b", str, 10, 10, "hello") - ... ] - >>> write_fields(s, fields) - >>> s.getvalue() + Returns + ------- + None + + Examples + -------- + s=io.String() + fields = [ + Field("a", int, 0, 10, 1), + Field("b", str, 10, 10, "hello") + ] + write_fields(s, fields) + s.getvalue() ' 1 hello' """ if values != None: @@ -194,6 +200,12 @@ def write_comment_line( format: format_type format to write in + Returns + ------- + None + + Examples + -------- >>> s=io.String() >>> fields = [ ... Field("a", int, 0, 10, 1), diff --git a/src/ansys/dyna/core/lib/io_utils.py b/src/ansys/dyna/core/lib/io_utils.py index 51941d123..1c682a0ad 100644 --- a/src/ansys/dyna/core/lib/io_utils.py +++ b/src/ansys/dyna/core/lib/io_utils.py @@ -31,6 +31,18 @@ def write_or_return(buf: typing.Optional[typing.TextIO], func: typing.Callable) Uses the callable `func` to write. If `buf` is None, then the function will create a string buffer before calling `func` and return the result as a string. + + Parameters + ---------- + buf : typing.Optional[typing.TextIO] + The buffer to write to. If None, a string will be returned. + func : typing.Callable + The function to call to write to the buffer. + + Returns + ------- + typing.Optional[str] + If `buf` is None, then a string is returned. Otherwise, None is returned """ if buf == None: to_return = True diff --git a/src/ansys/dyna/core/lib/keyword_base.py b/src/ansys/dyna/core/lib/keyword_base.py index 95e8c1791..8080da1e5 100644 --- a/src/ansys/dyna/core/lib/keyword_base.py +++ b/src/ansys/dyna/core/lib/keyword_base.py @@ -85,7 +85,13 @@ def _get_base_title(self) -> str: return f"{kwd}_{subkwd}" def get_title(self, format_symbol: str = "") -> str: - """Get the title of this keyword.""" + """Get the title of this keyword. + + Returns + ------- + str + The title of the keyword, including any active options and format symbol. + """ base_title = self._get_base_title() titles = [base_title] if self.options != None: @@ -258,7 +264,13 @@ def write( buf.seek(buf.tell() - 1) def dumps(self) -> str: - """Return the string representation of the keyword.""" + """Return the string representation of the keyword. + + Returns + ------- + str + The string representation of the keyword. + """ warnings.warn("dumps is deprecated - use write instead") return self.write() @@ -297,7 +309,10 @@ def read(self, buf: typing.TextIO, parameters: ParameterSet = None) -> None: def loads(self, value: str, parameters: ParameterSet = None) -> typing.Any: """Load the keyword from string. - Return `self` to support chaining + Returns + ------- + self + The keyword object itself. """ # TODO - add a method to load from a buffer. s = io.StringIO() diff --git a/src/ansys/dyna/core/lib/kwd_line_formatter.py b/src/ansys/dyna/core/lib/kwd_line_formatter.py index efd2312d6..e0575de1d 100644 --- a/src/ansys/dyna/core/lib/kwd_line_formatter.py +++ b/src/ansys/dyna/core/lib/kwd_line_formatter.py @@ -29,7 +29,23 @@ def read_line(buf: typing.TextIO, skip_comment=True) -> typing.Tuple[str, bool]: - """Read and return the line, and a flag on whether to stop reading.""" + """ + Read and return the line, and a flag on whether to stop reading. + + Parameters + ---------- + buf : typing.TextIO + Buffer to read from. + skip_comment : bool, optional + Whether to skip comment lines (default: True). + + Returns + ------- + line : str or None + The line read, or None if at end or keyword. + stop : bool + True if reading should stop, False otherwise. + """ while True: line = buf.readline() len_line = len(line) @@ -47,7 +63,19 @@ def read_line(buf: typing.TextIO, skip_comment=True) -> typing.Tuple[str, bool]: def at_end_of_keyword(buf: typing.TextIO) -> bool: - """Return whether the buffer is at the end of the keyword""" + """ + Return whether the buffer is at the end of the keyword. + + Parameters + ---------- + buf : typing.TextIO + Buffer to check. + + Returns + ------- + bool + True if at the end of the keyword, False otherwise. + """ pos = buf.tell() _, end_of_keyword = read_line(buf, True) if end_of_keyword: @@ -57,9 +85,20 @@ def at_end_of_keyword(buf: typing.TextIO) -> bool: def buffer_to_lines(buf: typing.TextIO, max_num_lines: int = -1) -> typing.List[str]: - """Read from the buffer into a list of string. - buf: buffer to read from - max_num_lines: number of lines to read. -1 means no limit + """ + Read from the buffer into a list of strings. + + Parameters + ---------- + buf : typing.TextIO + Buffer to read from. + max_num_lines : int, optional + Number of lines to read. -1 means no limit (default: -1). + + Returns + ------- + list of str + List of lines read from the buffer. """ # used by tabular cards (duplicate card, duplicate card group) # store all lines until one that starts with * into an array and then call load with it. @@ -91,6 +130,19 @@ def _is_flag(item_type: typing.Union[type, Flag]): def _expand_spec(spec: typing.List[tuple]) -> typing.List[tuple]: + """ + Expand a spec to include dataclass fields. + + Parameters + ---------- + spec : list of tuple + List of (position, width, type) tuples. + + Returns + ------- + list of tuple + Expanded list of (position, width, type) tuples. + """ specs = [] for item in spec: position, width, item_type = item @@ -108,6 +160,21 @@ def _expand_spec(spec: typing.List[tuple]) -> typing.List[tuple]: def _contract_data(spec: typing.List[tuple], data: typing.List) -> typing.Iterable: + """ + Contract flat data into dataclass instances or flags as needed. + + Parameters + ---------- + spec : list of tuple + List of (position, width, type) tuples. + data : list + List of data values. + + Returns + ------- + iterable + Iterable of contracted data values. + """ iterspec = iter(spec) iterdata = iter(data) while True: @@ -125,13 +192,25 @@ def _contract_data(spec: typing.List[tuple], data: typing.List) -> typing.Iterab def load_dataline(spec: typing.List[tuple], line_data: str, parameter_set: ParameterSet = None) -> typing.List: - """loads a keyword card line with fixed column offsets and width from string - spec: list of tuples representing the (offset, width, type) of each field - type can be a Flag which represents the True and False value - line_data: string with keyword data + """ + Loads a keyword card line with fixed column offsets and width from string. + + Parameters + ---------- + spec : list of tuple + List of (offset, width, type) for each field. + line_data : str + String with keyword data. + parameter_set : ParameterSet, optional + Parameter set for resolving parameters in keyword data. - Example + Returns ------- + tuple + Tuple of parsed values from the line. + + Examples + -------- >>> load_dataline([(0,10, int),(10,10, str)], ' 1 hello') (1, 'hello') """ diff --git a/src/ansys/dyna/core/lib/parameters.py b/src/ansys/dyna/core/lib/parameters.py index de4ab962e..1532643d2 100644 --- a/src/ansys/dyna/core/lib/parameters.py +++ b/src/ansys/dyna/core/lib/parameters.py @@ -37,7 +37,19 @@ def __init__(self): self._params = dict() def get(self, param: str) -> typing.Any: - """Get a parameter by name.""" + """ + Get a parameter by name. + + Parameters + ---------- + param : str + Name of the parameter. + + Returns + ------- + typing.Any + The value of the parameter. + """ return self._params[param] def add(self, param: str, value: typing.Any) -> None: diff --git a/src/ansys/dyna/core/lib/series_card.py b/src/ansys/dyna/core/lib/series_card.py index 508bf7cb9..5b55d3b86 100644 --- a/src/ansys/dyna/core/lib/series_card.py +++ b/src/ansys/dyna/core/lib/series_card.py @@ -79,10 +79,26 @@ def _make_struct_datatype(self, type_names, type_types): return dataclasses.make_dataclass(self._name, dataclass_spec) def __iter__(self) -> typing.Iterable: + """ + Iterate over the data values. + + Returns + ------- + typing.Iterable + Iterator over the data values. + """ return iter(self._data) @property def format(self) -> format_type: + """ + Get the format type. + + Returns + ------- + format_type + The format type. + """ return self._format_type @format.setter @@ -179,6 +195,14 @@ def __get_null_value(self): @property def active(self) -> bool: + """ + Whether the card is active. + + Returns + ------- + bool + True if active, else False. + """ if self._active_func == None: return True return self._active_func() @@ -188,6 +212,19 @@ def _num_rows(self): return math.ceil(self._length_func() / fields_per_card) def __getitem__(self, index): + """ + Get item(s) by index or slice. + + Parameters + ---------- + index : int or slice + Index or slice to retrieve. + + Returns + ------- + Any or list + Value(s) at the specified index or slice. + """ err_string = f"get indexer for SeriesCard must be of the form [index] or [start:end]. End must be greater than start" # noqa : E501 if not isinstance(index, (slice, int)): raise TypeError(err_string) @@ -366,10 +403,26 @@ def _get_row_data(self, index: int, format: format_type) -> str: return self._write_row(format, start_index, end_index) def __len__(self) -> int: + """ + Get the number of elements. + + Returns + ------- + int + Number of elements. + """ return self._length_func() @property def bounded(self) -> bool: + """ + Whether the card is bounded. + + Returns + ------- + bool + True if bounded, else False. + """ return self._bounded def __repr__(self) -> str: @@ -381,7 +434,14 @@ def __repr__(self) -> str: @property def data(self): - """Gets or sets the data list of parameter values""" + """ + Gets or sets the data list of parameter values. + + Returns + ------- + list + List of parameter values. + """ return self._data @data.setter diff --git a/src/ansys/dyna/core/lib/table_card.py b/src/ansys/dyna/core/lib/table_card.py index 4257f5ed0..f8e35687c 100644 --- a/src/ansys/dyna/core/lib/table_card.py +++ b/src/ansys/dyna/core/lib/table_card.py @@ -37,6 +37,19 @@ def _check_type(value): + """ + Check if the value is a pandas DataFrame. + + Parameters + ---------- + value : Any + Value to check. + + Raises + ------ + TypeError + If value is not a pandas DataFrame. + """ global CHECK_TYPE if CHECK_TYPE: if not isinstance(value, pd.DataFrame): @@ -44,7 +57,23 @@ def _check_type(value): def try_initialize_table(card, name: str, **kwargs): - """card is a TableCard or a TableCardGroup""" + """ + Try to initialize the table for a TableCard or TableCardGroup. + + Parameters + ---------- + card : TableCard or TableCardGroup + The card object to initialize. + name : str + Name of the table. + **kwargs + Additional keyword arguments. + + Returns + ------- + bool + True if initialized, False otherwise. + """ if name is not None: data = kwargs.get(name, None) if data is not None: @@ -54,7 +83,24 @@ def try_initialize_table(card, name: str, **kwargs): def get_first_row(fields: typing.List[Field], **kwargs) -> typing.Dict[str, typing.Any]: - """Get the first row data from the kwargs.""" + """ + Get the first row data from the kwargs. + + Parameters + ---------- + fields : list of Field + List of field objects. + **kwargs + Additional keyword arguments. + + Returns + ------- + str: + Dictionary of first row data, or None if not found. + + typing.Dict[str, typing.Any] or None + Dictionary of first row data, or None if not found. + """ result = dict() for field in fields: if field.name in kwargs: diff --git a/src/ansys/dyna/core/lib/text_card.py b/src/ansys/dyna/core/lib/text_card.py index c4b7920a5..b180ce18e 100644 --- a/src/ansys/dyna/core/lib/text_card.py +++ b/src/ansys/dyna/core/lib/text_card.py @@ -38,16 +38,38 @@ def __init__(self, name: str, content: str = None, format=format_type.default): @property def bounded(self) -> bool: - """Text cards are always unbounded.""" + """ + Text cards are always unbounded. + + Returns + ------- + bool + Always False. + """ return False @property def active(self) -> bool: - """Text cards are always active.""" + """ + Text cards are always active. + + Returns + ------- + bool + Always True. + """ return True @property def format(self) -> format_type: + """ + Get the format type. + + Returns + ------- + format_type + The format type. + """ return self._format_type @format.setter @@ -55,6 +77,19 @@ def format(self, value: format_type) -> None: self._format_type = value def _get_comment(self, format: typing.Optional[format_type]): + """ + Get the comment line for the card. + + Parameters + ---------- + format : format_type, optional + The format type. + + Returns + ------- + str + The comment line. + """ if format == None: format = self._format_type if format != format_type.long: @@ -90,6 +125,14 @@ def write( @property def value(self) -> str: + """ + Get the value of the card. + + Returns + ------- + str + The card value as a string. + """ return "\n".join(self._content_lines) @value.setter diff --git a/src/ansys/dyna/core/lib/transform.py b/src/ansys/dyna/core/lib/transform.py index 87f28061b..4b22c4165 100644 --- a/src/ansys/dyna/core/lib/transform.py +++ b/src/ansys/dyna/core/lib/transform.py @@ -45,6 +45,20 @@ def register_transform_handler( self._handlers[identity] = handler def after_import(self, context: ImportContext, keyword: typing.Union[KeywordBase, str]) -> None: + """ + Perform actions after import. + + Parameters + ---------- + context : ImportContext + The import context. + keyword : KeywordBase or str + The keyword or its name. + + Returns + ------- + None + """ if not isinstance(keyword, KeywordBase): return if context.xform is None: diff --git a/src/ansys/dyna/core/pre/dynabase.py b/src/ansys/dyna/core/pre/dynabase.py index 410c2a27a..0b1156042 100644 --- a/src/ansys/dyna/core/pre/dynabase.py +++ b/src/ansys/dyna/core/pre/dynabase.py @@ -292,7 +292,12 @@ def __init__(self): self.subtype = "" def get_data(self) -> List: - """Get the data of the object.""" + """Get the data of the object. + + Returns + ------- + None + """ return None @@ -2671,14 +2676,29 @@ def __init__(self, tail=Point(0, 0, 0), head=Point(0, 0, 0), radius=1, length=10 self.type = "rigidwall_cylinder" def set_motion(self, curve, motion=RWMotion.VELOCITY, dir=Direction(1, 0, 0)): - """Set the prescribed motion.""" + """Set the prescribed motion. + Parameters + ---------- + curve : Curve + The curve to follow. + motion : RWMotion + The type of motion. + dir : Direction + The direction of motion. + """ curve.create(self.stub) self.lcid = curve.id self.motion = motion.value self.dir = dir def get_data(self) -> List: - """Get the rigidwall data.""" + """Get the rigidwall data. + + Returns + ------- + list + rigidwall data = [type, tail.x, tail.y, tail.z, head.x, head.y, head.z, radius, length] + """ data = [ self.type, self.tail.x, @@ -2753,7 +2773,13 @@ def set_motion(self, curve, motion=RWMotion.VELOCITY, dir=Direction(1, 0, 0)): self.dir = dir def get_data(self) -> List: - """Get the rigidwall data.""" + """Get the rigidwall data. + + Returns + ------- + list + rigidwall data = [type, center.x, center.y, center.z, orient.x, orient.y, orient.z, radius] + """ data = [ self.type, self.center.x, @@ -2815,7 +2841,13 @@ def __init__(self, tail=Point(0, 0, 0), head=Point(0, 0, 0), coulomb_friction_co self.type = "rigidwall_planar" def get_data(self) -> List: - """Get the rigidwall data.""" + """Get the rigidwall data. + + Returns + ------- + list + rigidwall data = [type, tail.x, tail.y, tail.z, head.x, head.y, head.z] + """ data = [self.type, self.tail.x, self.tail.y, self.tail.z, self.head.x, self.head.y, self.head.z] return data diff --git a/src/ansys/dyna/core/pre/launcher.py b/src/ansys/dyna/core/pre/launcher.py index 7301ab566..131d13865 100644 --- a/src/ansys/dyna/core/pre/launcher.py +++ b/src/ansys/dyna/core/pre/launcher.py @@ -281,6 +281,10 @@ def launch_dynapre( The default is ``"localhost"``, in which case ``"127.0.0.1"`` is used. + Returns + ------- + DynaSolution + An instance of DynaSolution. """ check_valid_ip(ip) # double check diff --git a/src/ansys/dyna/core/pre/model.py b/src/ansys/dyna/core/pre/model.py index 02667e461..81c682b2d 100644 --- a/src/ansys/dyna/core/pre/model.py +++ b/src/ansys/dyna/core/pre/model.py @@ -33,7 +33,19 @@ class Model: - """Contains all information about Ansys PyDYNA Model.""" + """Contains all information about Ansys PyDYNA Model. + + Parameters + ---------- + stub : grpc.ClientStub + The gRPC client stub for communication with the server. + + Returns + ------- + ansys.dyna.core.pre.Model + An instance of Model. + + """ def __init__(self, stub): """Initialize the model and the parameters.""" @@ -124,7 +136,17 @@ def get_part(self, id: int) -> Part: return None def get_init_velocity(self) -> List: - """Get initial velocity data.""" + """Get initial velocity data. + + Parameters + ---------- + nids: list + + Returns + ------- + list + node coordinates and velocity,list = [[x1,y1,z1,vx1,vy1,vz1],[x2,y2,z2,vx2,vy2,vz2],...] + """ nids = [i[0] for i in self._init_velocity] data = self.stub.GetNodesCoord(GetNodesCoordRequest(nodeids=nids)) num = 3 @@ -134,8 +156,14 @@ def get_init_velocity(self) -> List: return nlist1 + nlist2 def get_bdy_spc(self) -> List: - """Get boundary spc data.""" - nids = self._bdy_spc + """Get boundary spc data. + + Returns + ------- + list + node coordinates and velocity,list = [[x1,y1,z1,vx1,vy1,vz1],[x2,y2,z2,vx2,vy2,vz2],...] + """ + nids = [i[0] for i in self._bdy_spc] data = self.stub.GetNodesCoord(GetNodesCoordRequest(nodeids=nids)) num = 3 coord = data.coords @@ -250,6 +278,5 @@ def parts(self) -> List[Part]: ------- List[Part] List of parts for the model. - """ return self._parts diff --git a/src/ansys/dyna/core/run/local_solver.py b/src/ansys/dyna/core/run/local_solver.py index c8b4b09ef..cf74cff34 100644 --- a/src/ansys/dyna/core/run/local_solver.py +++ b/src/ansys/dyna/core/run/local_solver.py @@ -66,7 +66,13 @@ def __prepare(input: typing.Union[str, Deck], **kwargs) -> typing.Tuple[str, str def get_runner(**kwargs) -> typing.Any: - """Return the runner for the job.""" + """Return the runner for the job. + + Returns + ------- + runner : object + The runner object, either LinuxRunner, WindowsRunner, or DockerRunner. + """ container = kwargs.get("container", None) if container != None: if not HAS_DOCKER: diff --git a/src/ansys/dyna/core/solver/launcher.py b/src/ansys/dyna/core/solver/launcher.py index 075757f20..3b9d3fc5a 100644 --- a/src/ansys/dyna/core/solver/launcher.py +++ b/src/ansys/dyna/core/solver/launcher.py @@ -301,15 +301,20 @@ def launch_dyna( * ``241`` : Ansys 24R1 * ``242`` : Ansys 24R2 - port : int + port: int Port to launch DYNA gRPC on. Final port will be the first port available after (or including) this port. Defaults to 5000. - ip : bool, optional + ip: str, optional You can provide a hostname as an alternative to an IP address. Defaults to ``'127.0.0.1'``. + Returns + ------- + ansys.dyna.core.solver.DynaSolver + An instance of DynaSolver. + Examples -------- Launch DYNA using the best protocol. @@ -359,6 +364,7 @@ class ServerThread: """ def __init__(self, threadID, port, ip, server_path): + """Initialize the server thread.""" # threading.Thread.__init__(self) self.threadID = threadID self.port = port @@ -367,7 +373,9 @@ def __init__(self, threadID, port, ip, server_path): self.process = None def run(self): + """Run the server thread.""" self.process = launch_grpc(ip=self.ip, port=self.port, server_path=self.server_path) def termination(self): + """Terminate the server thread.""" self.process.termination()