Source code for pysubsea.abaquspy

"""
This module provides functionality to create Abaqus sensitivity files by replacing
parameter fields in an Abaqus template file and executing embedded Python blocks.

Features
--------
 - The `AbaqusSensitivity` class orchestrates the full process end-to-end.
 - Designed to generate sensitivity inputs from a master Abaqus template.
 - Designed for finite element sensitivity studies and parameter sweeps with
   Abaqus .inp files.

.. raw:: html

     <hr style="height:6px; background-color:#888; border:none; margin:1.5em 0;" />

"""

from __future__ import annotations

import builtins
import re
import textwrap

[docs] class AbaqusSensitivity: """ Class to create sensitivity files by replacing parameter fields in a master template file. Parameters ---------- template_filename : str The base name of the Abaqus template file (without .inp extension). sensitivity_filename : str The base name for the output sensitivity files. param_dict : dict Dictionary mapping parameter names to lists of values. isens : int Index of the sensitivity case to use from param_dict values. """ def __init__(self, *, template_filename: str, sensitivity_filename: str, param_dict: dict, isens: int): self.template_filename = template_filename self.sensitivity_filename = sensitivity_filename self.param_dict = param_dict self.isens = isens self.lines: list[str] = [] self.replaced_lines: list[str] | None = None @staticmethod def _compile_inline_expression(pattern: re.Pattern[str], namespace: dict, line: str) -> str: def replacer(match: re.Match[str]) -> str: expression = match.group(1) evaluator = getattr(builtins, "eval") return str(evaluator(expression, namespace, namespace)) return pattern.sub(replacer, line) @staticmethod def _is_python_start(line: str) -> bool: stripped = line.strip() return ( stripped.startswith("<py") or stripped == r"**\start_python" or stripped == r"**\start_python_global" ) @staticmethod def _is_python_end(line: str) -> bool: stripped = line.strip() return stripped in {"py>", r"**\end_python", r"**\end_python_global"} @staticmethod def _translate_block_line(line: str) -> str: stripped = line.lstrip() indentation = line[: len(line) - len(stripped)] if not stripped: return "" if stripped.startswith("$"): content = stripped[1:] if not content: return "" leading_space = " " if content[:1].isspace() else "" escaped_content = content.lstrip().replace("\\", "\\\\").replace('"', '\\"') return f'{indentation}write(f"{leading_space}{escaped_content}\\n")' if stripped.startswith("writeLine +="): rhs = stripped.split("+=", 1)[1].strip() return f"{indentation}write({rhs})" return line.rstrip("\n") def _render_text_line(self, line: str, namespace: dict) -> str: inline_pattern = re.compile(r"\$\s*\{([^{}]+)\}") stripped = line.lstrip() if stripped.startswith("$"): content = stripped[1:] leading_space = " " if content[:1].isspace() else "" content = content.lstrip() def brace_replacer(match: re.Match[str]) -> str: expression = match.group(1) evaluator = getattr(builtins, "eval") return str(evaluator(expression, namespace, namespace)) rendered = re.sub(r"\{([^{}]+)\}", brace_replacer, content) return leading_space + rendered return self._compile_inline_expression(inline_pattern, namespace, line) def _extract_python_block(self, start_index: int) -> tuple[list[str], int]: start_line = self.replaced_lines[start_index] stripped = start_line.strip() if stripped.startswith("<py"): single_line_match = re.match(r"^\s*<py(?:\s+(.*?))?\s*py>\s*$", start_line) if single_line_match: body = (single_line_match.group(1) or "").strip() return ([body] if body else []), start_index + 1 body_lines: list[str] = [] remainder = start_line[start_line.find("<py") + 3 :] if remainder.strip(): body_lines.append(remainder.rstrip("\n")) current = start_index + 1 while current < len(self.replaced_lines): candidate = self.replaced_lines[current] if candidate.strip() == "py>": return body_lines, current + 1 body_lines.append(candidate.rstrip("\n")) current += 1 raise ValueError("Missing 'py>' terminator for '<py' block.") if stripped == r"**\start_python_global": end_marker = r"**\end_python_global" elif stripped == r"**\start_python": end_marker = r"**\end_python" else: raise ValueError(f"Unrecognized Python block start: {start_line!r}") body_lines = [] current = start_index + 1 while current < len(self.replaced_lines): candidate = self.replaced_lines[current] if candidate.strip() == end_marker: return body_lines, current + 1 body_lines.append(candidate.rstrip("\n")) current += 1 raise ValueError(f"Missing {end_marker!r} terminator for Python block.") def _build_execution_namespace(self) -> tuple[dict, list[str]]: output_lines: list[str] = [] def write(*parts: object) -> None: text = "".join(str(part) for part in parts) output_lines.append(text) namespace = { "__builtins__": __builtins__, "write": write, "emit": write, "pysubsea_write": write, "abaqus_write": write, } return namespace, output_lines def _render_template(self) -> list[str]: namespace, output_lines = self._build_execution_namespace() current = 0 while current < len(self.replaced_lines): line = self.replaced_lines[current] if self._is_python_start(line): block_lines, current = self._extract_python_block(current) translated_lines = [self._translate_block_line(block_line) for block_line in block_lines] code = "\n".join(translated_lines) code = textwrap.dedent(code).strip("\n") if code.strip(): executor = getattr(builtins, "exec") executor(compile(code, f"{self.template_filename}.inp", "exec"), namespace, namespace) continue if self._is_python_end(line): raise ValueError(f"Unexpected Python block terminator: {line!r}") output_lines.append(self._render_text_line(line, namespace)) current += 1 return output_lines def _read_template(self) -> None: with open(f"{self.template_filename}.inp", "r", encoding="utf-8") as file_old: self.lines = file_old.readlines() def _replace_parameters(self) -> None: if not self.param_dict: self.replaced_lines = list(self.lines) return def replacer(match: re.Match[str]) -> str: return str(self.param_dict[match.group(0)][self.isens]) pattern = re.compile("|".join(re.escape(k) for k in self.param_dict.keys())) self.replaced_lines = [pattern.sub(replacer, line) for line in self.lines]
[docs] def run(self) -> None: """Run the full process and write the final sensitivity file.""" self._read_template() self._replace_parameters() rendered_lines = self._render_template() with open(f"{self.sensitivity_filename}.inp", "w", encoding="utf-8") as sens_file: sens_file.writelines(rendered_lines)