Skip to content

Commit

Permalink
Refactor sim validation and add unit test example
Browse files Browse the repository at this point in the history
  • Loading branch information
rmoretti9 committed May 31, 2024
1 parent 53a0bab commit 9a9f98f
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,13 @@ def export_ansys(
Returns:
Path to exported bat file.
"""
validate_simulation(simulations)
write_commit_reference_file(path)
copy_content_into_directory(ANSYS_SCRIPT_PATHS, path, script_folder)
json_filenames = []
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 @@ -571,7 +571,7 @@ def export_elmer(
Returns:
Path to exported script file.
"""
validate_simulation(simulations)

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 @@ -582,7 +582,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,7 +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):
"""Return parameters of Simulation and Solution in a combined dictionary.
Expand Down Expand Up @@ -160,98 +160,16 @@ def cross_combine(simulations, solutions):
)


def validate_simulation(simulations):
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 or a list of Simulation objects.
Raises:
Errors when validation criteria are not met.
"""
if not isinstance(simulations, list):
simulations = [simulations]
for simulation in simulations:
port_and_simulation_type_compatibility(simulation)
flux_integration_layer_exists_if_needed(simulation)


def port_and_simulation_type_compatibility(simulation):
"""Validation check related to the presence of ports and the solution type.
Args:
simulation: A Simulation object.
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(simulation)
sim_name = simulation[0].name

# Ensure that a simulation object has ports when the solution type require it.
if not port_names and solution_type in [
"AnsysHfssSolution",
"AnsysVoltageSolution",
"AnsysCurrentSolution",
"ElmerVectorHelmholtzSolution",
"ElmerCapacitanceSolution",
]:
raise ValueError(f"Simulation '{sim_name}' has no ports assigned. This is incompatible with {solution_type}")

# Ensure that if at least one "EdgePort" is present, some solution types can't be chosen.
if "EdgePort" in port_names and solution_type in [
"AnsysEigenmodeSolution",
"AnsysVoltageSolution",
"AnsysCurrentSolution",
]:
raise ValueError(
f"Simulation '{sim_name}' has at least one 'EdgePort'. This is incompatible with {solution_type}"
)


def flux_integration_layer_exists_if_needed(simulation):
"""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[0].name
has_integrate_flux = hasattr(simulation[1], "integrate_magnetic_flux")
integrate_flux = simulation[1].integrate_magnetic_flux if has_integrate_flux else False

# Ensure that a layer with thickness == 0 and a material != "per" exsists in the setup when "integrate_magnetic_flux" is True.
if integrate_flux:
layers = simulation[0].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 ValueError(
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[0].ports
port_names = []
for port in port_list:
port_names.append(type(port).__name__)
return port_names


def get_solution_type(simulation):
"""Helper function that returns the solution type name of a Simulation object.
Args:
simulation: A Simulation object.
Returns:
A string containing the name of the solution type of simulation.
"""
return type(simulation[1]).__name__
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,120 @@
# 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
41 changes: 41 additions & 0 deletions tests/simulations/simulation_validate/test_simulation_validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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
from kqcircuits.util.export_helper import (
get_active_or_new_layout,
)
from kqcircuits.simulations.export.ansys.ansys_solution import AnsysCurrentSolution, AnsysHfssSolution, AnsysQ3dSolution, AnsysEigenmodeSolution
from kqcircuits.simulations.export.elmer.elmer_solution import ElmerVectorHelmholtzSolution, ElmerCapacitanceSolution, ElmerCrossSectionSolution

class MockSimulation(Simulation):
def build(self):
pass
self.ports.append(InternalPort(0, *self.etched_line(pya.DPoint(0, 0), pya.DPoint(1, 1))))
layout = get_active_or_new_layout()
simulation = MockSimulation(layout)

def test_has_no_ports_when_required():
simulation.ports = [InternalPort(0, *simulation.etched_line(pya.DPoint(0, 0), pya.DPoint(1, 1)))]
simulation.name = "test_sim"
solution = AnsysCurrentSolution()
validator = ValidateSim()
validator.has_no_ports_when_required(simulation, solution)

0 comments on commit 9a9f98f

Please sign in to comment.