Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8a5e1aa
add fields calc expressions
gmalinve Dec 10, 2025
aeca325
chore: adding changelog file 6996.added.md [dependabot-skip]
pyansys-ci-bot Dec 10, 2025
f7864f9
chore: adding changelog file 6996.added.md [dependabot-skip]
pyansys-ci-bot Dec 10, 2025
08555ec
add tests
gmalinve Dec 11, 2025
675ef8c
add system tests
gmalinve Dec 19, 2025
916506e
Merge branch 'refs/heads/main' into feat/get_fields_expressions
gmalinve Dec 19, 2025
d486f9f
add mock test fields calc write
gmalinve Dec 19, 2025
4822613
mock tests fields calc.
gmalinve Dec 22, 2025
c0fddee
leverage Path instead of os.path + move tests
gmalinve Dec 22, 2025
ccaf030
Merge branch 'refs/heads/main' into feat/get_fields_expressions
gmalinve Dec 22, 2025
d497917
update new API in fields dist. extension
gmalinve Dec 22, 2025
42dae28
update new API in fields dist. extension
gmalinve Dec 22, 2025
66aa7cc
fix load expressions fields calc.
gmalinve Dec 22, 2025
b78648d
Merge branch 'refs/heads/main' into feat/get_fields_expressions
gmalinve Dec 22, 2025
affb4c2
Update src/ansys/aedt/core/visualization/post/fields_calculator.py
gmalinve Jan 7, 2026
58efb5c
Update src/ansys/aedt/core/visualization/post/fields_calculator.py
gmalinve Jan 7, 2026
b350f07
Update src/ansys/aedt/core/visualization/post/fields_calculator.py
gmalinve Jan 7, 2026
ab3156e
CHORE: Auto fixes from pre-commit hooks
pre-commit-ci[bot] Jan 7, 2026
658d817
Update tests/system/visualization/test_fields_calculator.py
gmalinve Jan 7, 2026
fb7fa13
CHORE: Auto fixes from pre-commit hooks
pre-commit-ci[bot] Jan 7, 2026
48e5387
Merge branch 'refs/heads/main' into feat/get_fields_expressions
gmalinve Jan 7, 2026
6d9a04b
code review UTs
gmalinve Jan 7, 2026
f37a079
fix failing tests mock aedt version
gmalinve Jan 7, 2026
a91df91
fix indent
gmalinve Jan 7, 2026
8d6f750
code review
gmalinve Jan 7, 2026
43778b9
first failing test
gmalinve Jan 8, 2026
1e1b2f9
Merge branch 'refs/heads/main' into feat/get_fields_expressions
gmalinve Jan 8, 2026
e4ebd5b
code review
gmalinve Jan 8, 2026
b4bf7d9
Update tests/unit/test_fields_calculator.py
gmalinve Jan 8, 2026
3ce879a
API change in 2026.1
gmalinve Jan 8, 2026
20bb4f2
Merge remote-tracking branch 'origin/feat/get_fields_expressions' int…
gmalinve Jan 8, 2026
f9361a6
Update tests/unit/test_fields_calculator.py
gmalinve Jan 8, 2026
ff3f64f
Update tests/unit/test_fields_calculator.py
gmalinve Jan 8, 2026
d2c9802
Update tests/unit/test_fields_calculator.py
gmalinve Jan 8, 2026
a5efd8e
Update tests/unit/test_fields_calculator.py
gmalinve Jan 8, 2026
50c87d2
Merge remote-tracking branch 'origin/feat/get_fields_expressions' int…
gmalinve Jan 8, 2026
2783fee
CHORE: Auto fixes from pre-commit hooks
pre-commit-ci[bot] Jan 8, 2026
ff556b1
Merge remote-tracking branch 'origin/feat/get_fields_expressions' int…
gmalinve Jan 8, 2026
634df56
API change in 2026.1
gmalinve Jan 8, 2026
3e14750
comment review
gmalinve Jan 8, 2026
de67aa4
Update tests/unit/test_fields_calculator.py
gmalinve Jan 8, 2026
8c88740
Merge branch 'main' into feat/get_fields_expressions
gmalinve Jan 8, 2026
244c300
improve code cov
gmalinve Jan 9, 2026
5558017
Update tests/unit/test_fields_calculator.py
gmalinve Jan 9, 2026
e6c0ac2
mock patch
gmalinve Jan 9, 2026
f0ca145
Merge branch 'refs/heads/main' into feat/get_fields_expressions
gmalinve Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/6996.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add fields calc expressions and update fields distribution extension
25 changes: 14 additions & 11 deletions src/ansys/aedt/core/extensions/maxwell3d/fields_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,20 @@ def check_design_type(self):
def __load_aedt_info(self):
"""Load Maxwell design info."""
# Get named expressions for field quantities
point = self.aedt_application.modeler.create_point([0, 0, 0])
self.__named_expressions = self.aedt_application.post.available_report_quantities(
report_category="Fields", context=point.name, quantities_category="Calculator Expressions"
)

