Skip to content

Commit 3106e33

Browse files
PProfiziansMHanmerAnsMelanie
authored
feat(doc): operator documentation generation (#2423)
Co-authored-by: Marshall Hanmer <[email protected]> Co-authored-by: AnsMelanie <[email protected]>
1 parent bad5fe2 commit 3106e33

File tree

4 files changed

+320
-0
lines changed

4 files changed

+320
-0
lines changed

.ci/generate_operators_doc.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import argparse
2+
from pathlib import Path
3+
4+
from jinja2 import Template
5+
6+
from ansys.dpf import core as dpf
7+
from ansys.dpf.core.changelog import Changelog
8+
from ansys.dpf.core.core import load_library
9+
from ansys.dpf.core.dpf_operator import available_operator_names
10+
11+
12+
def initialize_server(ansys_path=None, include_composites=False, include_sound=False):
13+
server = dpf.start_local_server(ansys_path=ansys_path)
14+
print(server.plugins)
15+
print(f"Ansys Path: {server.ansys_path}")
16+
print(f"Server Info: {server.info}")
17+
print(f"Server Context: {server.context}")
18+
print(f"Server Config: {server.config}")
19+
print(f"Server version: {dpf.global_server().version}")
20+
if include_composites:
21+
print("Loading Composites Plugin")
22+
load_library(
23+
Path(server.ansys_path)
24+
/ "dpf"
25+
/ "plugins"
26+
/ "dpf_composites"
27+
/ "composite_operators.dll"
28+
)
29+
if include_sound:
30+
print("Loading Acoustics Plugin")
31+
load_library(Path(server.ansys_path) / "Acoustics" / "SAS" / "ads" / "dpf_sound.dll")
32+
return server
33+
34+
35+
def fetch_doc_info(server, operator_name):
36+
spec = dpf.Operator.operator_specification(op_name=operator_name, server=server)
37+
input_info = []
38+
output_info = []
39+
configurations_info = []
40+
for input_pin in spec.inputs:
41+
input = spec.inputs[input_pin]
42+
input_info.append(
43+
{
44+
"pin_number": input_pin,
45+
"name": input.name,
46+
"types": [str(t) for t in input._type_names],
47+
"document": input.document,
48+
"optional": input.optional,
49+
}
50+
)
51+
for output_pin in spec.outputs:
52+
output = spec.outputs[output_pin]
53+
output_info.append(
54+
{
55+
"pin_number": output_pin,
56+
"name": output.name,
57+
"types": [str(t) for t in output._type_names],
58+
"document": output.document,
59+
"optional": output.optional,
60+
}
61+
)
62+
for configuration_key in spec.config_specification:
63+
configuration = spec.config_specification[configuration_key]
64+
configurations_info.append(
65+
{
66+
"name": configuration.name,
67+
"types": [str(t) for t in configuration.type_names],
68+
"document": configuration.document,
69+
"default_value": configuration.default_value_str,
70+
}
71+
)
72+
properties = spec.properties
73+
plugin = properties.pop("plugin", "N/A")
74+
75+
category = properties.pop("category", None)
76+
77+
scripting_name = properties.pop("scripting_name", None)
78+
if category and scripting_name:
79+
full_name = category + "." + scripting_name
80+
else:
81+
full_name = None
82+
83+
user_name = properties.pop("user_name", operator_name)
84+
85+
# Retrieve version and changelog using the Changelog class
86+
if hasattr(spec, "changelog") and isinstance(spec.changelog, dpf.GenericDataContainer):
87+
changelog_gdc = spec.changelog
88+
changelog = Changelog(gdc=changelog_gdc, server=server)
89+
last_version = changelog.last_version
90+
changelog_entries = [
91+
f"Version {str(version)}: {changelog[version]}" for version in changelog.versions
92+
]
93+
else:
94+
last_version = "0.0.0"
95+
changelog_entries = [f"Version {last_version}: Initial release."]
96+
97+
op_friendly_name = user_name
98+
if category:
99+
op_friendly_name = category + ":" + op_friendly_name
100+
101+
license = properties.pop("license", "None")
102+
103+
exposure = properties.pop("exposure", "private")
104+
scripting_info = {
105+
"category": category,
106+
"plugin": plugin,
107+
"scripting_name": scripting_name,
108+
"full_name": full_name,
109+
"internal_name": operator_name,
110+
"license": license,
111+
"version": str(last_version), # Include last version in scripting_info
112+
"changelog": changelog_entries, # Include all changelog entries
113+
}
114+
115+
return {
116+
"operator_name": op_friendly_name,
117+
"operator_description": spec.description,
118+
"inputs": input_info,
119+
"outputs": output_info,
120+
"configurations": configurations_info,
121+
"scripting_info": scripting_info,
122+
"exposure": exposure,
123+
}
124+
125+
126+
def get_plugin_operators(server, plugin_name):
127+
operators = available_operator_names(server)
128+
plugin_operators = []
129+
for operator_name in operators:
130+
spec = dpf.Operator.operator_specification(op_name=operator_name, server=server)
131+
if "plugin" in spec.properties and spec.properties["plugin"] == plugin_name:
132+
plugin_operators.append(operator_name)
133+
return plugin_operators
134+
135+
136+
def generate_operator_doc(server, operator_name, include_private):
137+
operator_info = fetch_doc_info(server, operator_name)
138+
scripting_name = operator_info["scripting_info"]["scripting_name"]
139+
category = operator_info["scripting_info"]["category"]
140+
if scripting_name:
141+
file_name = scripting_name
142+
else:
143+
file_name = operator_name
144+
if "::" in file_name:
145+
file_name = file_name.replace("::", "_")
146+
if not include_private and operator_info["exposure"] == "private":
147+
return
148+
script_path = Path(__file__)
149+
root_dir = script_path.parent.parent
150+
template_dir = Path(root_dir) / "doc" / "source" / "operators_doc" / "operator-specifications"
151+
category_dir = Path(template_dir) / category
152+
if category is not None:
153+
category_dir.mkdir(parents=True, exist_ok=True) # Ensure all parent directories are created
154+
file_dir = category_dir
155+
else:
156+
file_dir = template_dir
157+
with Path.open(Path(template_dir) / "operator_doc_template.md", "r") as file:
158+
template = Template(file.read())
159+
160+
output = template.render(operator_info)
161+
with Path.open(Path(file_dir) / f"{file_name}.md", "w") as file:
162+
file.write(output)
163+
164+
165+
def generate_toc_tree(docs_path):
166+
# Target the operator-specifications folder for iteration
167+
# operator_specs_path = docs_path / "operator-specifications"
168+
data = []
169+
for folder in docs_path.iterdir():
170+
if folder.is_dir(): # Ensure 'folder' is a directory
171+
category = folder.name
172+
operators = [] # Reset operators for each category
173+
for file in folder.iterdir():
174+
if (
175+
file.is_file() and file.suffix == ".md"
176+
): # Ensure 'file' is a file with .md extension
177+
file_name = file.name
178+
file_path = f"{category}/{file_name}"
179+
operator_name = file_name.replace("_", " ").replace(".md", "")
180+
operators.append({"operator_name": operator_name, "file_path": file_path})
181+
data.append({"category": category, "operators": operators})
182+
183+
# Render the Jinja2 template
184+
template_path = docs_path / "toc_template.j2"
185+
with Path.open(template_path, "r") as template_file:
186+
template = Template(template_file.read())
187+
output = template.render(data=data) # Pass 'data' as a named argument
188+
189+
# Write the rendered output to toc.yml at the operators_doc level
190+
# toc_path = docs_path / "toc.yml"
191+
with Path.open(docs_path / "toc.yml", "w") as file:
192+
file.write(output)
193+
194+
195+
def main():
196+
parser = argparse.ArgumentParser(description="Fetch available operators")
197+
parser.add_argument("--plugin", help="Filter operators by plugin")
198+
parser.add_argument(
199+
"--ansys_path", default=None, help="Path to Ansys DPF Server installation directory"
200+
)
201+
parser.add_argument("--include_private", action="store_true", help="Include private operators")
202+
parser.add_argument(
203+
"--include_composites", action="store_true", help="Include composites operators"
204+
)
205+
parser.add_argument("--include_sound", action="store_true", help="Include sound operators")
206+
args = parser.parse_args()
207+
desired_plugin = args.plugin
208+
209+
server = initialize_server(args.ansys_path, args.include_composites, args.include_sound)
210+
if desired_plugin is None:
211+
operators = available_operator_names(server)
212+
else:
213+
operators = get_plugin_operators(server, desired_plugin)
214+
for operator_name in operators:
215+
generate_operator_doc(server, operator_name, args.include_private)
216+
217+
docs_path = (
218+
Path(__file__).parent.parent
219+
/ "doc"
220+
/ "source"
221+
/ "operators_doc"
222+
/ "operator-specifications"
223+
)
224+
print(docs_path)
225+
generate_toc_tree(docs_path)
226+
227+
228+
if __name__ == "__main__":
229+
main()

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ instance/
6969
# Sphinx documentation
7070
doc/_build/
7171

72+
# Operators documentation
73+
doc/source/operators_doc/operator-specifications/*
74+
!doc/source/operators_doc/operator-specifications/operator_doc_template.md
75+
!doc/source/operators_doc/operator-specifications/toc_template.j2
76+
7277
# PyBuilder
7378
.pybuilder/
7479
target/
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
category: {{ scripting_info.category }}
3+
plugin: {{ scripting_info.plugin }}
4+
license: {{ scripting_info.license }}
5+
---
6+
7+
# {{ operator_name }}
8+
9+
**Version: {{ scripting_info.version }}**
10+
11+
## Description
12+
13+
{{ operator_description }}
14+
15+
## Inputs
16+
17+
| Input | Name | Expected type(s) | Description |
18+
|-------|-------|------------------|-------------|
19+
{%- for input in inputs %}
20+
| {% if not input.optional %}<strong>Pin {{ input.pin_number }}</strong> <br><span style="background-color:#d93025; color:white; padding:2px 6px; border-radius:3px; font-size:0.75em;">Required</span>{% else %}<strong>Pin {{ input.pin_number }}</strong>{% endif %}| {{ input.name }} |
21+
{%- for t in input.types -%}{% if "::" in t %}{{ t }}{% elif t == "int32" or t == "bool" or t == char or t == "double" or t == "string" or t == "uint32" or t == "uint64" or t == "vector<int32>" or t == "vector<bool>" or t == "vector<char>" or t == "vector<double>" or t == "vector<string>" or t == "vector<float>" %}[`{{ t }}`](../../core-concepts/dpf-types.md#standard-types}}){% elif t.startswith("abstract_") %}[`{{ t }}`](../../core-concepts/dpf-types.md#{{ t | replace("abstract_", "") | replace("_", "-") | replace (" ", "-") | lower}}){% else %}[`{{ t }}`](../../core-concepts/dpf-types.md#{{ t | replace("_", "-") | replace(" ", "-") | lower}}){% endif %}{% if not loop.last %}, {% endif %}{%- endfor %} | {{ input.document | replace("\n", "<br>") }} |
22+
{%- endfor %}
23+
24+
## Outputs
25+
26+
| Output | Name | Expected type(s) | Description |
27+
|-------|------|------------------|-------------|
28+
{%- for output in outputs %}
29+
| **Pin {{ output.pin_number }}**| {{ output.name }} |
30+
{%- for t in output.types -%}{% if "::" in t %}{{ t }}{% elif t == "int32" or t == "bool" or t == char or t == "double" or t == "string" or t == "uint32" or t == "uint64" or t == "vector<int32>" or t == "vector<bool>" or t == "vector<char>" or t == "vector<double>" or t == "vector<string>" or t == "vector<float>" %}[`{{ t }}`](../../core-concepts/dpf-types.md#standard-types}}){% elif t.startswith("abstract_") %}[`{{ t }}`](../../core-concepts/dpf-types.md#{{ t | replace("abstract_", "") | replace("_", "-") | replace (" ", "-") | lower}}){% else %}[`{{ t }}`](../../core-concepts/dpf-types.md#{{ t | replace("_", "-") | replace(" ", "-") | lower}}){% endif %}{% if not loop.last %}, {% endif %}{%- endfor %} | {{ output.document }} |
31+
{%- endfor %}
32+
33+
## Configurations
34+
35+
| Name| Expected type(s) | Default value | Description |
36+
|-----|------|----------|-------------|
37+
{%- for configuration in configurations %}
38+
| **{{ configuration.name }}** |
39+
{%- for t in configuration.types -%}{% if "::" in t %}{{ t }}{% elif t == "int32" or t == "bool" or t == char or t == "double" or t == "string" or t == "uint32" or t == "uint64" or t == "vector<int32>" or t == "vector<bool>" or t == "vector<char>" or t == "vector<double>" or t == "vector<string>" or t == "vector<float>" %}[`{{ t }}`](../../core-concepts/dpf-types.md#standard-types}}){% elif t.startswith("abstract_") %}[`{{ t }}`](../../core-concepts/dpf-types.md#{{ t | replace("abstract_", "") | replace("_", "-") | replace (" ", "-") | lower}}){% else %}[`{{ t }}`](../../core-concepts/dpf-types.md#{{ t | replace("_", "-") | replace(" ", "-") | lower}}){% endif %}{% if not loop.last %}, {% endif %}{%- endfor %} | {{ configuration.default_value }} | {{ configuration.document }} |
40+
{%- endfor %}
41+
42+
## Scripting
43+
44+
**Category**: {{ scripting_info.category }}
45+
46+
**Plugin**: {{ scripting_info.plugin }}
47+
48+
**Scripting name**: {{ scripting_info.scripting_name }}
49+
50+
**Full name**: {{ scripting_info.full_name }}
51+
52+
**Internal name**: {{ scripting_info.internal_name }}
53+
54+
**License**: {{ scripting_info.license }}
55+
56+
57+
## Changelog
58+
59+
{%- for entry in scripting_info.changelog %}
60+
61+
- {{ entry }}
62+
{%- endfor %}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
- name: Introduction
2+
href: index.md
3+
- name: Getting started
4+
items:
5+
- name: Using data containers
6+
href: getting-started/using-data-containers.md
7+
- name: Using operators
8+
href: getting-started/using-operators.md
9+
- name: Workflow examples
10+
href: getting-started/workflow-examples.md
11+
- name: Core concepts
12+
items:
13+
- name: Available types
14+
href: core-concepts/dpf-types.md
15+
- name: Operator
16+
href: core-concepts/operator.md
17+
- name: Operator specifications
18+
items:
19+
{% for category in data -%}
20+
- name: {{ category.category }}
21+
items:{% for operator in category.operators %}
22+
- name: {{ operator.operator_name }}
23+
href: operator-specifications/{{ operator.file_path }}{% endfor %}
24+
{% endfor %}

0 commit comments

Comments
 (0)