feat: integrate AMESim media models and editor UI
This commit is contained in:
1 parent
3e4f466114
commit
e7177ab03e
181 files changed
+7042
-25270
No files matched your search
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.systems import test_mql
|
||||
from app.simulation.examples.test_mql import system as test_mql
|
||||
|
||||
|
||||
MATRIX_PATH = Path(__file__).resolve().parents[1] / "docs" / "amesim-component-migration-matrix.md"
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_BUILTIN_AIR_GAS_INDEX,
|
||||
AMESIM_DEFAULT_GAS_INDEX,
|
||||
AmesimGasDefinition,
|
||||
AmesimGasRegistry,
|
||||
default_amesim_gas_registry,
|
||||
normalize_amesim_defined_gas_index,
|
||||
normalize_amesim_gas_index,
|
||||
)
|
||||
from app.simulation.components.amesim.media import AmesimIdealAirMedium
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
GAS_INDEXED_MODEL_TYPES = (
|
||||
"amesim_pnch023",
|
||||
"amesim_pnch012",
|
||||
"amesim_pnor001",
|
||||
"amesim_pnvo001_fixed",
|
||||
"amesim_pnvo001",
|
||||
"amesim_pnl00r",
|
||||
"amesim_pnl0001",
|
||||
"amesim_pnl0002",
|
||||
"amesim_pnl0003",
|
||||
)
|
||||
|
||||
|
||||
def pneumatic_pipe_node(
|
||||
component_id: str,
|
||||
*,
|
||||
gi: float,
|
||||
) -> dict[str, object]:
|
||||
return component_node(
|
||||
component_id,
|
||||
"amesim_pnl00r",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": gi},
|
||||
)
|
||||
|
||||
|
||||
def pneumatic_chamber_node(
|
||||
component_id: str,
|
||||
*,
|
||||
gi: float | None = None,
|
||||
volume: float = 0.01,
|
||||
pressure: float = 200000.0,
|
||||
temperature: float = 300.0,
|
||||
) -> dict[str, object]:
|
||||
parameters = {
|
||||
"cvol": volume,
|
||||
"kth": 0.0,
|
||||
"sth": 0.1,
|
||||
"extemp": 293.15,
|
||||
"p0": pressure,
|
||||
"T0": temperature,
|
||||
}
|
||||
if gi is not None:
|
||||
parameters["gi"] = gi
|
||||
return component_node(
|
||||
component_id,
|
||||
"amesim_pnch023",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
parameters,
|
||||
)
|
||||
|
||||
|
||||
def ideal_air_medium_node(
|
||||
component_id: str,
|
||||
*,
|
||||
gi: float,
|
||||
property_model: float | None = None,
|
||||
) -> dict[str, object]:
|
||||
parameters = {"gi": gi}
|
||||
if property_model is not None:
|
||||
parameters["property_model"] = property_model
|
||||
return component_node(
|
||||
component_id,
|
||||
"amesim_ideal_air_medium",
|
||||
[],
|
||||
parameters,
|
||||
)
|
||||
|
||||
|
||||
def two_slot_registry() -> tuple[AmesimGasRegistry, IdealGasMedium, IdealGasMedium]:
|
||||
future_helium = IdealGasMedium(
|
||||
name="FutureHeliumStub",
|
||||
R_gas=2077.26439404998,
|
||||
cp_ref=5193.0,
|
||||
viscosity_ref=1.96e-5,
|
||||
sutherland_constant=79.4,
|
||||
)
|
||||
registry = AmesimGasRegistry(
|
||||
(
|
||||
AmesimGasDefinition(
|
||||
gi=2,
|
||||
label="Future helium",
|
||||
medium=future_helium,
|
||||
fluid_type=12,
|
||||
eos_type=6,
|
||||
),
|
||||
)
|
||||
)
|
||||
air = registry.resolve(AMESIM_BUILTIN_AIR_GAS_INDEX)
|
||||
return registry, air, future_helium
|
||||
|
||||
|
||||
class AmesimGasRegistryTests(unittest.TestCase):
|
||||
def test_all_gas_sensitive_models_expose_the_same_gi_contract(self) -> None:
|
||||
medium = IdealGasMedium()
|
||||
|
||||
for model_type in GAS_INDEXED_MODEL_TYPES:
|
||||
with self.subTest(model_type=model_type):
|
||||
spec = COMPONENT_MODEL_REGISTRY[model_type]
|
||||
parameter = spec.parameter_by_name["gi"]
|
||||
|
||||
self.assertIs(spec.parameters[0], parameter)
|
||||
self.assertEqual(parameter.default, 0.0)
|
||||
self.assertEqual(parameter.minimum, 0.0)
|
||||
self.assertEqual(parameter.maximum, 99.0)
|
||||
self.assertEqual(parameter.editor, "amesimGasReference")
|
||||
component = spec.create(f"{model_type}_1", medium, {})
|
||||
self.assertEqual(component.gi, AMESIM_DEFAULT_GAS_INDEX)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
|
||||
spec.create(f"{model_type}_fractional", medium, {"gi": 1.5})
|
||||
|
||||
def test_default_slot_resolves_to_air(self) -> None:
|
||||
registry = default_amesim_gas_registry()
|
||||
|
||||
medium = registry.resolve(AMESIM_DEFAULT_GAS_INDEX)
|
||||
|
||||
self.assertIsInstance(medium, AmesimIdealAirMedium)
|
||||
self.assertEqual(medium.name, "AMESimAirIdealGas")
|
||||
self.assertEqual(medium.SUBSTANCE_ID, "air")
|
||||
self.assertEqual(medium.PROPERTY_METHOD_ID, "ideal_gas")
|
||||
self.assertEqual(medium.R_gas, 287.0)
|
||||
self.assertEqual(medium.cp_ref, 1005.0)
|
||||
self.assertEqual(medium.cv, 718.0)
|
||||
|
||||
def test_gi_normalization_rejects_fractional_and_out_of_range_values(self) -> None:
|
||||
self.assertEqual(normalize_amesim_gas_index(0.0), 0)
|
||||
self.assertEqual(normalize_amesim_gas_index(1.0), 1)
|
||||
with self.assertRaisesRegex(ValueError, "integer"):
|
||||
normalize_amesim_gas_index(1.5)
|
||||
with self.assertRaisesRegex(ValueError, "between 0 and 99"):
|
||||
normalize_amesim_gas_index(-1)
|
||||
with self.assertRaisesRegex(ValueError, "between 0 and 99"):
|
||||
normalize_amesim_gas_index(100)
|
||||
with self.assertRaisesRegex(ValueError, "reserved"):
|
||||
normalize_amesim_defined_gas_index(0)
|
||||
self.assertEqual(normalize_amesim_defined_gas_index(1), 1)
|
||||
|
||||
def test_unknown_slot_is_not_silently_replaced_with_air(self) -> None:
|
||||
registry = default_amesim_gas_registry()
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "gi=2.*not defined"):
|
||||
registry.resolve(2, component_name="pipe_1")
|
||||
|
||||
project = ReactFlowProjectPayload(
|
||||
name="undefined-gas",
|
||||
nodes=[pneumatic_pipe_node("pipe_1", gi=2.0)],
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "gi=2.*pipe_1.*not defined"):
|
||||
compile_reactflow_network(project)
|
||||
|
||||
def test_custom_registry_can_define_future_helium_slot(self) -> None:
|
||||
registry, air, future_helium = two_slot_registry()
|
||||
|
||||
self.assertIs(registry.resolve(0), air)
|
||||
self.assertIs(registry.resolve(2), future_helium)
|
||||
self.assertEqual(registry.definitions[2].fluid_type, 12)
|
||||
self.assertEqual(registry.definitions[2].eos_type, 6)
|
||||
|
||||
def test_builtin_air_slot_cannot_be_overridden(self) -> None:
|
||||
registry = default_amesim_gas_registry()
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "gi=0.*reserved"):
|
||||
registry.register(
|
||||
AmesimGasDefinition(
|
||||
gi=0,
|
||||
label="Invalid override",
|
||||
medium=IdealGasMedium(name="NotBuiltinAir"),
|
||||
)
|
||||
)
|
||||
self.assertEqual(registry.resolve(0).name, "AMESimAirIdealGas")
|
||||
|
||||
def test_air_medium_definition_is_a_zero_port_compile_time_component(self) -> None:
|
||||
spec = COMPONENT_MODEL_REGISTRY["amesim_ideal_air_medium"]
|
||||
|
||||
self.assertEqual(spec.display.category_id, "media")
|
||||
self.assertEqual(spec.display.role, "amesimGasMediumDefinition")
|
||||
self.assertEqual(spec.display.label, "空气介质定义")
|
||||
self.assertEqual(spec.ports, ())
|
||||
self.assertEqual(spec.parameters[0].minimum, 1.0)
|
||||
property_model = spec.parameter_by_name["property_model"]
|
||||
self.assertIs(spec.parameters[1], property_model)
|
||||
self.assertEqual(property_model.default, 0.0)
|
||||
self.assertEqual(property_model.editor, "amesimGasPropertyModel")
|
||||
self.assertEqual(
|
||||
[(option.value, option.label) for option in property_model.options],
|
||||
[(0, "理想气体")],
|
||||
)
|
||||
|
||||
project = ReactFlowProjectPayload(
|
||||
name="project-air-slot",
|
||||
nodes=[
|
||||
pneumatic_pipe_node("pipe_1", gi=1.0),
|
||||
ideal_air_medium_node("air_properties", gi=1.0),
|
||||
],
|
||||
)
|
||||
network = compile_reactflow_network(project)
|
||||
|
||||
self.assertNotIn("air_properties", network.components)
|
||||
self.assertEqual(
|
||||
network.components["pipe_1"].medium.name,
|
||||
"AMESimAirIdealGas",
|
||||
)
|
||||
|
||||
def test_air_definition_dispatches_the_selected_property_model(self) -> None:
|
||||
spec = COMPONENT_MODEL_REGISTRY["amesim_ideal_air_medium"]
|
||||
|
||||
definition = spec.create(
|
||||
"air_properties",
|
||||
IdealGasMedium(),
|
||||
{"gi": 3.0, "property_model": 0.0},
|
||||
)
|
||||
gas_definition = definition.gas_definition()
|
||||
|
||||
self.assertEqual(
|
||||
definition.parameter_values,
|
||||
{"gi": 3.0, "property_model": 0.0},
|
||||
)
|
||||
self.assertEqual(gas_definition.label, "空气(理想气体)")
|
||||
self.assertEqual(gas_definition.fluid_type, 2)
|
||||
self.assertEqual(gas_definition.eos_type, 1)
|
||||
self.assertIsInstance(gas_definition.medium, AmesimIdealAirMedium)
|
||||
self.assertEqual(
|
||||
gas_definition.medium.PROPERTY_METHOD_ID,
|
||||
"ideal_gas",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "property_model.*at most 0"):
|
||||
spec.create(
|
||||
"unsupported_air_properties",
|
||||
IdealGasMedium(),
|
||||
{"gi": 4.0, "property_model": 1.0},
|
||||
)
|
||||
|
||||
def test_project_medium_definition_may_appear_after_its_references(self) -> None:
|
||||
project = ReactFlowProjectPayload(
|
||||
name="two-phase-medium-resolution",
|
||||
nodes=[
|
||||
pneumatic_chamber_node("chamber_1", gi=7.0),
|
||||
ideal_air_medium_node("air_properties", gi=7.0),
|
||||
],
|
||||
)
|
||||
|
||||
network = compile_reactflow_network(project)
|
||||
|
||||
self.assertEqual(
|
||||
network.components["chamber_1"].medium.name,
|
||||
"AMESimAirIdealGas",
|
||||
)
|
||||
|
||||
def test_medium_definition_and_reference_survive_system_xml_round_trip(
|
||||
self,
|
||||
) -> None:
|
||||
original = ReactFlowProjectPayload(
|
||||
name="medium-xml-round-trip",
|
||||
nodes=[
|
||||
ideal_air_medium_node(
|
||||
"air_properties",
|
||||
gi=4.0,
|
||||
property_model=0.0,
|
||||
),
|
||||
pneumatic_pipe_node("pipe_1", gi=4.0),
|
||||
],
|
||||
)
|
||||
|
||||
xml_bytes = build_reactflow_system_xml(original)
|
||||
report = validate_system_xml_document(xml_bytes)
|
||||
|
||||
self.assertTrue(report.valid, report.issues)
|
||||
self.assertIsNotNone(report.document)
|
||||
assert report.document is not None
|
||||
self.assertEqual(report.document.medium_reference_version, "1")
|
||||
components = {
|
||||
component.id: component for component in report.document.components
|
||||
}
|
||||
self.assertEqual(
|
||||
components["air_properties"].model_type,
|
||||
"amesim_ideal_air_medium",
|
||||
)
|
||||
self.assertEqual(
|
||||
{item.name: item.value for item in components["air_properties"].parameters},
|
||||
{"gi": 4.0, "property_model": 0.0},
|
||||
)
|
||||
self.assertEqual(
|
||||
{item.name: item.value for item in components["pipe_1"].parameters}[
|
||||
"gi"
|
||||
],
|
||||
4.0,
|
||||
)
|
||||
|
||||
parsed_project = ReactFlowProjectPayload(
|
||||
**report.document.as_project_data()
|
||||
)
|
||||
network = compile_reactflow_network(parsed_project)
|
||||
|
||||
self.assertNotIn("air_properties", network.components)
|
||||
self.assertIsInstance(
|
||||
network.components["pipe_1"].medium,
|
||||
AmesimIdealAirMedium,
|
||||
)
|
||||
|
||||
def test_duplicate_project_medium_indices_are_rejected(self) -> None:
|
||||
project = ReactFlowProjectPayload(
|
||||
name="duplicate-medium-index",
|
||||
nodes=[
|
||||
ideal_air_medium_node("air_properties_1", gi=1.0),
|
||||
ideal_air_medium_node("air_properties_2", gi=1.0),
|
||||
],
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"air_properties_2.*gi=1.*already defined",
|
||||
):
|
||||
compile_reactflow_network(project)
|
||||
|
||||
def test_medium_definition_cannot_claim_builtin_slot_zero(self) -> None:
|
||||
project = ReactFlowProjectPayload(
|
||||
name="reserved-medium-index",
|
||||
nodes=[ideal_air_medium_node("air_properties", gi=0.0)],
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "gi.*at least 1"):
|
||||
compile_reactflow_network(project)
|
||||
|
||||
def test_default_compile_uses_air_before_state_initialization(self) -> None:
|
||||
project = ReactFlowProjectPayload(
|
||||
name="default-air",
|
||||
nodes=[pneumatic_chamber_node("chamber_1")],
|
||||
)
|
||||
|
||||
network = compile_reactflow_network(project)
|
||||
chamber = network.components["chamber_1"]
|
||||
|
||||
self.assertEqual(chamber.medium.name, "AMESimAirIdealGas")
|
||||
self.assertAlmostEqual(
|
||||
chamber.state.m,
|
||||
chamber.medium.density(200000.0, 300.0) * 0.01,
|
||||
)
|
||||
|
||||
def test_gi_is_resolved_before_dynamic_state_initialization(self) -> None:
|
||||
registry, _, future_helium = two_slot_registry()
|
||||
project = ReactFlowProjectPayload(
|
||||
name="selected-medium-initialization",
|
||||
nodes=[pneumatic_chamber_node("chamber_1", gi=2.0)],
|
||||
)
|
||||
|
||||
network = compile_reactflow_network(
|
||||
project,
|
||||
amesim_gas_registry=registry,
|
||||
)
|
||||
chamber = network.components["chamber_1"]
|
||||
|
||||
self.assertIs(chamber.medium, future_helium)
|
||||
self.assertAlmostEqual(
|
||||
chamber.state.m,
|
||||
future_helium.density(200000.0, 300.0) * 0.01,
|
||||
)
|
||||
self.assertNotAlmostEqual(
|
||||
chamber.state.m,
|
||||
IdealGasMedium().density(200000.0, 300.0) * 0.01,
|
||||
)
|
||||
|
||||
def test_selected_medium_is_injected_into_the_whole_pneumatic_circuit(
|
||||
self,
|
||||
) -> None:
|
||||
registry, _, future_helium = two_slot_registry()
|
||||
project = ReactFlowProjectPayload(
|
||||
name="future-helium-circuit",
|
||||
nodes=[
|
||||
pneumatic_pipe_node("pnl_1", gi=2.0),
|
||||
component_node(
|
||||
"tank_1",
|
||||
"tank",
|
||||
[physical_port("port_a", "inlet", "left")],
|
||||
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
physical_edge("edge-1", "pnl_1", "port_2", "tank_1", "port_a"),
|
||||
],
|
||||
)
|
||||
|
||||
network = compile_reactflow_network(
|
||||
project,
|
||||
amesim_gas_registry=registry,
|
||||
)
|
||||
|
||||
self.assertIs(network.components["pnl_1"].medium, future_helium)
|
||||
self.assertIs(network.components["tank_1"].medium, future_helium)
|
||||
|
||||
def test_connected_circuit_rejects_conflicting_gas_indices(self) -> None:
|
||||
registry, _, _ = two_slot_registry()
|
||||
project = ReactFlowProjectPayload(
|
||||
name="conflicting-gases",
|
||||
nodes=[
|
||||
pneumatic_pipe_node("air_pipe", gi=0.0),
|
||||
pneumatic_pipe_node("helium_pipe", gi=2.0),
|
||||
],
|
||||
edges=[
|
||||
physical_edge(
|
||||
"edge-1",
|
||||
"air_pipe",
|
||||
"port_2",
|
||||
"helium_pipe",
|
||||
"port_1",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"conflicting AMESim gas definitions",
|
||||
):
|
||||
compile_reactflow_network(
|
||||
project,
|
||||
amesim_gas_registry=registry,
|
||||
)
|
||||
|
||||
def test_disconnected_circuits_may_use_different_gas_indices(self) -> None:
|
||||
registry, air, future_helium = two_slot_registry()
|
||||
project = ReactFlowProjectPayload(
|
||||
name="separate-gases",
|
||||
nodes=[
|
||||
pneumatic_pipe_node("air_pipe", gi=0.0),
|
||||
pneumatic_pipe_node("helium_pipe", gi=2.0),
|
||||
],
|
||||
)
|
||||
|
||||
network = compile_reactflow_network(
|
||||
project,
|
||||
amesim_gas_registry=registry,
|
||||
)
|
||||
|
||||
self.assertIs(network.components["air_pipe"].medium, air)
|
||||
self.assertIs(network.components["helium_pipe"].medium, future_helium)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,154 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import ReactFlowProjectPayload, compile_reactflow_network
|
||||
from app.simulation.components.amesim.library import LIBRARY
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AMESIM_AIR_PROPERTY_MODELS,
|
||||
AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL,
|
||||
AMESIM_HELIUM_PROPERTY_MODELS,
|
||||
AmesimHeliumPengRobinsonMedium,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.peng_robinson import HELIUM_PR
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
from tests.test_generic_system_xml_simulation import component_node
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
|
||||
def test_uses_shared_peng_robinson_eos_and_committed_caloric_constants(
|
||||
self,
|
||||
) -> None:
|
||||
medium = AmesimHeliumPengRobinsonMedium()
|
||||
pressure = 15.3e6
|
||||
temperature = 293.15
|
||||
|
||||
density = medium.density(pressure, temperature)
|
||||
volume = 0.02
|
||||
|
||||
self.assertIs(medium.fluid, HELIUM_PR)
|
||||
self.assertEqual(medium.SUBSTANCE_ID, "helium")
|
||||
self.assertEqual(medium.PROPERTY_METHOD_ID, "peng_robinson")
|
||||
self.assertAlmostEqual(medium.R_gas, HELIUM_PR.specific_gas_constant)
|
||||
self.assertEqual(medium.cp_ref, 5193.0)
|
||||
self.assertEqual(medium.cv, 3116.0)
|
||||
self.assertEqual(medium.cp_at_temperature(400.0), 5193.0)
|
||||
self.assertEqual(medium.cv_at_temperature(400.0), 3116.0)
|
||||
self.assertAlmostEqual(medium.gamma, 5193.0 / 3116.0)
|
||||
self.assertAlmostEqual(density, HELIUM_PR.density(pressure, temperature))
|
||||
self.assertAlmostEqual(
|
||||
medium.pressure(density * volume, temperature, volume),
|
||||
pressure,
|
||||
delta=pressure * 1.0e-12,
|
||||
)
|
||||
|
||||
def test_constant_caloric_model_and_sutherland_viscosity_complete_contract(
|
||||
self,
|
||||
) -> None:
|
||||
medium = AmesimHeliumPengRobinsonMedium()
|
||||
temperature = 340.0
|
||||
|
||||
internal_energy = medium.specific_internal_energy(temperature)
|
||||
enthalpy = medium.specific_enthalpy(temperature)
|
||||
|
||||
self.assertEqual(internal_energy, 3116.0 * temperature)
|
||||
self.assertEqual(enthalpy, 5193.0 * temperature)
|
||||
self.assertEqual(
|
||||
medium.temperature_from_internal_energy(internal_energy),
|
||||
temperature,
|
||||
)
|
||||
self.assertEqual(
|
||||
medium.temperature_from_enthalpy(enthalpy),
|
||||
temperature,
|
||||
)
|
||||
self.assertAlmostEqual(medium.dynamic_viscosity(293.15), 1.96e-5)
|
||||
self.assertEqual(medium.sutherland_constant, 79.4)
|
||||
|
||||
def test_definition_maps_local_selector_to_amesim_fluid_and_eos_codes(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(LIBRARY.version, "0.2.0")
|
||||
self.assertEqual(AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL, 0)
|
||||
self.assertEqual(AMESIM_HELIUM_PROPERTY_MODELS[0].value, 0)
|
||||
self.assertEqual(AMESIM_HELIUM_PROPERTY_MODELS[0].eos_type, 6)
|
||||
self.assertEqual(AMESIM_AIR_PROPERTY_MODELS[0].eos_type, 1)
|
||||
|
||||
spec = COMPONENT_MODEL_REGISTRY["amesim_helium_medium"]
|
||||
property_model = spec.parameter_by_name["property_model"]
|
||||
|
||||
self.assertEqual(spec.display.category_id, "media")
|
||||
self.assertEqual(spec.display.role, "amesimGasMediumDefinition")
|
||||
self.assertEqual(spec.display.label, "氦气介质定义")
|
||||
self.assertEqual(spec.model_version, "0.1.0")
|
||||
self.assertEqual(spec.ports, ())
|
||||
self.assertEqual(property_model.default, 0.0)
|
||||
self.assertEqual(property_model.editor, "amesimGasPropertyModel")
|
||||
self.assertEqual(
|
||||
[(option.value, option.label) for option in property_model.options],
|
||||
[(0, "Peng–Robinson")],
|
||||
)
|
||||
|
||||
component = spec.create(
|
||||
"helium_properties",
|
||||
IdealGasMedium(),
|
||||
{"gi": 2.0, "property_model": 0.0},
|
||||
)
|
||||
definition = component.gas_definition()
|
||||
|
||||
self.assertEqual(component.parameter_values, {"gi": 2.0, "property_model": 0.0})
|
||||
self.assertEqual(definition.label, "氦气(Peng–Robinson)")
|
||||
self.assertEqual(definition.fluid_type, 12)
|
||||
self.assertEqual(definition.eos_type, 6)
|
||||
self.assertIsInstance(definition.medium, AmesimHeliumPengRobinsonMedium)
|
||||
|
||||
def test_project_compile_injects_helium_before_dynamic_state_initialization(
|
||||
self,
|
||||
) -> None:
|
||||
pressure = 13_839_965.0
|
||||
temperature = 287.7322
|
||||
volume = 0.01
|
||||
project = ReactFlowProjectPayload(
|
||||
name="helium-peng-robinson-compile",
|
||||
nodes=[
|
||||
component_node(
|
||||
"helium_properties",
|
||||
"amesim_helium_medium",
|
||||
[],
|
||||
{"gi": 2.0, "property_model": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"chamber_1",
|
||||
"amesim_pnch023",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"gi": 2.0,
|
||||
"cvol": volume,
|
||||
"kth": 0.0,
|
||||
"sth": 0.1,
|
||||
"extemp": 293.15,
|
||||
"p0": pressure,
|
||||
"T0": temperature,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
network = compile_reactflow_network(project)
|
||||
chamber = network.components["chamber_1"]
|
||||
|
||||
self.assertNotIn("helium_properties", network.components)
|
||||
self.assertIsInstance(chamber.medium, AmesimHeliumPengRobinsonMedium)
|
||||
self.assertAlmostEqual(
|
||||
chamber.state.m,
|
||||
HELIUM_PR.density(pressure, temperature) * volume,
|
||||
delta=1.0e-12,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.components.amesim_mechanical import (
|
||||
from app.simulation.examples.test_mql.primitives.mechanical import (
|
||||
AmesimElasticEndstop,
|
||||
AmesimMassFrictionEndstops,
|
||||
AmesimPistonGeometry,
|
||||
@@ -11,7 +11,7 @@ from PythonModels.components.amesim_mechanical import (
|
||||
m_to_mm,
|
||||
mm_to_m,
|
||||
)
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -40,7 +40,7 @@ def amesim_pnch012_project() -> ReactFlowProjectPayload:
|
||||
"kth": 0.0,
|
||||
"sth": 0.7,
|
||||
"extemp": 300.0,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"p0": 200000.0,
|
||||
"T0": 300.0,
|
||||
"vol1": 0.001,
|
||||
|
||||
@@ -25,7 +25,7 @@ class AmesimPnch023ComponentTests(unittest.TestCase):
|
||||
{"cvol", "kth", "sth", "extemp", "gi", "p0", "T0"},
|
||||
)
|
||||
self.assertEqual(chamber.parameter_values["cvol"], 0.057)
|
||||
self.assertEqual(chamber.gi, 1)
|
||||
self.assertEqual(chamber.gi, 0)
|
||||
|
||||
def test_rejects_fractional_gas_index(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
|
||||
|
||||
@@ -44,7 +44,7 @@ def amesim_pnch023_project() -> ReactFlowProjectPayload:
|
||||
"kth": 0.0,
|
||||
"sth": 0.1,
|
||||
"extemp": 293.15,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"p0": 100000.0,
|
||||
"T0": 293.15,
|
||||
},
|
||||
@@ -56,7 +56,7 @@ def amesim_pnch023_project() -> ReactFlowProjectPayload:
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 1.0},
|
||||
{"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
@@ -91,6 +91,10 @@ class AmesimPnch023XmlTests(unittest.TestCase):
|
||||
|
||||
def test_pnch023_system_xml_validates_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(amesim_pnch023_project())
|
||||
xml_text = xml.decode("utf-8")
|
||||
self.assertNotIn("description", xml_text)
|
||||
self.assertIn('<Parameter name="cvol" value="0.057"', xml_text)
|
||||
self.assertIn('<Parameter name="gi" value="0.0"', xml_text)
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.components.amesim_pneumatic import (
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import (
|
||||
HELIUM_PNEUMATIC_GAS,
|
||||
AmesimPneumaticGas,
|
||||
AmesimPneumaticOrifice,
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.components.amesim_pneumatic_line import (
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic_lines import (
|
||||
AmesimPnl0002Pipe,
|
||||
AmesimPnl0003Pipe,
|
||||
AmesimPnl00rPipe,
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.components.amesim_pneumatic_line import AmesimPnl0001Pipe
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic_lines import AmesimPnl0001Pipe
|
||||
|
||||
|
||||
class AmesimPnl0001PipeTests(unittest.TestCase):
|
||||
|
||||
@@ -37,7 +37,7 @@ def amesim_pnl0001_project() -> ReactFlowProjectPayload:
|
||||
"k": 1.35,
|
||||
"kth": 0.0,
|
||||
"extemp": 300.0,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"mode": 2.0,
|
||||
"p0": 300000.0,
|
||||
"T0": 300.0,
|
||||
@@ -50,7 +50,7 @@ def amesim_pnl0001_project() -> ReactFlowProjectPayload:
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 1.0},
|
||||
{"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
|
||||
@@ -38,7 +38,7 @@ def _dynamic_pipe_params() -> dict[str, float]:
|
||||
"k": 1.35,
|
||||
"kth": 0.0,
|
||||
"extemp": 300.0,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"mode": 2.0,
|
||||
"p0": 300000.0,
|
||||
"T0": 300.0,
|
||||
@@ -53,7 +53,7 @@ def _resistance_node(name: str) -> object:
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 1.0},
|
||||
{"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 0.0},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
|
||||
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
|
||||
self.assertEqual(set(pipe.parameter_values), {"diam", "le", "rr", "gi"})
|
||||
self.assertEqual(pipe.parameter_values["diam"], 0.01)
|
||||
self.assertEqual(pipe.gi, 1)
|
||||
self.assertEqual(pipe.gi, 0)
|
||||
|
||||
def test_rejects_fractional_gas_index(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
|
||||
|
||||
@@ -30,7 +30,7 @@ def amesim_pnl00r_project() -> ReactFlowProjectPayload:
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": 1.0},
|
||||
{"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
|
||||
@@ -35,7 +35,7 @@ def amesim_pnor001_project() -> ReactFlowProjectPayload:
|
||||
"area": 5.0e-6,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"flowset": 1.0,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -29,7 +29,7 @@ def amesim_pnpl01_project() -> ReactFlowProjectPayload:
|
||||
"kth": 0.0,
|
||||
"sth": 0.1,
|
||||
"extemp": 293.15,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"p0": 100000.0,
|
||||
"T0": 293.15,
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ def amesim_pnvo001_fixed_project() -> ReactFlowProjectPayload:
|
||||
"area0": 5.0e-6,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"flowset": 1.0,
|
||||
"opening": 0.5,
|
||||
},
|
||||
@@ -47,7 +47,7 @@ def amesim_pnvo001_fixed_project() -> ReactFlowProjectPayload:
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 1.0},
|
||||
{"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
|
||||
@@ -63,7 +63,7 @@ def amesim_pnvo001_signal_project() -> ReactFlowProjectPayload:
|
||||
"area0": 5.0e-6,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"flowset": 1.0,
|
||||
"opening0": 0.0,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
from PythonModels.reporting.amesim_results import (
|
||||
from app.simulation.reporting.amesim_results import (
|
||||
_resolve_result_members,
|
||||
load_test_mql_amesim_results,
|
||||
)
|
||||
|
||||
@@ -71,7 +71,7 @@ def amesim_ud00_signal_project() -> ReactFlowProjectPayload:
|
||||
"area0": 5.0e-6,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"gi": 1.0,
|
||||
"gi": 0.0,
|
||||
"flowset": 1.0,
|
||||
"opening0": 0.0,
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.simulation.registry import (
|
||||
COMPONENT_MODEL_REGISTRY,
|
||||
validate_component_registries,
|
||||
)
|
||||
from app.simulation.core.metadata import ParameterDefinition
|
||||
|
||||
|
||||
class ComponentCatalogTests(unittest.TestCase):
|
||||
@@ -47,8 +48,58 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
}
|
||||
|
||||
self.assertEqual(library["label"], "AMESim 组件库")
|
||||
self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary", "signals", "mechanical"])
|
||||
self.assertEqual(set(components), {"amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
|
||||
self.assertEqual([category["id"] for category in library["categories"]], ["media", "storage", "flow", "junctions", "boundary", "signals", "mechanical"])
|
||||
self.assertEqual(set(components), {"amesim_ideal_air_medium", "amesim_helium_medium", "amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
|
||||
self.assertEqual(
|
||||
components["amesim_ideal_air_medium"]["role"],
|
||||
"amesimGasMediumDefinition",
|
||||
)
|
||||
self.assertEqual(
|
||||
components["amesim_ideal_air_medium"]["category"]["id"],
|
||||
"media",
|
||||
)
|
||||
self.assertEqual(
|
||||
components["amesim_ideal_air_medium"]["label"],
|
||||
"空气介质定义",
|
||||
)
|
||||
self.assertEqual(
|
||||
components["amesim_ideal_air_medium"]["modelVersion"],
|
||||
"0.2.0",
|
||||
)
|
||||
medium_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_ideal_air_medium"]["parameters"]
|
||||
}
|
||||
self.assertEqual(
|
||||
list(medium_parameters),
|
||||
["gi", "property_model"],
|
||||
)
|
||||
self.assertEqual(
|
||||
medium_parameters["property_model"]["editor"],
|
||||
"amesimGasPropertyModel",
|
||||
)
|
||||
self.assertEqual(
|
||||
medium_parameters["property_model"]["options"],
|
||||
[{"value": 0, "label": "理想气体"}],
|
||||
)
|
||||
helium_medium = components["amesim_helium_medium"]
|
||||
self.assertEqual(helium_medium["role"], "amesimGasMediumDefinition")
|
||||
self.assertEqual(helium_medium["category"]["id"], "media")
|
||||
self.assertEqual(helium_medium["label"], "氦气介质定义")
|
||||
self.assertEqual(helium_medium["modelVersion"], "0.1.0")
|
||||
helium_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in helium_medium["parameters"]
|
||||
}
|
||||
self.assertEqual(list(helium_parameters), ["gi", "property_model"])
|
||||
self.assertEqual(
|
||||
helium_parameters["property_model"]["editor"],
|
||||
"amesimGasPropertyModel",
|
||||
)
|
||||
self.assertEqual(
|
||||
helium_parameters["property_model"]["options"],
|
||||
[{"value": 0, "label": "Peng–Robinson"}],
|
||||
)
|
||||
self.assertEqual(
|
||||
[port["name"] for port in components["amesim_p4node2"]["ports"]],
|
||||
["port_1", "port_2", "port_3", "port_4"],
|
||||
@@ -174,6 +225,61 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertEqual(parameters["p0"]["unit"], "Pa")
|
||||
self.assertTrue(parameters["p0"]["minimumExclusive"])
|
||||
|
||||
def test_parameter_description_is_optional_interface_metadata(self) -> None:
|
||||
described = ParameterDefinition(
|
||||
"diam",
|
||||
0.01,
|
||||
label="管径",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="用于计算流通面积。",
|
||||
).as_interface_dict()
|
||||
undescribed = ParameterDefinition(
|
||||
"gain",
|
||||
1.0,
|
||||
label="增益",
|
||||
).as_interface_dict()
|
||||
|
||||
self.assertEqual(described["description"], "用于计算流通面积。")
|
||||
self.assertEqual(described["default"], 0.01)
|
||||
self.assertEqual(described["unit"], "m")
|
||||
self.assertEqual(described["minimum"], 0.0)
|
||||
self.assertTrue(described["minimumExclusive"])
|
||||
self.assertNotIn("description", undescribed)
|
||||
|
||||
def test_catalog_exposes_descriptions_for_core_amesim_pneumatic_parameters(
|
||||
self,
|
||||
) -> None:
|
||||
medium_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in self.components["amesim_helium_medium"]["parameters"]
|
||||
}
|
||||
chamber_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in self.components["amesim_pnch023"]["parameters"]
|
||||
}
|
||||
pipe_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in self.components["amesim_pnl00r"]["parameters"]
|
||||
}
|
||||
orifice_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in self.components["amesim_pnor001"]["parameters"]
|
||||
}
|
||||
|
||||
self.assertIn("自动分配", medium_parameters["gi"]["description"])
|
||||
self.assertIn(
|
||||
"Peng–Robinson",
|
||||
medium_parameters["property_model"]["description"],
|
||||
)
|
||||
self.assertIn("固定有效容积", chamber_parameters["cvol"]["description"])
|
||||
self.assertIn("Darcy", pipe_parameters["rr"]["description"])
|
||||
self.assertIn("有效孔口面积", orifice_parameters["area"]["description"])
|
||||
self.assertEqual(chamber_parameters["cvol"]["default"], 0.057)
|
||||
self.assertEqual(pipe_parameters["diam"]["unit"], "m")
|
||||
|
||||
def test_catalog_adds_port_layout_without_changing_physical_contract(self) -> None:
|
||||
tee_ports = {
|
||||
port["name"]: port for port in self.components["tee"]["ports"]
|
||||
@@ -200,6 +306,14 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertEqual(schema["properties"]["schemaVersion"]["const"], 1)
|
||||
self.assertIn("modelVersion", schema["$defs"]["component"]["required"])
|
||||
self.assertIn("version", schema["$defs"]["library"]["required"])
|
||||
parameter_schema = schema["$defs"]["parameter"]
|
||||
self.assertEqual(
|
||||
parameter_schema["properties"]["description"],
|
||||
{"type": "string", "minLength": 1},
|
||||
)
|
||||
self.assertNotIn("description", parameter_schema["required"])
|
||||
self.assertFalse(parameter_schema["additionalProperties"])
|
||||
self.assertNotIn("unknownField", parameter_schema["properties"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.simulation.components.experimental.library import LIBRARY
|
||||
from app.simulation.components.amesim.library import LIBRARY as AMESIM_LIBRARY
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, ParameterOption
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.registry import (
|
||||
@@ -30,6 +31,8 @@ class ComponentRegistryTests(unittest.TestCase):
|
||||
"pipe",
|
||||
"orifice",
|
||||
"tee",
|
||||
"amesim_ideal_air_medium",
|
||||
"amesim_helium_medium",
|
||||
"amesim_pnpl01",
|
||||
"amesim_step0",
|
||||
"amesim_ud00",
|
||||
@@ -62,6 +65,11 @@ class ComponentRegistryTests(unittest.TestCase):
|
||||
|
||||
self.assertIn("amesim", libraries)
|
||||
self.assertEqual(libraries["amesim"], AMESIM_LIBRARY)
|
||||
self.assertEqual(
|
||||
models["amesim_ideal_air_medium"].library.id,
|
||||
"amesim",
|
||||
)
|
||||
self.assertEqual(models["amesim_helium_medium"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnpl01"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_step0"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_ud00"].library.id, "amesim")
|
||||
@@ -134,6 +142,97 @@ class ComponentRegistryTests(unittest.TestCase):
|
||||
library=LIBRARY,
|
||||
)
|
||||
|
||||
def test_property_model_parameter_options_are_strictly_validated(self) -> None:
|
||||
def validate_parameter(parameter: ParameterDefinition) -> None:
|
||||
class PropertyModelComponent(AlgebraicComponent):
|
||||
MODEL_TYPE = "property_model_component"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = (parameter,)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="物性模型测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium, parameters
|
||||
return cls(name)
|
||||
|
||||
validate_component_model_class(
|
||||
PropertyModelComponent,
|
||||
library=LIBRARY,
|
||||
)
|
||||
|
||||
def property_parameter(
|
||||
*,
|
||||
default: float = 0.0,
|
||||
minimum: float | None = None,
|
||||
maximum: float | None = None,
|
||||
options: tuple[ParameterOption, ...] = (),
|
||||
description: object = "",
|
||||
) -> ParameterDefinition:
|
||||
return ParameterDefinition(
|
||||
"property_model",
|
||||
default,
|
||||
label="物性计算模型",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=minimum,
|
||||
maximum=maximum,
|
||||
editor="amesimGasPropertyModel",
|
||||
options=options,
|
||||
description=description, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "description must be a string"):
|
||||
validate_parameter(property_parameter(description=None))
|
||||
with self.assertRaisesRegex(ValueError, "description must not be blank"):
|
||||
validate_parameter(property_parameter(description=" "))
|
||||
with self.assertRaisesRegex(ValueError, "must declare options"):
|
||||
validate_parameter(property_parameter())
|
||||
with self.assertRaisesRegex(ValueError, "duplicate option values"):
|
||||
validate_parameter(
|
||||
property_parameter(
|
||||
options=(
|
||||
ParameterOption(0, "理想气体"),
|
||||
ParameterOption(0, "重复模型"),
|
||||
)
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "finite numbers"):
|
||||
validate_parameter(
|
||||
property_parameter(
|
||||
options=(ParameterOption(float("nan"), "非法模型"),)
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "default value must be one of 0"):
|
||||
validate_parameter(
|
||||
property_parameter(
|
||||
default=1.0,
|
||||
options=(ParameterOption(0, "理想气体"),),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "option 1 must be at most 0"):
|
||||
validate_parameter(
|
||||
property_parameter(
|
||||
minimum=0.0,
|
||||
maximum=0.0,
|
||||
options=(ParameterOption(1, "越界模型"),),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "option values must be integers"):
|
||||
validate_parameter(
|
||||
property_parameter(
|
||||
default=0.5,
|
||||
options=(ParameterOption(0.5, "非整数模型"),),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -3,10 +3,13 @@ import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from PythonModels.core.solver import SolveIVPConfig, integrate_ode
|
||||
from app.simulation.solvers.solver import SolveIVPConfig, integrate_ode
|
||||
|
||||
|
||||
class IntegrateOdeTests(unittest.TestCase):
|
||||
def test_generic_solver_keeps_canonical_default_tolerance(self) -> None:
|
||||
self.assertEqual(SolveIVPConfig().atol, 1.0e-8)
|
||||
|
||||
def test_scipy_solver_receives_step_size_controls(self) -> None:
|
||||
calls: list[dict[str, object]] = []
|
||||
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.simulation.components.experimental.library import LIBRARY
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.registry import (
|
||||
COMPONENT_MODEL_REGISTRY,
|
||||
ComponentModelSpec,
|
||||
validate_component_model_class,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
|
||||
|
||||
GAS_REFERENCE_PARAMETER = ParameterDefinition(
|
||||
"gi",
|
||||
0.0,
|
||||
label="气体类型索引",
|
||||
minimum=0.0,
|
||||
maximum=99.0,
|
||||
editor="amesimGasReference",
|
||||
)
|
||||
|
||||
|
||||
class GasReferenceFixture(AlgebraicComponent):
|
||||
MODEL_TYPE = "gas_reference_fixture"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = (GAS_REFERENCE_PARAMETER,)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="气体引用测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(),
|
||||
)
|
||||
|
||||
def __init__(self, name: str, gi: float) -> None:
|
||||
super().__init__(name)
|
||||
self.set_parameter_values({"gi": gi})
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium
|
||||
return cls(name, parameters["gi"])
|
||||
|
||||
|
||||
class GasMediumDefinitionFixture(AlgebraicComponent):
|
||||
MODEL_TYPE = "gas_medium_definition_fixture"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"gi",
|
||||
1.0,
|
||||
label="介质索引",
|
||||
minimum=1.0,
|
||||
maximum=99.0,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="气体介质定义测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(),
|
||||
role="amesimGasMediumDefinition",
|
||||
)
|
||||
|
||||
def __init__(self, name: str, gi: float) -> None:
|
||||
super().__init__(name)
|
||||
self.set_parameter_values({"gi": gi})
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium
|
||||
return cls(name, parameters["gi"])
|
||||
|
||||
|
||||
class ConnectedGasReferenceFixture(AlgebraicComponent):
|
||||
MODEL_TYPE = "connected_gas_reference_fixture"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (GAS_REFERENCE_PARAMETER,)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="相连气体引用测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(PortDisplaySpec("port_1", "right"),),
|
||||
)
|
||||
|
||||
def __init__(self, name: str, gi: float) -> None:
|
||||
super().__init__(name)
|
||||
self.register_declared_port("port_1")
|
||||
self.set_parameter_values({"gi": gi})
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium
|
||||
return cls(name, parameters["gi"])
|
||||
|
||||
|
||||
GAS_REFERENCE_SPEC = ComponentModelSpec(
|
||||
component_class=GasReferenceFixture,
|
||||
library=LIBRARY,
|
||||
)
|
||||
GAS_MEDIUM_DEFINITION_SPEC = ComponentModelSpec(
|
||||
component_class=GasMediumDefinitionFixture,
|
||||
library=LIBRARY,
|
||||
)
|
||||
CONNECTED_GAS_REFERENCE_SPEC = ComponentModelSpec(
|
||||
component_class=ConnectedGasReferenceFixture,
|
||||
library=LIBRARY,
|
||||
)
|
||||
|
||||
|
||||
def system_xml(
|
||||
*,
|
||||
gi: float | None,
|
||||
medium_reference_version: str | None = None,
|
||||
medium_gas_indices: tuple[float, ...] = (),
|
||||
) -> bytes:
|
||||
version_attribute = (
|
||||
f' mediumReferenceVersion="{medium_reference_version}"'
|
||||
if medium_reference_version is not None
|
||||
else ""
|
||||
)
|
||||
reference_parameter = (
|
||||
f'<Parameter name="gi" value="{gi}"/>' if gi is not None else ""
|
||||
)
|
||||
medium_components = "".join(
|
||||
f"""
|
||||
<Component id="medium_{index}" name="medium_{index}"
|
||||
type="gas_medium_definition_fixture"
|
||||
componentType="gas_medium_definition_fixture"
|
||||
x="0" y="0">
|
||||
<Parameter name="gi" value="{gas_index}"/>
|
||||
</Component>"""
|
||||
for index, gas_index in enumerate(medium_gas_indices, start=1)
|
||||
)
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<System name="medium-contract" schemaVersion="2" unitSystem="SI"{version_attribute}>
|
||||
<Simulation tStart="0" tStop="1" step="0.1" maxStep="0.01" method="BDF"/>
|
||||
<Components>
|
||||
<Component id="consumer_1" name="consumer_1"
|
||||
type="gas_reference_fixture"
|
||||
componentType="gas_reference_fixture"
|
||||
x="0" y="0">
|
||||
{reference_parameter}
|
||||
</Component>{medium_components}
|
||||
</Components>
|
||||
<Connections/>
|
||||
</System>
|
||||
""".encode()
|
||||
|
||||
|
||||
def connected_system_xml(
|
||||
*,
|
||||
first_gi: float,
|
||||
second_gi: float,
|
||||
medium_gas_indices: tuple[float, ...],
|
||||
) -> bytes:
|
||||
medium_components = "".join(
|
||||
f"""
|
||||
<Component id="medium_{index}" name="medium_{index}"
|
||||
type="gas_medium_definition_fixture"
|
||||
componentType="gas_medium_definition_fixture"
|
||||
x="0" y="0">
|
||||
<Parameter name="gi" value="{gas_index}"/>
|
||||
</Component>"""
|
||||
for index, gas_index in enumerate(medium_gas_indices, start=1)
|
||||
)
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<System name="connected-medium-contract" schemaVersion="2" unitSystem="SI"
|
||||
mediumReferenceVersion="1">
|
||||
<Simulation tStart="0" tStop="1" step="0.1" maxStep="0.01" method="BDF"/>
|
||||
<Components>
|
||||
<Component id="consumer_1" name="consumer_1"
|
||||
type="connected_gas_reference_fixture"
|
||||
componentType="connected_gas_reference_fixture"
|
||||
x="0" y="0">
|
||||
<Port name="port_1" kind="physical" domain="pneumatic"
|
||||
nominalRole="bidirectional" positiveFlowDirection="intoComponent"
|
||||
side="right"/>
|
||||
<Parameter name="gi" value="{first_gi}"/>
|
||||
</Component>
|
||||
<Component id="consumer_2" name="consumer_2"
|
||||
type="connected_gas_reference_fixture"
|
||||
componentType="connected_gas_reference_fixture"
|
||||
x="100" y="0">
|
||||
<Port name="port_1" kind="physical" domain="pneumatic"
|
||||
nominalRole="bidirectional" positiveFlowDirection="intoComponent"
|
||||
side="left"/>
|
||||
<Parameter name="gi" value="{second_gi}"/>
|
||||
</Component>{medium_components}
|
||||
</Components>
|
||||
<Connections>
|
||||
<Connection id="gas_connection" kind="physical" domain="pneumatic">
|
||||
<Endpoint component="consumer_1" port="port_1"/>
|
||||
<Endpoint component="consumer_2" port="port_1"/>
|
||||
</Connection>
|
||||
</Connections>
|
||||
</System>
|
||||
""".encode()
|
||||
|
||||
|
||||
def air_medium_system_xml(*, property_model: float | None) -> bytes:
|
||||
property_parameter = (
|
||||
f'<Parameter name="property_model" value="{property_model}"/>'
|
||||
if property_model is not None
|
||||
else ""
|
||||
)
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<System name="air-medium-contract" schemaVersion="2" unitSystem="SI"
|
||||
mediumReferenceVersion="1">
|
||||
<Simulation tStart="0" tStop="1" step="0.1" maxStep="0.01" method="BDF"/>
|
||||
<Components>
|
||||
<Component id="air_1" name="air_1"
|
||||
type="amesim_ideal_air_medium"
|
||||
componentType="amesim_ideal_air_medium"
|
||||
x="0" y="0">
|
||||
<Parameter name="gi" value="1"/>
|
||||
{property_parameter}
|
||||
</Component>
|
||||
</Components>
|
||||
<Connections/>
|
||||
</System>
|
||||
""".encode()
|
||||
|
||||
|
||||
class MediumReferenceCatalogContractTests(unittest.TestCase):
|
||||
def test_registry_emits_parameter_editor_and_component_role(self) -> None:
|
||||
validate_component_model_class(GasReferenceFixture, library=LIBRARY)
|
||||
validate_component_model_class(GasMediumDefinitionFixture, library=LIBRARY)
|
||||
|
||||
reference_payload = GAS_REFERENCE_SPEC.as_catalog_dict()
|
||||
definition_payload = GAS_MEDIUM_DEFINITION_SPEC.as_catalog_dict()
|
||||
|
||||
self.assertEqual(
|
||||
reference_payload["parameters"][0]["editor"],
|
||||
"amesimGasReference",
|
||||
)
|
||||
self.assertEqual(
|
||||
definition_payload["role"],
|
||||
"amesimGasMediumDefinition",
|
||||
)
|
||||
|
||||
def test_committed_catalog_schema_constrains_the_extension_values(self) -> None:
|
||||
schema_path = (
|
||||
Path(__file__).resolve().parent.parent
|
||||
/ "schemas"
|
||||
/ "component-catalog-v1.schema.json"
|
||||
)
|
||||
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual(
|
||||
schema["$defs"]["parameter"]["properties"]["editor"]["enum"],
|
||||
["amesimGasReference", "amesimGasPropertyModel"],
|
||||
)
|
||||
self.assertEqual(
|
||||
schema["$defs"]["parameter"]["properties"]["options"]["items"][
|
||||
"$ref"
|
||||
],
|
||||
"#/$defs/parameterOption",
|
||||
)
|
||||
self.assertEqual(
|
||||
schema["$defs"]["component"]["properties"]["role"]["enum"],
|
||||
["amesimGasMediumDefinition"],
|
||||
)
|
||||
|
||||
|
||||
class MediumReferenceSystemXmlContractTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.registry_patch = patch.dict(
|
||||
COMPONENT_MODEL_REGISTRY,
|
||||
{
|
||||
GasReferenceFixture.MODEL_TYPE: GAS_REFERENCE_SPEC,
|
||||
GasMediumDefinitionFixture.MODEL_TYPE: GAS_MEDIUM_DEFINITION_SPEC,
|
||||
ConnectedGasReferenceFixture.MODEL_TYPE: CONNECTED_GAS_REFERENCE_SPEC,
|
||||
},
|
||||
)
|
||||
self.registry_patch.start()
|
||||
self.addCleanup(self.registry_patch.stop)
|
||||
|
||||
def test_legacy_gi_one_without_definition_maps_to_builtin_air(self) -> None:
|
||||
report = validate_system_xml_document(system_xml(gi=1.0))
|
||||
|
||||
self.assertTrue(report.valid)
|
||||
self.assertEqual(
|
||||
{issue.code for issue in report.issues},
|
||||
{"AMESIM_GI_LEGACY_AIR_MAPPED"},
|
||||
)
|
||||
assert report.document is not None
|
||||
self.assertEqual(report.document.medium_reference_version, "1")
|
||||
self.assertEqual(
|
||||
report.document.as_project_data()["mediumReferenceVersion"],
|
||||
1,
|
||||
)
|
||||
self.assertEqual(
|
||||
report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"],
|
||||
0.0,
|
||||
)
|
||||
|
||||
def test_legacy_missing_gi_injects_builtin_air(self) -> None:
|
||||
report = validate_system_xml_document(system_xml(gi=None))
|
||||
|
||||
self.assertTrue(report.valid)
|
||||
self.assertEqual(
|
||||
{issue.code for issue in report.issues},
|
||||
{"AMESIM_GI_DEFAULT_INJECTED"},
|
||||
)
|
||||
assert report.document is not None
|
||||
self.assertEqual(
|
||||
report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"],
|
||||
0.0,
|
||||
)
|
||||
|
||||
def test_versioned_gi_one_is_not_reinterpreted(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
system_xml(gi=1.0, medium_reference_version="1")
|
||||
)
|
||||
|
||||
self.assertFalse(report.valid)
|
||||
self.assertEqual(
|
||||
{issue.code for issue in report.issues},
|
||||
{"AMESIM_GAS_REFERENCE_UNDEFINED"},
|
||||
)
|
||||
assert report.document is not None
|
||||
self.assertEqual(report.document.medium_reference_version, "1")
|
||||
self.assertEqual(
|
||||
report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"],
|
||||
1.0,
|
||||
)
|
||||
|
||||
def test_legacy_document_with_definition_is_not_reinterpreted(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
system_xml(gi=1.0, medium_gas_indices=(1.0,))
|
||||
)
|
||||
|
||||
self.assertTrue(report.valid)
|
||||
self.assertEqual(report.issues, ())
|
||||
assert report.document is not None
|
||||
self.assertEqual(
|
||||
report.document.as_project_data()["nodes"][0]["data"]["parameters"]["gi"],
|
||||
1.0,
|
||||
)
|
||||
|
||||
def test_versioned_positive_reference_requires_matching_definition(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
system_xml(
|
||||
gi=2.0,
|
||||
medium_reference_version="1",
|
||||
medium_gas_indices=(2.0,),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(report.valid)
|
||||
self.assertEqual(report.issues, ())
|
||||
|
||||
def test_missing_property_model_is_defaulted_for_versioned_medium(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
air_medium_system_xml(property_model=None)
|
||||
)
|
||||
|
||||
self.assertTrue(report.valid)
|
||||
self.assertEqual(
|
||||
{issue.code for issue in report.issues},
|
||||
{"AMESIM_GAS_PROPERTY_MODEL_DEFAULTED"},
|
||||
)
|
||||
assert report.document is not None
|
||||
parameters = report.document.as_project_data()["nodes"][0]["data"][
|
||||
"parameters"
|
||||
]
|
||||
self.assertEqual(parameters["gi"], 1.0)
|
||||
self.assertEqual(parameters["property_model"], 0.0)
|
||||
|
||||
def test_explicit_supported_property_model_round_trips_without_warning(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
air_medium_system_xml(property_model=0.0)
|
||||
)
|
||||
|
||||
self.assertTrue(report.valid)
|
||||
self.assertEqual(report.issues, ())
|
||||
|
||||
def test_unsupported_property_model_is_rejected_semantically(self) -> None:
|
||||
for property_model in (1.0, 99.0):
|
||||
with self.subTest(property_model=property_model):
|
||||
report = validate_system_xml_document(
|
||||
air_medium_system_xml(property_model=property_model)
|
||||
)
|
||||
|
||||
self.assertFalse(report.valid)
|
||||
self.assertEqual(
|
||||
{issue.code for issue in report.issues},
|
||||
{"PARAMETER_VALUE_INVALID"},
|
||||
)
|
||||
|
||||
def test_medium_definition_index_must_be_integer_in_range(self) -> None:
|
||||
fractional = validate_system_xml_document(
|
||||
system_xml(
|
||||
gi=0.0,
|
||||
medium_reference_version="1",
|
||||
medium_gas_indices=(1.5,),
|
||||
)
|
||||
)
|
||||
zero = validate_system_xml_document(
|
||||
system_xml(
|
||||
gi=0.0,
|
||||
medium_reference_version="1",
|
||||
medium_gas_indices=(0.0,),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"AMESIM_GAS_MEDIUM_INDEX_INVALID",
|
||||
{issue.code for issue in fractional.issues},
|
||||
)
|
||||
self.assertIn(
|
||||
"AMESIM_GAS_MEDIUM_INDEX_INVALID",
|
||||
{issue.code for issue in zero.issues},
|
||||
)
|
||||
|
||||
def test_medium_definition_indices_must_be_unique(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
system_xml(
|
||||
gi=1.0,
|
||||
medium_reference_version="1",
|
||||
medium_gas_indices=(1.0, 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertFalse(report.valid)
|
||||
self.assertIn(
|
||||
"AMESIM_GAS_MEDIUM_INDEX_DUPLICATE",
|
||||
{issue.code for issue in report.issues},
|
||||
)
|
||||
|
||||
def test_gas_reference_index_must_be_integer_in_range(self) -> None:
|
||||
fractional = validate_system_xml_document(
|
||||
system_xml(gi=1.5, medium_reference_version="1")
|
||||
)
|
||||
out_of_range = validate_system_xml_document(
|
||||
system_xml(gi=100.0, medium_reference_version="1")
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"AMESIM_GAS_REFERENCE_INDEX_INVALID",
|
||||
{issue.code for issue in fractional.issues},
|
||||
)
|
||||
self.assertIn(
|
||||
"AMESIM_GAS_REFERENCE_INDEX_INVALID",
|
||||
{issue.code for issue in out_of_range.issues},
|
||||
)
|
||||
|
||||
def test_connected_pneumatic_references_cannot_conflict(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
connected_system_xml(
|
||||
first_gi=1.0,
|
||||
second_gi=2.0,
|
||||
medium_gas_indices=(1.0, 2.0),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertFalse(report.valid)
|
||||
self.assertEqual(
|
||||
{issue.code for issue in report.issues},
|
||||
{"AMESIM_GAS_REFERENCE_CONFLICT"},
|
||||
)
|
||||
|
||||
def test_connected_pneumatic_references_may_share_one_medium(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
connected_system_xml(
|
||||
first_gi=1.0,
|
||||
second_gi=1.0,
|
||||
medium_gas_indices=(1.0,),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(report.valid)
|
||||
self.assertEqual(report.issues, ())
|
||||
|
||||
def test_unknown_medium_reference_version_is_rejected_by_xsd(self) -> None:
|
||||
report = validate_system_xml_document(
|
||||
system_xml(gi=0.0, medium_reference_version="2")
|
||||
)
|
||||
|
||||
self.assertFalse(report.valid)
|
||||
self.assertIsNone(report.document)
|
||||
self.assertEqual({issue.code for issue in report.issues}, {"XSD_VALIDATION_ERROR"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.core.peng_robinson import AIR_PR, HELIUM_PR, NITROGEN_PR, PengRobinsonFluid
|
||||
from app.simulation.core.peng_robinson import AIR_PR, HELIUM_PR, NITROGEN_PR, PengRobinsonFluid
|
||||
|
||||
|
||||
class PengRobinsonTest(unittest.TestCase):
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
pydantic_to_jsonable,
|
||||
)
|
||||
|
||||
|
||||
class ReactFlowProjectMediumReferenceVersionTests(unittest.TestCase):
|
||||
def test_version_one_is_accepted_and_serialized(self) -> None:
|
||||
project = ReactFlowProjectPayload(
|
||||
name="versioned-medium-references",
|
||||
mediumReferenceVersion=1,
|
||||
)
|
||||
|
||||
self.assertEqual(project.mediumReferenceVersion, 1)
|
||||
self.assertEqual(
|
||||
pydantic_to_jsonable(project)["mediumReferenceVersion"],
|
||||
1,
|
||||
)
|
||||
|
||||
def test_other_medium_reference_versions_are_rejected(self) -> None:
|
||||
for version in (0, 2, -1, "1"):
|
||||
with self.subTest(version=version):
|
||||
with self.assertRaises(ValueError):
|
||||
ReactFlowProjectPayload(
|
||||
name="unsupported-medium-references",
|
||||
mediumReferenceVersion=version,
|
||||
)
|
||||
|
||||
def test_legacy_project_without_version_remains_compatible(self) -> None:
|
||||
project = ReactFlowProjectPayload(name="legacy-project")
|
||||
|
||||
self.assertIsNone(project.mediumReferenceVersion)
|
||||
|
||||
def test_xml_export_adds_default_property_model_to_legacy_medium_node(self) -> None:
|
||||
project = ReactFlowProjectPayload(
|
||||
name="legacy-medium-node",
|
||||
mediumReferenceVersion=1,
|
||||
nodes=[
|
||||
{
|
||||
"id": "air_1",
|
||||
"position": {"x": 10, "y": 20},
|
||||
"data": {
|
||||
"label": "Air 1",
|
||||
"componentType": "amesim_ideal_air_medium",
|
||||
"modelType": "amesim_ideal_air_medium",
|
||||
"ports": [],
|
||||
"parameters": {"gi": 1},
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
xml = build_reactflow_system_xml(project).decode("utf-8")
|
||||
|
||||
self.assertIn('<Parameter name="gi" value="1"', xml)
|
||||
self.assertIn('<Parameter name="property_model" value="0.0"', xml)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4,7 +4,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.scripts.run_test_mql_baseline import (
|
||||
from app.simulation.examples.test_mql.run_baseline import (
|
||||
TestMqlBaselineExecutionConfig,
|
||||
TestMqlBaselinePathConfig,
|
||||
TestMqlBaselineScriptConfig,
|
||||
|
||||
@@ -4,10 +4,10 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.core.solver import SolveIVPConfig
|
||||
from PythonModels.systems.test_mql import TestMqlPnl0001LineRhsDiagnostic
|
||||
from app.simulation.examples.test_mql.solver import SolveIVPConfig
|
||||
from app.simulation.examples.test_mql.system import TestMqlPnl0001LineRhsDiagnostic
|
||||
|
||||
from PythonModels.scripts.run_test_mql_full_state_comparison import (
|
||||
from app.simulation.examples.test_mql.run_full_state_comparison import (
|
||||
TestMqlFullStateComparisonExecutionConfig,
|
||||
TestMqlFullStateComparisonPathConfig,
|
||||
TestMqlFullStateComparisonScriptConfig,
|
||||
@@ -43,7 +43,7 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase):
|
||||
def test_ideal_pn2vol_reference_dtemp_uses_internal_energy_mass_offset(
|
||||
self,
|
||||
) -> None:
|
||||
from PythonModels.components.amesim_pneumatic import HELIUM_PNEUMATIC_GAS
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import HELIUM_PNEUMATIC_GAS
|
||||
|
||||
gas = HELIUM_PNEUMATIC_GAS
|
||||
result = _ideal_pn2vol_reference_dtemp(
|
||||
|
||||
@@ -87,6 +87,7 @@ class SystemXmlProtocolTests(unittest.TestCase):
|
||||
self.assertEqual(root.attrib["name"], "protocol-test")
|
||||
self.assertEqual(root.attrib["schemaVersion"], "2")
|
||||
self.assertEqual(root.attrib["unitSystem"], "SI")
|
||||
self.assertEqual(root.attrib["mediumReferenceVersion"], "1")
|
||||
self.assertEqual(
|
||||
[child.tag for child in root],
|
||||
["Simulation", "Components", "Connections"],
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.systems.test_mql_baseline import run_test_mql_baseline_passthrough
|
||||
from app.simulation.examples.test_mql.baseline import run_test_mql_baseline_passthrough
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.core.solver import SolveIVPConfig
|
||||
from PythonModels.systems.test_mql import TestMqlSystem
|
||||
from app.simulation.examples.test_mql.solver import SolveIVPConfig
|
||||
from app.simulation.examples.test_mql.system import TestMqlSystem
|
||||
|
||||
|
||||
class TestMqlPneumaticBranchSimulationTests(unittest.TestCase):
|
||||
|
||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_chamber_observations import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_chamber_observations import (
|
||||
build_test_mql_chamber_observation_catalog,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.core.solver import SolveIVPConfig
|
||||
from PythonModels.systems.test_mql import TestMqlSystem
|
||||
from app.simulation.examples.test_mql.solver import SolveIVPConfig
|
||||
from app.simulation.examples.test_mql.system import TestMqlSystem
|
||||
|
||||
|
||||
class TestMqlPneumaticChamberSegmentTests(unittest.TestCase):
|
||||
|
||||
@@ -2,11 +2,11 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.components.amesim_pneumatic import (
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import (
|
||||
AmesimPneumaticOrifice,
|
||||
AmesimPneumaticVolume,
|
||||
)
|
||||
from PythonModels.systems.test_mql_closure import (
|
||||
from app.simulation.examples.test_mql.closure import (
|
||||
TestMqlPneumaticBranchComponents,
|
||||
TestMqlPneumaticClosure,
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_comparison import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_comparison import (
|
||||
DEFAULT_TEST_MQL_ALIGNMENT_PATHS,
|
||||
TestMqlComparisonError,
|
||||
compare_test_mql_series,
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.systems.test_mql_computed import (
|
||||
from app.simulation.examples.test_mql.computed import (
|
||||
run_test_mql_computed_geometry,
|
||||
run_test_mql_computed_line_relations,
|
||||
run_test_mql_computed_mechanical_relations,
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.systems.test_mql_config import TestMqlConfig, resolve_numeric_expression
|
||||
from app.simulation.examples.test_mql.config import TestMqlConfig, resolve_numeric_expression
|
||||
|
||||
|
||||
class TestMqlConfigTest(unittest.TestCase):
|
||||
|
||||
@@ -6,11 +6,15 @@ from pathlib import Path
|
||||
|
||||
from app.main import ReactFlowProjectPayload, run_reactflow_test_mql
|
||||
from app.simulation.examples.test_mql.run import prepare_test_mql_run, run_test_mql
|
||||
from app.simulation.examples.test_mql.solver import SolveIVPConfig
|
||||
from app.simulation.examples.test_mql.system import TestMqlRunConfig
|
||||
from app.simulation.paths import PROJECT_ROOT, SIMULATION_RUNS_DIR
|
||||
from PythonModels.systems.test_mql import TestMqlRunConfig
|
||||
|
||||
|
||||
class TestMqlExampleRunnerTests(unittest.TestCase):
|
||||
def test_test_mql_keeps_calibrated_default_tolerance(self) -> None:
|
||||
self.assertEqual(SolveIVPConfig().atol, 1.0e-10)
|
||||
|
||||
def test_default_run_paths_use_app_simulation_runs_dir(self) -> None:
|
||||
prepared = prepare_test_mql_run()
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_line_observations import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_line_observations import (
|
||||
G_PER_S_TO_KG_PER_S,
|
||||
build_test_mql_line_observation_catalog,
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog
|
||||
from PythonModels.systems.test_mql_lines import build_test_mql_line_assembly
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_variables import build_test_mql_variable_catalog
|
||||
from app.simulation.examples.test_mql.lines import build_test_mql_line_assembly
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.core.solver import SolveIVPConfig
|
||||
from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog
|
||||
from PythonModels.systems.test_mql import TestMqlSystem
|
||||
from PythonModels.systems.test_mql_mechanical import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.examples.test_mql.solver import SolveIVPConfig
|
||||
from app.simulation.reporting.test_mql_variables import build_test_mql_variable_catalog
|
||||
from app.simulation.examples.test_mql.system import TestMqlSystem
|
||||
from app.simulation.examples.test_mql.mechanical import (
|
||||
TestMqlMechanicalMassClosure,
|
||||
build_test_mql_mechanical_assembly,
|
||||
circular_area,
|
||||
|
||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_mechanical_observations import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_mechanical_observations import (
|
||||
build_test_mql_mechanical_observation_catalog,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.systems.test_mql import TestMqlSystem
|
||||
from PythonModels.systems.test_mql_nodes import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.examples.test_mql.system import TestMqlSystem
|
||||
from app.simulation.examples.test_mql.nodes import (
|
||||
build_test_mql_node3_assembly,
|
||||
build_test_mql_node4_assembly,
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_comparison import compare_test_mql_series
|
||||
from PythonModels.reporting.test_mql_observations import build_test_mql_observation_catalog
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_comparison import compare_test_mql_series
|
||||
from app.simulation.reporting.test_mql_observations import build_test_mql_observation_catalog
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -3,12 +3,12 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_orifice_observations import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_orifice_observations import (
|
||||
G_PER_S_TO_KG_PER_S,
|
||||
build_test_mql_orifice_observation_catalog,
|
||||
)
|
||||
from PythonModels.systems.test_mql_pneumatic import (
|
||||
from app.simulation.examples.test_mql.pneumatic import (
|
||||
TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_observations import build_test_mql_observation_catalog
|
||||
from PythonModels.reporting.test_mql_output_schema import build_test_mql_output_schema
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_observations import build_test_mql_observation_catalog
|
||||
from app.simulation.reporting.test_mql_output_schema import build_test_mql_output_schema
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -3,10 +3,10 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_observations import build_test_mql_observation_catalog
|
||||
from PythonModels.reporting.test_mql_output_schema import build_test_mql_output_schema
|
||||
from PythonModels.reporting.test_mql_output_validation import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_observations import build_test_mql_observation_catalog
|
||||
from app.simulation.reporting.test_mql_output_schema import build_test_mql_output_schema
|
||||
from app.simulation.reporting.test_mql_output_validation import (
|
||||
TestMqlOutputValidationError,
|
||||
compare_validated_test_mql_output,
|
||||
validate_test_mql_output,
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.components.amesim_pneumatic import HELIUM_PNEUMATIC_GAS, cm3_to_m3
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.systems.test_mql_pneumatic import (
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import HELIUM_PNEUMATIC_GAS, cm3_to_m3
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.examples.test_mql.pneumatic import (
|
||||
TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER,
|
||||
absolute_pressure_from_amesim_bar_parameter,
|
||||
build_test_mql_pneumatic_assembly,
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.systems.test_mql_line_parameters import load_test_mql_pnl0001_specs
|
||||
from PythonModels.systems.test_mql_pneumatic_lines import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.examples.test_mql.line_parameters import load_test_mql_pnl0001_specs
|
||||
from app.simulation.examples.test_mql.pneumatic_lines import (
|
||||
TEST_MQL_PNL0001_D20_L1_LINEAR_CONDUCTANCE,
|
||||
build_test_mql_pnl0001_assembly,
|
||||
)
|
||||
|
||||
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.core.solver import SolveIVPConfig
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_output_schema import build_test_mql_output_schema
|
||||
from PythonModels.reporting.test_mql_output_validation import compare_validated_test_mql_output
|
||||
from PythonModels.systems.test_mql import TestMqlSystem
|
||||
from app.simulation.examples.test_mql.solver import SolveIVPConfig
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_output_schema import build_test_mql_output_schema
|
||||
from app.simulation.reporting.test_mql_output_validation import compare_validated_test_mql_output
|
||||
from app.simulation.examples.test_mql.system import TestMqlSystem
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -3,10 +3,10 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.systems.test_mql import TestMqlSystem
|
||||
from PythonModels.systems.test_mql_line_parameters import load_test_mql_pnl0002_specs
|
||||
from PythonModels.systems.test_mql_pneumatic_lines import build_test_mql_pnl0002_assembly
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.examples.test_mql.system import TestMqlSystem
|
||||
from app.simulation.examples.test_mql.line_parameters import load_test_mql_pnl0002_specs
|
||||
from app.simulation.examples.test_mql.pneumatic_lines import build_test_mql_pnl0002_assembly
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -3,13 +3,13 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.systems.test_mql import TestMqlSystem
|
||||
from PythonModels.systems.test_mql_line_parameters import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.examples.test_mql.system import TestMqlSystem
|
||||
from app.simulation.examples.test_mql.line_parameters import (
|
||||
load_test_mql_pnl0003_specs,
|
||||
load_test_mql_pnl00r_specs,
|
||||
)
|
||||
from PythonModels.systems.test_mql_pneumatic_lines import (
|
||||
from app.simulation.examples.test_mql.pneumatic_lines import (
|
||||
build_test_mql_pnl0003_assembly,
|
||||
build_test_mql_pnl00r_assembly,
|
||||
)
|
||||
|
||||
@@ -2,12 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from PythonModels.components.amesim_pneumatic import (
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import (
|
||||
AmesimPneumaticOrifice,
|
||||
AmesimPneumaticVolume,
|
||||
AmesimVariablePneumaticVolume,
|
||||
)
|
||||
from PythonModels.systems.test_mql import GLOBAL_PARAMETERS, SUBMODEL_COUNTS, TestMqlSystem
|
||||
from app.simulation.examples.test_mql.system import GLOBAL_PARAMETERS, SUBMODEL_COUNTS, TestMqlSystem
|
||||
|
||||
|
||||
class TestMqlStructuralTest(unittest.TestCase):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.systems.test_mql_topology import load_test_mql_cir_topology
|
||||
from app.simulation.examples.test_mql.topology import load_test_mql_cir_topology
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from PythonModels.reporting.test_mql_variables import (
|
||||
from app.simulation.reporting.amesim_results import load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
build_test_mql_variable_catalog,
|
||||
split_data_path,
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user