# Load vector fields from JSON
json_path = Path(__file__).resolve().parent / "vector_fields.json"
with open(json_path, "r") as f:
vector_fields = json.load(f)
self.__named_expressions.extend(vector_fields[self.aedt_application.design_type])
point.delete()
# make it backward compatible by implementing a check on AEDT version
if self.aedt_application._aedt_version < "2026.1":
point = self.aedt_application.modeler.create_point([0, 0, 0])
self.__named_expressions = self.aedt_application.post.available_report_quantities(
report_category="Fields", context=point.name, quantities_category="Calculator Expressions"
)
# Load vector fields from JSON
json_path = Path(__file__).resolve().parent / "vector_fields.json"
with open(json_path, "r") as f:
vector_fields = json.load(f)
self.__named_expressions.extend(vector_fields[self.aedt_application.design_type])
point.delete()
else:
self.__named_expressions = self.aedt_application.post.fields_calculator.get_expressions()

# Get objects list
self.__objects_list = list(self.aedt_application.modeler.objects_by_name.keys())
Expand Down
111 changes: 76 additions & 35 deletions src/ansys/aedt/core/visualization/post/fields_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@
# SOFTWARE.

import copy
import os
from pathlib import Path

from jsonschema import exceptions
from jsonschema import validate

import ansys.aedt.core
from ansys.aedt.core.base import PyAedtBase
from ansys.aedt.core.generic.file_utils import generate_unique_name
from ansys.aedt.core.generic.file_utils import generate_unique_project_name
from ansys.aedt.core.generic.file_utils import open_file
from ansys.aedt.core.generic.file_utils import read_configuration_file
from ansys.aedt.core.generic.general_methods import pyaedt_function_handler
from ansys.aedt.core.internal.checks import min_aedt_version
from ansys.aedt.core.internal.errors import AEDTRuntimeError

PARENT_DIR = Path(__file__).parent


class FieldsCalculator(PyAedtBase):
Expand Down Expand Up @@ -91,22 +94,10 @@ class FieldsCalculator(PyAedtBase):

def __init__(self, app):
self.expression_catalog = read_configuration_file(
os.path.join(
ansys.aedt.core.__path__[0],
"visualization",
"post",
"fields_calculator_files",
"expression_catalog.toml",
)
PARENT_DIR / "fields_calculator_files" / "expression_catalog.toml"
)
self.expression_schema = read_configuration_file(
os.path.join(
ansys.aedt.core.__path__[0],
"visualization",
"post",
"fields_calculator_files",
"fields_calculator.schema.json",
)
PARENT_DIR / "fields_calculator_files" / "fields_calculator.schema.json"
)
self.__app = app
self.design_type = app.design_type
Expand Down Expand Up @@ -253,7 +244,7 @@ def add_expression(self, calculation, assignment, name=None):

# Import clc
self.ofieldsreporter.LoadNamedExpressions(
os.path.abspath(file_name), expression_info["fields_type"][design_type_index], [name]
str(Path(file_name).resolve()), expression_info["fields_type"][design_type_index], [name]
)

return expression_info["name"]
Expand Down Expand Up @@ -289,7 +280,7 @@ def create_expression_file(self, name, operations):
file.write("$end 'Named_Expression'\n")
except Exception: # pragma: no cover
return False
return os.path.abspath(file_name)
return str(Path(file_name).resolve())

