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 committed Jun 10, 2024
1 parent 609d0ab commit c3fe9b2
Show file tree
Hide file tree
Showing 7 changed files with 266 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,123 @@
# 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).


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

def __init__(self):
pass

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)
solution_type = get_solution_type(solution)
sim_name = simulation.name
if not port_names and solution_type in [
"AnsysHfssSolution",
"AnsysVoltageSolution",
"AnsysCurrentSolution",
"ElmerVectorHelmholtzSolution",
"ElmerCapacitanceSolution",
]:
raise ValidateSimError(
f"Simulation '{sim_name}' has no ports assigned. This is incompatible with {solution_type}"
)

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)
solution_type = get_solution_type(solution)
sim_name = simulation.name
if "EdgePort" in port_names and solution_type in [
"AnsysEigenmodeSolution",
"AnsysVoltageSolution",
"AnsysCurrentSolution",
]:
raise ValidateSimError(
f"Simulation '{sim_name}' has at least one 'EdgePort'. This is incompatible with {solution_type}"
)

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" exsists in the setup when "integrate_magnetic_flux" is True.
if integrate_flux:
layers = simulation.layers
has_flux_integration_layer = False
for layer_name in layers:
if "thickness" in layers[layer_name] and "material" in layers[layer_name]:
if layers[layer_name]["thickness"] == 0 and layers[layer_name]["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."
)


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


def get_solution_type(solution):
"""Helper function that returns the solution type name of a Solution object.
Args:
solution: A Solution object.
Returns:
A string containing the name of the solution type of simulation.
"""
return type(solution).__name__


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

def __init__(self, message, errors=None):
super().__init__(message)
self.errors = errors
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())
117 changes: 117 additions & 0 deletions tests/simulations/simulation_validate/test_simulation_validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 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.simulation_validate import ValidateSim
from kqcircuits.simulations.simulation import Simulation
from kqcircuits.pya_resolver import pya
from kqcircuits.simulations.port import InternalPort, EdgePort
from kqcircuits.util.export_helper import (
get_active_or_new_layout,
)
from kqcircuits.simulations.export.ansys.ansys_solution import (
AnsysCurrentSolution,
AnsysHfssSolution,
AnsysQ3dSolution,
AnsysEigenmodeSolution,
AnsysVoltageSolution,
)
from kqcircuits.simulations.export.elmer.elmer_solution import (
ElmerVectorHelmholtzSolution,
ElmerCapacitanceSolution,
ElmerCrossSectionSolution,
)
from kqcircuits.simulations.export.simulation_validate import ValidateSimError
import pytest


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


def test_has_no_ports_when_required(mock_simulation):
ports = [InternalPort(0, [0, 0, 1, 1])]
mock_simulation.ports = ports
validator = ValidateSim()
sol_types = [
AnsysCurrentSolution(),
AnsysHfssSolution(),
AnsysVoltageSolution(),
ElmerVectorHelmholtzSolution(),
ElmerCapacitanceSolution(),
]
for solution in sol_types:
validator.has_no_ports_when_required(mock_simulation, solution)


def test_raise_no_port_error_when_required(mock_simulation):
validator = ValidateSim()
sol_types = [
AnsysCurrentSolution(),
AnsysHfssSolution(),
AnsysVoltageSolution(),
ElmerVectorHelmholtzSolution(),
ElmerCapacitanceSolution(),
]
for solution in sol_types:
with pytest.raises(ValidateSimError):
validator.has_no_ports_when_required(mock_simulation, solution)


def test_has_edgeport_when_forbidden(mock_simulation):
ports = [InternalPort(0, [0, 0, 1, 1])]
mock_simulation.ports = ports
sol_types = [AnsysEigenmodeSolution(), AnsysVoltageSolution(), AnsysCurrentSolution()]
validator = ValidateSim()
for solution in sol_types:
validator.has_edgeport_when_forbidden(mock_simulation, solution)


def test_raise_edgeport_error_when_forbidden(mock_simulation):
ports = [EdgePort(0, [0, 0, 1, 1])]
mock_simulation.ports = ports
sol_types = [AnsysEigenmodeSolution(), AnsysVoltageSolution(), AnsysCurrentSolution()]
validator = ValidateSim()
for solution in sol_types:
with pytest.raises(ValidateSimError):
validator.has_edgeport_when_forbidden(mock_simulation, solution)


def test_flux_integration_layer_exists_if_needed(mock_simulation):
validator = ValidateSim()
solution = AnsysVoltageSolution()
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)
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)


def test_raise_flux_integration_error(mock_simulation):
validator = ValidateSim()
solution = AnsysVoltageSolution()
solution.integrate_magnetic_flux = True
with pytest.raises(ValidateSimError):
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)
mock_simulation.layers["flux_integration_layer"] = {"z": 0.0, "thickness": 0.1, "material": "non-pec"}
with pytest.raises(ValidateSimError):
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)
mock_simulation.layers["flux_integration_layer"] = {"z": 0.0, "thickness": 0.0, "material": "pec"}
with pytest.raises(ValidateSimError):
validator.flux_integration_layer_exists_if_needed(mock_simulation, solution)

0 comments on commit c3fe9b2

Please sign in to comment.