Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement simulation object validation checks #94

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
get_post_process_command_lines,
get_combined_parameters,
export_simulation_json,
validate_simulation,
)
from kqcircuits.util.export_helper import write_commit_reference_file
from kqcircuits.simulations.export.util import export_layers
Expand Down Expand Up @@ -177,6 +178,7 @@ def export_ansys(
common_sol = None if all(isinstance(s, Sequence) for s in simulations) else get_ansys_solution(**solution_params)
for sim_sol in simulations:
simulation, solution = sim_sol if isinstance(sim_sol, Sequence) else (sim_sol, common_sol)
validate_simulation(simulation, solution)
try:
json_filenames.append(export_ansys_json(simulation, solution, path))
except (IndexError, ValueError, Exception) as e: # pylint: disable=broad-except
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
get_post_process_command_lines,
get_combined_parameters,
export_simulation_json,
validate_simulation,
)
from kqcircuits.simulations.export.util import export_layers
from kqcircuits.util.export_helper import write_commit_reference_file
Expand Down Expand Up @@ -590,6 +591,7 @@ def export_elmer(
Returns:
Path to exported script file.
"""

common_sol = None if all(isinstance(s, Sequence) for s in simulations) else get_elmer_solution(**solution_params)

workflow = _update_elmer_workflow(simulations, common_sol, workflow)
Expand All @@ -605,7 +607,7 @@ def export_elmer(

for sim_sol in simulations:
simulation, solution = sim_sol if isinstance(sim_sol, Sequence) else (sim_sol, common_sol)

validate_simulation(simulation, solution)
try:
json_filenames.append(export_elmer_json(simulation, solution, path, workflow))
except (IndexError, ValueError, Exception) as e: # pylint: disable=broad-except
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from kqcircuits.simulations.export.util import export_layers
from kqcircuits.util.geometry_json_encoder import GeometryJsonEncoder
from kqcircuits.simulations.export.simulation_validate import ValidateSim


def get_combined_parameters(simulation, solution):
Expand Down Expand Up @@ -158,3 +159,18 @@ def cross_combine(simulations, solutions):
solutions if isinstance(solutions, Sequence) else [solutions],
)
)


def validate_simulation(simulation, solution):
"""Analyses a Simulation object or list and raises an error if specific inconsistencies are found.

Args:
simulations: A Simulation object.
solutions: A Solution object.
Raises:
Errors when validation criteria are not met.
"""
validate_sim = ValidateSim()
validate_sim.has_no_ports_when_required(simulation, solution)
validate_sim.has_edgeport_when_forbidden(simulation, solution)
validate_sim.flux_integration_layer_exists_if_needed(simulation, solution)
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# This code is part of KQCircuits
# Copyright (C) 2024 IQM Finland Oy
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see
# https://www.gnu.org/licenses/gpl-3.0.html.
#
# The software distribution should follow IQM trademark policy for open-source software
# (meetiqm.com/iqm-open-source-trademark-policy). IQM welcomes contributions to the code.
# Please see our contribution agreements for individuals (meetiqm.com/iqm-individual-contributor-license-agreement)
# and organizations (meetiqm.com/iqm-organization-contributor-license-agreement).
from kqcircuits.simulations.export.ansys.ansys_solution import (
AnsysEigenmodeSolution,
AnsysCurrentSolution,
AnsysVoltageSolution,
AnsysHfssSolution,
)
from kqcircuits.simulations.export.elmer.elmer_solution import ElmerVectorHelmholtzSolution, ElmerCapacitanceSolution


class ValidateSim:
"""Validation class that contains consistency checks."""

def has_no_ports_when_required(self, simulation, solution):
"""Validation check: ensures that a simulation object has ports when the solution type requires it.
Args:
simulations: A Simulation object.
solutions: A Solution object.
Raises:
Errors when validation criteria are not met.
"""
port_names = get_port_names(simulation)
sim_name = simulation.name
if not port_names and type(solution) in [
AnsysHfssSolution,
AnsysVoltageSolution,
AnsysCurrentSolution,
ElmerVectorHelmholtzSolution,
ElmerCapacitanceSolution,
]:
raise ValidateSimError(
f"Simulation '{sim_name}' has no ports assigned. This is incompatible with {type(solution)}",
validation_type=self.has_no_ports_when_required.__name__,
)

def has_edgeport_when_forbidden(self, simulation, solution):
"""Validation check: ensure that if at least one "EdgePort" is present, some solution types can't be chosen.
Args:
simulations: A Simulation object.
solutions: A Solution object.
Raises:
Errors when validation criteria are not met.
"""
port_names = get_port_names(simulation)
sim_name = simulation.name
if "EdgePort" in port_names and type(solution) in [
AnsysEigenmodeSolution,
AnsysVoltageSolution,
AnsysCurrentSolution,
]:
raise ValidateSimError(
f"Simulation '{sim_name}' has at least one 'EdgePort'. This is incompatible with {type(solution)}",
validation_type=self.has_edgeport_when_forbidden.__name__,
)

def flux_integration_layer_exists_if_needed(self, simulation, solution):
"""Validation check related to the presence of layers and magnetic flux integration.
Args:
simulation: A Simulation object.
Raises:
Errors when validation criteria are not met.
"""
sim_name = simulation.name
has_integrate_flux = hasattr(solution, "integrate_magnetic_flux")
integrate_flux = solution.integrate_magnetic_flux if has_integrate_flux else False

# Ensures that a layer with thickness == 0 and a material != "pec"
# exists in the setup when "integrate_magnetic_flux" is True.
if integrate_flux:
layers = simulation.layers
has_flux_integration_layer = False
for layer in layers.values():
if layer.get("thickness", -1) == 0 and layer.get("material", "") != "pec":
has_flux_integration_layer = True
break
if not has_flux_integration_layer:
raise ValidateSimError(
f"Simulation '{sim_name}' has 'integrate_magnetic_flux = True' "
+ "but the integration layer is missing.",
validation_type=self.flux_integration_layer_exists_if_needed.__name__,
)


def get_port_names(simulation):
"""Helper function that returns a list of port names in a Simulation object.
Args:
simulation: A Simulation object.
Returns:
port_names: A list of names related to the ports present in simulation.
"""
port_list = simulation.ports
port_names = []
for port in port_list:
port_names.append(type(port).__name__)
return port_names


class ValidateSimError(Exception):
"""Custom exception class for specific error handling."""

def __init__(self, message, validation_type=None):
super().__init__(message)
self.validation_type = validation_type
5 changes: 3 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ def get_sim(cls, **parameters):

@pytest.fixture
def perform_test_ansys_export_produces_output_files(tmp_path, get_simulation):
def perform_test_ansys_export_produces_output_implementation(cls, **parameters):
bat_filename = export_ansys([get_simulation(cls, **parameters)], path=tmp_path)
def perform_test_ansys_export_produces_output_implementation(cls, ansys_solution=None, **parameters):
simulation = get_simulation(cls, **parameters)
bat_filename = export_ansys([(simulation, ansys_solution) if ansys_solution else simulation], path=tmp_path)
assert Path(bat_filename).exists()

return perform_test_ansys_export_produces_output_implementation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@


from kqcircuits.simulations.empty_simulation import EmptySimulation
from kqcircuits.simulations.export.ansys.ansys_solution import AnsysSolution


def test_export_produces_output_files(perform_test_ansys_export_produces_output_files):
perform_test_ansys_export_produces_output_files(EmptySimulation)
perform_test_ansys_export_produces_output_files(EmptySimulation, ansys_solution=AnsysSolution())
157 changes: 157 additions & 0 deletions tests/simulations/simulation_validate/test_simulation_validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# This code is part of KQCircuits
# Copyright (C) 2024 IQM Finland Oy
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see
# https://www.gnu.org/licenses/gpl-3.0.html.
#
# The software distribution should follow IQM trademark policy for open-source software
# (meetiqm.com/iqm-open-source-trademark-policy). IQM welcomes contributions to the code.
# Please see our contribution agreements for individuals (meetiqm.com/iqm-individual-contributor-license-agreement)
# and organizations (meetiqm.com/iqm-organization-contributor-license-agreement).

import pytest
from kqcircuits.simulations.export.simulation_validate import ValidateSim
from kqcircuits.simulations.simulation import Simulation
from kqcircuits.simulations.port import InternalPort, EdgePort
from kqcircuits.simulations.export.ansys.ansys_solution import (
AnsysCurrentSolution,
AnsysHfssSolution,
AnsysEigenmodeSolution,
AnsysVoltageSolution,
)
from kqcircuits.simulations.export.elmer.elmer_solution import (
ElmerVectorHelmholtzSolution,
ElmerCapacitanceSolution,
)
from kqcircuits.simulations.export.simulation_validate import ValidateSimError


@pytest.fixture
def mock_simulation(layout):
simulation = Simulation(layout)
simulation.name = "test_sim"
return simulation


@pytest.mark.parametrize(
"solution",
[
AnsysCurrentSolution(),
AnsysHfssSolution(),
AnsysVoltageSolution(),
ElmerVectorHelmholtzSolution(),
ElmerCapacitanceSolution(),
],
)
def test_has_no_ports_when_required(mock_simulation, solution):
ports = [InternalPort(0, [0, 0, 1, 1])]
mock_simulation.ports = ports
validator = ValidateSim()
validator.has_no_ports_when_required(mock_simulation, solution)


@pytest.mark.parametrize(
"solution",
[
AnsysCurrentSolution(),
AnsysHfssSolution(),
AnsysVoltageSolution(),
ElmerVectorHelmholtzSolution(),
ElmerCapacitanceSolution(),
],
)
def test_raise_no_port_error_when_required(mock_simulation, solution):
validator = ValidateSim()
with pytest.raises(ValidateSimError) as expected_error:
validator.has_no_ports_when_required(mock_simulation, solution)
assert expected_error.value.validation_type == "has_no_ports_when_required"


@pytest.mark.parametrize("solution", [AnsysEigenmodeSolution(), AnsysVoltageSolution(), AnsysCurrentSolution()])
def test_has_edgeport_when_forbidden(mock_simulation, solution):
ports = [InternalPort(0, [0, 0, 1, 1])]
mock_simulation.ports = ports
validator = ValidateSim()
validator.has_edgeport_when_forbidden(mock_simulation, solution)


@pytest.mark.parametrize("solution", [AnsysEigenmodeSolution(), AnsysVoltageSolution(), AnsysCurrentSolution()])
def test_raise_edgeport_error_when_forbidden(mock_simulation, solution):
ports = [EdgePort(0, [0, 0, 1, 1])]
mock_simulation.ports = ports
validator = ValidateSim()
with pytest.raises(ValidateSimError) as expected_error:
validator.has_edgeport_when_forbidden(mock_simulation, solution)
assert expected_error.value.validation_type == "has_edgeport_when_forbidden"


@pytest.mark.parametrize(
"solution", [AnsysHfssSolution(), AnsysEigenmodeSolution(), AnsysCurrentSolution(), AnsysVoltageSolution()]
)
def test_flux_integration_layer_exists_if_needed_passes_if_no_layers(mock_simulation, solution):
validator = ValidateSim()
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)


@pytest.mark.parametrize(
"solution", [AnsysHfssSolution(), AnsysEigenmodeSolution(), AnsysCurrentSolution(), AnsysVoltageSolution()]
)
def test_flux_integration_layer_exists_if_needed_passes_if_has_needed_layer(mock_simulation, solution):
validator = ValidateSim()
mock_simulation.layers["flux_integration_layer"] = {"z": 0.0, "thickness": 0.0, "material": "non-pec"}
solution.integrate_magnetic_flux = True
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)


@pytest.mark.parametrize(
"solution", [AnsysHfssSolution(), AnsysEigenmodeSolution(), AnsysCurrentSolution(), AnsysVoltageSolution()]
)
def test_flux_integration_layer_exists_if_needed_passes_if_has_needed_layer_and_others(mock_simulation, solution):
validator = ValidateSim()
mock_simulation.layers["flux_integration_layer"] = {"z": 0.0, "thickness": 0.0, "material": "non-pec"}
mock_simulation.layers["thick_non_pec"] = {"z": 0.0, "thickness": 0.1, "material": "non-pec"}
mock_simulation.layers["sheet_pec"] = {"z": 0.0, "thickness": 0.0, "material": "pec"}
solution.integrate_magnetic_flux = True
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)


@pytest.mark.parametrize(
"solution", [AnsysHfssSolution(), AnsysEigenmodeSolution(), AnsysCurrentSolution(), AnsysVoltageSolution()]
)
def test_flux_integration_layer_passes_if_not_integrating_flux(mock_simulation, solution):
validator = ValidateSim()
solution.integrate_magnetic_flux = False
mock_simulation.layers["thick_non_pec"] = {"z": 0.0, "thickness": 0.1, "material": "non-pec"}
mock_simulation.layers["sheet_pec"] = {"z": 0.0, "thickness": 0.0, "material": "pec"}
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)


@pytest.mark.parametrize(
"solution", [AnsysHfssSolution(), AnsysEigenmodeSolution(), AnsysCurrentSolution(), AnsysVoltageSolution()]
)
def test_raise_flux_integration_error_if_has_no_needed_layer(mock_simulation, solution):
validator = ValidateSim()
solution.integrate_magnetic_flux = True
with pytest.raises(ValidateSimError) as expected_error:
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)
assert expected_error.value.validation_type == "flux_integration_layer_exists_if_needed"


@pytest.mark.parametrize(
"solution", [AnsysHfssSolution(), AnsysEigenmodeSolution(), AnsysCurrentSolution(), AnsysVoltageSolution()]
)
def test_raise_flux_integration_error_if_has_no_needed_layer_and_others(mock_simulation, solution):
validator = ValidateSim()
solution.integrate_magnetic_flux = True
mock_simulation.layers["thick_non_pec"] = {"z": 0.0, "thickness": 0.1, "material": "non-pec"}
mock_simulation.layers["sheet_pec"] = {"z": 0.0, "thickness": 0.0, "material": "pec"}
with pytest.raises(ValidateSimError) as expected_error:
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)
assert expected_error.value.validation_type == "flux_integration_layer_exists_if_needed"
Loading