@pyaedt_function_handler()
def expression_plot(self, calculation, assignment, names, setup=None):
Expand Down Expand Up @@ -476,21 +467,19 @@ def load_expression_file(self, input_file):
--------
>>> from ansys.aedt.core import Hfss
>>> hfss = Hfss()
>>> my_toml = os.path.join("my_path_to_toml", "my_toml.toml")
>>> my_toml = str(Path("my_path_to_toml") / "my_toml.toml")
>>> new_catalog = hfss.post.fields_calculator.load_expression_file(my_toml)
>>> hfss.desktop_class.release_desktop(False, False)
"""
if not os.path.isfile(input_file):
if not Path(input_file).is_file():
self.__app.logger.error("File does not exist.")
return False

new_expression_catalog = read_configuration_file(input_file)

if new_expression_catalog:
for _, new_expression_props in new_expression_catalog.items():
new_expression = self.validate_expression(new_expression_props)
if new_expression:
self.expression_catalog.update(new_expression)
for new_expression_key, new_expression_props in new_expression_catalog.items():
if self.validate_expression(new_expression_props):
self.expression_catalog[new_expression_key] = new_expression_props

return self.expression_catalog

Expand Down Expand Up @@ -561,14 +550,14 @@ def write(self, expression, output_file, setup=None, intrinsics=None):
>>> hfss = Hfss()
>>> poly = hfss.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
>>> expr_name = hfss.post.fields_calculator.add_expression("voltage_line", "Polyline1")
>>> file_path = os.path.join(hfss.working_directory, "my_expr.fld")
>>> file_path = Path(hfss.working_directory) / "my_expr.fld"
>>> hfss.post.fields_calculator.write("voltage_line", file_path, hfss.nominal_adaptive)
>>> hfss.desktop_class.release_desktop(False, False)
"""
if not self.is_expression_defined(expression):
self.__app.logger.error("Expression does not exist in current stack.")
return False
if os.path.splitext(output_file)[1] not in [".fld", ".reg"]:
if Path(output_file).suffix not in [".fld", ".reg"]:
self.__app.logger.error("Invalid file extension. Accepted extensions are '.fld' and '.reg'.")
return False
if not setup:
Expand All @@ -580,7 +569,11 @@ def write(self, expression, output_file, setup=None, intrinsics=None):
self.ofieldsreporter.CalcStack("clear")
self.ofieldsreporter.CopyNamedExprToStack(expression)
args = []
for k, v in self.__app.variable_manager.design_variables.items():
for k, v in (
self.__app.variable_manager.design_variables | self.__app.variable_manager.project_variables
).items():
if "[" in v.evaluated_value:
raise AEDTRuntimeError("The method does not currently support array variables.")
args.append(f"{k}:=")
args.append(v.expression)
intrinsics = self.__app.post._check_intrinsics(intrinsics)
Expand All @@ -589,7 +582,12 @@ def write(self, expression, output_file, setup=None, intrinsics=None):
continue
args.append(f"{k}:=")
args.append(v)
self.ofieldsreporter.CalculatorWrite(output_file, ["Solution:=", setup], args)
if self.__app.aedt_version_id < "2026.1":
self.ofieldsreporter.CalculatorWrite(output_file, ["Solution:=", setup], args)
else:
solution_args = ["NAME:Setup", "Solution:=", setup]
expression_args = ["NAME:Expression", "NameOfExpression:=", [expression]]
self.ofieldsreporter.CalculatorWrite(output_file, ["NAME:Write", solution_args, expression_args], args)
self.ofieldsreporter.CalcStack("clear")
return True

Expand Down Expand Up @@ -620,16 +618,19 @@ def evaluate(self, expression, setup=None, intrinsics=None):
float
Value computed.
"""
out_file = os.path.join(self.__app.working_directory, generate_unique_name("expression") + ".fld")
self.write(expression, setup=setup, intrinsics=intrinsics, output_file=out_file)
out_file = Path(self.__app.working_directory) / (generate_unique_name("expression") + ".fld")
self.write(expression, setup=setup, intrinsics=intrinsics, output_file=str(out_file))

# ClcEval does not return any value
# This is why we open the and read the file
value = None
if os.path.exists(out_file):
if out_file.exists():
with open_file(out_file, "r") as f:
lines = f.readlines()
lines = [line.strip() for line in lines]
value = lines[-1]
try:
os.remove(out_file)
out_file.unlink()
except OSError:
pass
return value
Expand Down Expand Up @@ -804,10 +805,50 @@ def export(
)
return False

if os.path.exists(output_file):
return output_file
if output_file:
output_file = Path(output_file)

if output_file.exists():
return str(output_file)
return False

@pyaedt_function_handler()
@min_aedt_version("2026.1")
def get_expressions(self, field_type: str = None) -> dict: # pragma: no cover
"""Get dictionary of available Field Calculator expressions.

Parameters
----------
field_type : str, optional
Field type. Options are ``"Fields"`` or ``"Time Averaged Fields"``.
The default is ``None``, in which case all expressions are returned.

Returns
-------
dict
Field Calculator expressions.
Where key is the named expression and value is the expression string.

