From 3124abb4e2670c6ed926df506fa386732585810d Mon Sep 17 00:00:00 2001 From: rmoretti9 Date: Thu, 30 May 2024 13:51:58 +0200 Subject: [PATCH] Implement simulation object validation checks --- .../simulations/export/ansys/ansys_export.py | 2 + .../simulations/export/elmer/elmer_export.py | 4 +- .../simulations/export/simulation_export.py | 16 ++ .../simulations/export/simulation_validate.py | 119 +++++++++++++ tests/conftest.py | 5 +- .../ansys_export/test_ansys_export_write.py | 3 +- .../test_simulation_validate.py | 157 ++++++++++++++++++ 7 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 klayout_package/python/kqcircuits/simulations/export/simulation_validate.py create mode 100644 tests/simulations/simulation_validate/test_simulation_validate.py diff --git a/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py b/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py index 2c6fb1f1c..9fbf760e8 100644 --- a/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py +++ b/klayout_package/python/kqcircuits/simulations/export/ansys/ansys_export.py @@ -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 @@ -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 diff --git a/klayout_package/python/kqcircuits/simulations/export/elmer/elmer_export.py b/klayout_package/python/kqcircuits/simulations/export/elmer/elmer_export.py index 21a147af3..7450d9b6a 100644 --- a/klayout_package/python/kqcircuits/simulations/export/elmer/elmer_export.py +++ b/klayout_package/python/kqcircuits/simulations/export/elmer/elmer_export.py @@ -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 @@ -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) @@ -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 diff --git a/klayout_package/python/kqcircuits/simulations/export/simulation_export.py b/klayout_package/python/kqcircuits/simulations/export/simulation_export.py index ac33bbbc6..1f0c5cbc5 100644 --- a/klayout_package/python/kqcircuits/simulations/export/simulation_export.py +++ b/klayout_package/python/kqcircuits/simulations/export/simulation_export.py @@ -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): @@ -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) diff --git a/klayout_package/python/kqcircuits/simulations/export/simulation_validate.py b/klayout_package/python/kqcircuits/simulations/export/simulation_validate.py new file mode 100644 index 000000000..c59aa0ad8 --- /dev/null +++ b/klayout_package/python/kqcircuits/simulations/export/simulation_validate.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index edd07ee32..4637baf48 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/simulations/ansys/ansys_export/test_ansys_export_write.py b/tests/simulations/ansys/ansys_export/test_ansys_export_write.py index 2e01a620c..176dad7c5 100644 --- a/tests/simulations/ansys/ansys_export/test_ansys_export_write.py +++ b/tests/simulations/ansys/ansys_export/test_ansys_export_write.py @@ -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()) diff --git a/tests/simulations/simulation_validate/test_simulation_validate.py b/tests/simulations/simulation_validate/test_simulation_validate.py new file mode 100644 index 000000000..e661c449d --- /dev/null +++ b/tests/simulations/simulation_validate/test_simulation_validate.py @@ -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"