Skip to content

Commit

Permalink
Implement simulation object validation checks
Browse files Browse the repository at this point in the history
  • Loading branch information
rmoretti9 authored and qpavsmi committed Jun 11, 2024
1 parent 94ca67e commit 93c4176
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 4 deletions.
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"

0 comments on commit 93c4176

Please sign in to comment.