References
----------
>>> oModule.GetFieldsCalculatorExpressions
Example
-------
>>> from ansys.aedt.core import Hfss
>>> hfss = Hfss()
>>> poly = hfss.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
>>> exprs = hfss.post.fields_calculator.get_expressions()
>>> hfss.desktop_class.release_desktop(False, False)
"""
expressions = {}
field_type = field_type or ""
if field_type and field_type not in ["Fields", "Time Averaged Fields"]:
raise AEDTRuntimeError("Invalid field type.")
for expr in self.ofieldsreporter.GetFieldsCalculatorExpressions(field_type):
k, val = map(str.strip, expr.split("=", 1))
expressions[k] = val
return expressions

@staticmethod
def __has_integer(lst): # pragma: no cover
"""Check if a list has integers."""
Expand Down
121 changes: 121 additions & 0 deletions tests/system/visualization/test_fields_calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 - 2026 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import pytest

from ansys.aedt.core import Hfss
from tests.conftest import DESKTOP_VERSION

test_subfolder = "fields_calculator"


@pytest.fixture()
def aedtapp(add_app):
app = add_app(application=Hfss)
yield app
app.close_project(app.project_name)


def test_add_expressions(aedtapp):
"""Test adding custom field calculator expressions."""
poly = aedtapp.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
my_expression = {
"name": "test",
"description": "Voltage drop along a line",
"design_type": ["HFSS", "Q3D Extractor"],
"fields_type": ["Fields", "CG Fields"],
"solution_type": "",
"primary_sweep": "Freq",
"assignment": "",
"assignment_type": ["Line"],
"operations": [
"Fundamental_Quantity('E')",
"Operation('Real')",
"Operation('Tangent')",
"Operation('Dot')",
"EnterLine('assignment')",
"Operation('LineValue')",
"Operation('Integrate')",
"Operation('CmplxR')",
],
"report": ["Data Table", "Rectangular Plot"],
}
expr_name = aedtapp.post.fields_calculator.add_expression(my_expression, poly.name)
assert isinstance(expr_name, str)
assert expr_name == "test"


def test_expression_plot(aedtapp):
"""Test creating plots from field calculator expressions."""
aedtapp.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
aedtapp.create_setup()
expr_name = aedtapp.post.fields_calculator.add_expression("voltage_line", "Polyline1")
reports = aedtapp.post.fields_calculator.expression_plot("voltage_line", "Polyline1", [expr_name])
assert reports
assert aedtapp.post.plots
assert reports == aedtapp.post.plots
assert reports[0].expressions == ["Voltage_Line"]


def test_delete_expression(aedtapp):
"""Test deleting named expressions from the fields calculator."""
poly = aedtapp.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
expr_name = aedtapp.post.fields_calculator.add_expression("voltage_line", poly.name)
aedtapp.post.fields_calculator.delete_expression(expr_name)
aedtapp.post.fields_calculator.delete_expression()


def test_is_expression_defined(aedtapp):
"""Test checking if a named expression exists in the fields calculator."""
aedtapp.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
aedtapp.create_setup()
assert not aedtapp.post.fields_calculator.is_expression_defined("Voltage_Line")
expr_name = aedtapp.post.fields_calculator.add_expression("voltage_line", "Polyline1")
assert aedtapp.post.fields_calculator.is_expression_defined(expr_name)


def test_is_general_expression(aedtapp):
"""Test identifying general vs. assignment-specific expressions."""
aedtapp.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
aedtapp.create_setup()
assert not aedtapp.post.fields_calculator.is_general_expression("invalid")
assert not aedtapp.post.fields_calculator.is_general_expression("voltage_line")
assert aedtapp.post.fields_calculator.is_general_expression("voltage_drop")


def test_validate_expression(aedtapp):
"""Test expression validation against the schema."""
assert not aedtapp.post.fields_calculator.validate_expression("invalid")


@pytest.mark.skipif(DESKTOP_VERSION < "2026.1", reason="Native API method only available from AEDT 2026.1 onwards.")
def test_get_expressions(aedtapp):
"""Test deleting named expressions from the fields calculator."""
poly = aedtapp.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1")
expressions = aedtapp.post.fields_calculator.get_expressions()
assert "Voltage_Line" not in list(expressions.keys())
aedtapp.post.fields_calculator.add_expression("voltage_line", poly.name)
expressions = aedtapp.post.fields_calculator.get_expressions()
assert "Voltage_Line" in list(expressions.keys())
6 changes: 4 additions & 2 deletions tests/unit/extensions/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,23 @@ def mock_hfss_3d_layout_app():


@pytest.fixture
def mock_maxwell_3d_app():
def mock_maxwell_3d_app(request):
"""Fixture to mock Maxwell 3D application."""
with patch.object(ExtensionCommon, "aedt_application", new_callable=PropertyMock) as mock_property:
mock_instance = MagicMock()
mock_instance.design_type = "Maxwell 3D"
mock_instance._aedt_version = getattr(request, "param", "2026.1")
mock_property.return_value = mock_instance
yield mock_instance


@pytest.fixture
def mock_maxwell_2d_app():
def mock_maxwell_2d_app(request):
"""Fixture to mock Maxwell 2D application."""
with patch.object(ExtensionCommon, "aedt_application", new_callable=PropertyMock) as mock_property:
mock_instance = MagicMock()
mock_instance.design_type = "Maxwell 2D"
mock_instance._aedt_version = getattr(request, "param", "2026.1")
mock_property.return_value = mock_instance
yield mock_instance

Expand Down
Loading
Loading