def measurement_chain_with_equipment() -> MeasurementChain: """Get a default measurement chain with attached equipment.""" source = SignalSource( "Current measurement", output_signal=Signal(signal_type="analog", units="V"), error=Error(Q_(1, "percent")), ) ad_conversion = SignalTransformation( "AD conversion current measurement", error=Error(Q_(0, "percent")), func=MathematicalExpression(expression="a*x+b", parameters=dict(a=Q_(1, "1/V"), b=Q_(1, ""))), ) calibration = SignalTransformation( "Current measurement calibration", error=Error(Q_(1.2, "percent")), func=MathematicalExpression(expression="a*x+b", parameters=dict(a=Q_(1, "A"), b=Q_(1, "A"))), ) eq_source = MeasurementEquipment( name="Source Equipment", sources=[source], ) eq_ad_conversion = MeasurementEquipment(name="AD Equipment", transformations=[ad_conversion]) eq_calibration = MeasurementEquipment(name="Calibration Equipment", transformations=[calibration]) mc = MeasurementChain.from_equipment("Measurement chain", eq_source) mc.add_transformation_from_equipment(eq_ad_conversion) mc.add_transformation_from_equipment(eq_calibration) return mc
def _determine_output_signal_unit( func: MathematicalExpression, input_unit: Union[str, Union] ) -> pint.Unit: """Determine the unit of a transformations' output signal. Parameters ---------- func : The function describing the transformation input_unit : The unit of the input signal Returns ------- pint.Unit: Unit of the transformations' output signal """ input_unit = U_(input_unit) if func is not None: variables = func.get_variable_names() if len(variables) != 1: raise ValueError("The provided function must have exactly 1 parameter") try: test_output = func.evaluate(**{variables[0]: Q_(1, input_unit)}) except Exception as e: raise ValueError( "The provided function is incompatible with the input signals unit." f" \nThe test raised the following exception:\n{e}" ) return test_output.data.units return input_unit
def sine( f: pint.Quantity, amp: pint.Quantity, bias: pint.Quantity = None, phase: pint.Quantity = Q_(0, "rad"), ) -> TimeSeries: """Create a simple sine TimeSeries from quantity parameters. f(t) = amp*sin(f*t+phase)+bias Parameters ---------- f : pint.Quantity Frequency of the sine (in Hz) amp : pint.Quantity Sine amplitude bias : pint.Quantity function bias phase : pint.Quantity phase shift Returns ------- TimeSeries """ if bias is None: bias = 0.0 * amp.u expr_string = "a*sin(o*t+p)+b" parameters = {"a": amp, "b": bias, "o": Q_(2 * np.pi, "rad") * f, "p": phase} expr = MathematicalExpression(expression=expr_string, parameters=parameters) return TimeSeries(expr)
def measurement_chain_without_equipment() -> MeasurementChain: """Get a default measurement chain without attached equipment.""" mc = MeasurementChain( "Current measurement chain", SignalSource( "Current measurement", Signal("analog", "V"), Error(Q_(0.1, "percent")), ), ) mc.add_transformation( SignalTransformation( name="AD conversion", error=Error(Q_(0.5, "percent")), type_transformation="AD", )) mc.add_transformation( SignalTransformation( name="Calibration", error=Error(Q_(1.5, "percent")), func=MathematicalExpression("a*x+b", parameters=dict(a=Q_(3, "A/V"), b=Q_(2, "A"))), )) return mc
def from_yaml_tree(self, node: dict, tag: str, ctx): """Construct from tree.""" parameters = {} for k, v in node["parameters"].items(): if hasattr(v, META_ATTR): dims = getattr(v, META_ATTR)["dims"] delattr(v, META_ATTR) v = (v, dims) parameters[k] = v return MathematicalExpression(sympy.sympify(node["expression"]), parameters=parameters)
def _default_transformation(kwargs: dict = None) -> SignalTransformation: """Return a default `SignalTransformation`. Use the kwargs parameter to modify the default values. """ default_kwargs = dict( name="transformation", error=Error(0.1), func=MathematicalExpression("a*x", parameters={"a": Q_(1, "1/V")}), type_transformation="AD", ) if kwargs is not None: default_kwargs.update(kwargs) return SignalTransformation(**default_kwargs)
def test_set_parameter(self): """Test the set_parameter function of the mathematical expression.""" expr = MathematicalExpression("a*b + c/d - e") # check initial configuration self._check_params_and_vars(expr, {}, ["a", "b", "c", "d", "e"]) # set first parameters expr.set_parameter("d", 1) expr.set_parameter("e", 2) self._check_params_and_vars(expr, {"d": 1, "e": 2}, ["a", "b", "c"]) # set another parameter and overwrite others expr.set_parameter("a", 5) expr.set_parameter("d", 7) expr.set_parameter("e", -1) self._check_params_and_vars(expr, {"a": 5, "d": 7, "e": -1}, ["b", "c"])
def sine( f: QuantityLike, amp: QuantityLike, bias: QuantityLike = None, phase: QuantityLike = None, ) -> TimeSeries: """Create a simple sine TimeSeries from quantity parameters. f(t) = amp*sin(f*t+phase)+bias Parameters ---------- f : Frequency of the sine (in Hz) amp : Sine amplitude bias : function bias phase : phase shift Returns ------- weldx.TimeSeries : """ if phase is None: phase = Q_(0, "rad") if bias is None: amp = Q_(amp) bias = 0.0 * amp.u expr_string = "a*sin(o*t+p)+b" parameters = { "a": amp, "b": bias, "o": Q_(2 * np.pi, "rad") * Q_(f), "p": phase } expr = MathematicalExpression(expression=expr_string, parameters=parameters) return TimeSeries(expr)
def test_generic_measurement(): """Test basic measurement creation and ASDF read/write.""" data_01 = msm.Data( name="Welding current", data=xr.DataArray([1, 2, 3, 4], dims=["time"]) ) data_02 = msm.Data( name="Welding voltage", data=xr.DataArray([10, 20, 30, 40], dims=["time"]) ) src_01 = msm.Source( name="Current Sensor", output_signal=msm.Signal("analog", "V", data=None), error=msm.Error(1337.42), ) src_02 = msm.Source( name="Voltage Sensor", output_signal=msm.Signal("analog", "V", data=None), error=msm.Error(1), ) dp_01 = msm.DataTransformation( name="AD conversion current measurement", input_signal=src_01.output_signal, output_signal=msm.Signal("digital", "V", data=None), error=msm.Error(999.0), ) dp_02 = msm.DataTransformation( name="Calibration current measurement", input_signal=dp_01.output_signal, output_signal=msm.Signal("digital", "A", data=data_01), error=msm.Error(43.0), ) dp_03 = msm.DataTransformation( name="AD conversion voltage measurement", input_signal=dp_02.output_signal, output_signal=msm.Signal("digital", "V", data=None), error=msm.Error(2.0), ) dp_04 = msm.DataTransformation( name="Calibration voltage measurement", input_signal=dp_03.output_signal, output_signal=msm.Signal("digital", "V", data=data_02), error=msm.Error(Q_(3.0, "percent")), ) chn_01 = msm.MeasurementChain( name="Current measurement", data_source=src_01, data_processors=[dp_01, dp_02] ) chn_02 = msm.MeasurementChain( name="Voltage measurement", data_source=src_02, data_processors=[dp_03, dp_04] ) eqp_01 = msm.GenericEquipment( "Current Sensor", sources=[src_01], data_transformations=[dp_02] ) eqp_02 = msm.GenericEquipment( "AD Converter", sources=None, data_transformations=[dp_01, dp_03] ) eqp_03 = msm.GenericEquipment( "Voltage Sensor", sources=None, data_transformations=[dp_04] ) measurement_01 = msm.Measurement( name="Current measurement", data=[data_01], measurement_chain=chn_01 ) measurement_02 = msm.Measurement( name="Voltage measurement", data=[data_02], measurement_chain=chn_02 ) equipment = [eqp_01, eqp_02, eqp_03] measurement_data = [data_01, data_02] measurement_chains = [chn_01] measurements = [measurement_01, measurement_02] sources = [src_01] processors = [dp_01, dp_02] [a, x, b] = sympy.symbols("a x b") expr_01 = MathematicalExpression(a * x + b) expr_01.set_parameter("a", 2) expr_01.set_parameter("b", 3) print(expr_01.parameters) print(expr_01.get_variable_names()) print(expr_01.evaluate(x=3)) tree = { "equipment": equipment, "data": measurement_data, "measurements": measurements, # "expression": expr_01, "measurement_chains": measurement_chains, "data_sources": sources, "data_processors": processors, } _write_read_buffer(tree)
def single_pass_weld_example( out_file: Optional[Union[str, BytesIO]] = "single_pass_weld_example.asdf", ) -> Optional[tuple[BytesIO, dict]]: """Create ASDF file containing all required fields of the single_pass_weld schema. Parameters ---------- out_file : destination file, if None returns a BytesIO buffer. Returns ------- buff, tree When writing to memory, return the buffer and the tree (as dictionary). """ # Imports import asdf import numpy as np import pandas as pd from asdf.tags.core import Software import weldx.geometry as geo import weldx.measurement as msm # importing the weldx package with prevalent default abbreviations import weldx.transformations as tf from weldx.asdf.util import get_schema_path, write_buffer, write_read_buffer from weldx.constants import META_ATTR, Q_ from weldx.core import MathematicalExpression, TimeSeries from weldx.tags.aws.process.gas_component import GasComponent from weldx.tags.aws.process.shielding_gas_for_procedure import ( ShieldingGasForProcedure, ) from weldx.tags.aws.process.shielding_gas_type import ShieldingGasType from weldx.tags.processes.process import GmawProcess from weldx.transformations.local_cs import LocalCoordinateSystem as lcs from weldx.transformations.rotation import WXRotation from weldx.welding.groove.iso_9692_1 import get_groove from weldx.welding.util import sine # Timestamp reference_timestamp = pd.Timestamp("2020-11-09 12:00:00") # Geometry # groove + trace = geometry groove = get_groove( groove_type="VGroove", workpiece_thickness=Q_(5, "mm"), groove_angle=Q_(50, "deg"), root_face=Q_(1, "mm"), root_gap=Q_(1, "mm"), ) # define the weld seam length in mm seam_length = Q_(300, "mm") # create a linear trace segment a the complete weld seam trace trace_segment = geo.LinearHorizontalTraceSegment(seam_length) trace = geo.Trace(trace_segment) geometry = dict(groove_shape=groove, seam_length=seam_length) base_metal = dict(common_name="S355J2+N", standard="DIN EN 10225-2:2011") workpiece = dict(base_metal=base_metal, geometry=geometry) # Setup the Coordinate System Manager (CSM) # crete a new coordinate system manager with default base coordinate system csm = tf.CoordinateSystemManager("base") # add the workpiece coordinate system csm.add_cs( coordinate_system_name="workpiece", reference_system_name="base", lcs=trace.coordinate_system, ) tcp_start_point = Q_([5.0, 0.0, 2.0], "mm") tcp_end_point = Q_([-5.0, 0.0, 2.0], "mm") + np.append( seam_length, Q_([0, 0], "mm")) v_weld = Q_(10, "mm/s") s_weld = (tcp_end_point - tcp_start_point)[0] # length of the weld t_weld = s_weld / v_weld t_start = pd.Timedelta("0s") t_end = pd.Timedelta(str(t_weld.to_base_units())) rot = WXRotation.from_euler(seq="x", angles=180, degrees=True) coords = [tcp_start_point.magnitude, tcp_end_point.magnitude] tcp_wire = lcs(coordinates=coords, orientation=rot, time=[t_start, t_end]) # add the workpiece coordinate system csm.add_cs( coordinate_system_name="tcp_wire", reference_system_name="workpiece", lcs=tcp_wire, ) tcp_contact = lcs(coordinates=[0, 0, -10]) # add the workpiece coordinate system csm.add_cs( coordinate_system_name="tcp_contact", reference_system_name="tcp_wire", lcs=tcp_contact, ) TCP_reference = csm.get_cs("tcp_contact", "workpiece") # Measurements # time time = pd.timedelta_range(start="0s", end="10s", freq="2s") # current data I_ts = sine(f=Q_(10, "1/s"), amp=Q_(20, "A"), bias=Q_(300, "A")) current_data = TimeSeries(I_ts.interp_time(time).data, time) # voltage data U_ts = sine(f=Q_(10, "1/s"), amp=Q_(3, "V"), bias=Q_(40, "V"), phase=Q_(0.1, "rad")) voltage_data = TimeSeries(U_ts.interp_time(time).data, time) # define current source and transformations src_current = msm.SignalSource( name="Current Sensor", output_signal=msm.Signal(signal_type="analog", units="V", data=None), error=msm.Error(Q_(0.1, "percent")), ) current_AD_transform = msm.SignalTransformation( name="AD conversion current measurement", error=msm.Error(Q_(0.01, "percent")), func=MathematicalExpression( "a * x + b", dict(a=Q_(32768.0 / 10.0, "1/V"), b=Q_(0.0, ""))), type_transformation="AD", ) current_calib_transform = msm.SignalTransformation( name="Calibration current measurement", error=msm.Error(0.0), func=MathematicalExpression( "a * x + b", dict(a=Q_(1000.0 / 32768.0, "A"), b=Q_(0.0, "A"))), ) # define voltage source and transformations src_voltage = msm.SignalSource( name="Voltage Sensor", output_signal=msm.Signal("analog", "V", data=None), error=msm.Error(Q_(0.1, "percent")), ) voltage_AD_transform = msm.SignalTransformation( name="AD conversion voltage measurement", error=msm.Error(Q_(0.01, "percent")), func=MathematicalExpression( "a * x + b", dict(a=Q_(32768.0 / 10.0, "1/V"), b=Q_(0.0, ""))), type_transformation="AD", ) voltage_calib_transform = msm.SignalTransformation( name="Calibration voltage measurement", error=msm.Error(0.0), func=MathematicalExpression( "a * x + b", dict(a=Q_(100.0 / 32768.0, "V"), b=Q_(0.0, "V"))), ) # Define lab equipment HKS_sensor = msm.MeasurementEquipment( name="HKS P1000-S3", sources=[src_current, src_voltage], ) BH_ELM = msm.MeasurementEquipment( name="Beckhoff ELM3002-0000", transformations=[current_AD_transform, voltage_AD_transform], ) twincat_scope = Software(name="Beckhoff TwinCAT ScopeView", version="3.4.3143") setattr(current_calib_transform, META_ATTR, dict(software=twincat_scope)) setattr(voltage_calib_transform, META_ATTR, dict(software=twincat_scope)) # Define current measurement chain welding_current_chain = msm.MeasurementChain.from_equipment( name="welding current measurement chain", equipment=HKS_sensor, source_name="Current Sensor", ) welding_current_chain.add_transformation_from_equipment( equipment=BH_ELM, transformation_name="AD conversion current measurement", ) welding_current_chain.add_transformation( transformation=current_calib_transform, data=current_data, ) # Define voltage measurement chain welding_voltage_chain = msm.MeasurementChain.from_equipment( name="welding voltage measurement chain", equipment=HKS_sensor, source_name="Voltage Sensor", ) welding_voltage_chain.add_transformation_from_equipment( equipment=BH_ELM, transformation_name="AD conversion voltage measurement", ) welding_voltage_chain.add_transformation( transformation=voltage_calib_transform, data=voltage_data, ) # Define measurements welding_current = msm.Measurement( name="welding current measurement", data=[current_data], measurement_chain=welding_current_chain, ) welding_voltage = msm.Measurement( name="welding voltage measurement", data=[voltage_data], measurement_chain=welding_voltage_chain, ) # GMAW Process params_pulse = dict( wire_feedrate=Q_(10.0, "m/min"), pulse_voltage=Q_(40.0, "V"), pulse_duration=Q_(5.0, "ms"), pulse_frequency=Q_(100.0, "Hz"), base_current=Q_(60.0, "A"), ) process_pulse = GmawProcess( "pulse", "CLOOS", "Quinto", params_pulse, tag="CLOOS/pulse", meta={"modulation": "UI"}, ) gas_comp = [ GasComponent("argon", Q_(82, "percent")), GasComponent("carbon dioxide", Q_(18, "percent")), ] gas_type = ShieldingGasType(gas_component=gas_comp, common_name="SG") gas_for_procedure = ShieldingGasForProcedure( use_torch_shielding_gas=True, torch_shielding_gas=gas_type, torch_shielding_gas_flowrate=Q_(20, "l / min"), ) process = dict( welding_process=process_pulse, shielding_gas=gas_for_procedure, weld_speed=TimeSeries(v_weld), welding_wire={"diameter": Q_(1.2, "mm")}, ) # ASDF file tree = dict( reference_timestamp=reference_timestamp, equipment=[HKS_sensor, BH_ELM], measurements=[welding_current, welding_voltage], welding_current=welding_current_chain.get_signal( "Calibration current measurement").data, welding_voltage=welding_voltage_chain.get_signal( "Calibration voltage measurement").data, coordinate_systems=csm, TCP=TCP_reference, workpiece=workpiece, process=process, ) tree[META_ATTR] = {"welder": "A.W. Elder"} model_path = get_schema_path("single_pass_weld-0.1.0.yaml") # pre-validate? write_read_buffer( tree, asdffile_kwargs=dict(custom_schema=str(model_path)), ) if out_file: with asdf.AsdfFile( tree, custom_schema=str(model_path), ) as ff: ff.write_to(out_file, all_array_storage="inline") else: return ( write_buffer( tree, asdffile_kwargs=dict(custom_schema=str(model_path)), write_kwargs=dict(all_array_storage="inline"), ), tree, )
class TestMeasurementChain: """Test the `MeasurementChain` class.""" # helper functions ----------------------------------------------------------------- @staticmethod def _default_source_kwargs(kwargs: dict = None) -> dict: """Update a dict with default keyword arguments to create a `SignalSource`.""" default_kwargs = dict( name="source", output_signal=Signal("analog", "V"), error=Error(0.01) ) if kwargs is not None: default_kwargs.update(kwargs) return default_kwargs @classmethod def _default_init_kwargs( cls, kwargs: dict = None, source_kwargs: dict = None ) -> dict: """Return a dictionary of keyword arguments required by the `__init__` method. Parameters ---------- kwargs : A dictionary containing some key word arguments that should replace the default ones. source_kwargs : A dictionary of key word arguments that should replace the arguments used for the default source. Returns ------- Dict : Dictionary with keyword arguments for the `__init__` method """ source_kwargs = cls._default_source_kwargs(source_kwargs) default_kwargs = dict( name="name", source=SignalSource(**source_kwargs), signal_data=[1, 3, 5], ) if kwargs is not None: default_kwargs.update(kwargs) return default_kwargs @staticmethod def _default_transformation(kwargs: dict = None) -> SignalTransformation: """Return a default `SignalTransformation`. Use the kwargs parameter to modify the default values. """ default_kwargs = dict( name="transformation", error=Error(0.1), func=MathematicalExpression("a*x", parameters={"a": Q_(1, "1/V")}), type_transformation="AD", ) if kwargs is not None: default_kwargs.update(kwargs) return SignalTransformation(**default_kwargs) @classmethod def _default_add_transformation_kwargs(cls, kwargs: dict = None) -> dict: """Update a dict with default keyword arguments to call `add_transformation`.""" default_kwargs = dict( transformation=cls._default_transformation(), error=Error(0.02), output_signal_type="digital", output_signal_unit="", ) if kwargs is not None: default_kwargs.update(kwargs) return default_kwargs # test_init ------------------------------------------------------------------------ @staticmethod @pytest.mark.parametrize( "kwargs, source_kwargs", [ ({}, {}), (dict(signal_data=None), dict(output_signal=Signal("analog", "V", [1]))), ], ) def test_init(kwargs: dict, source_kwargs: dict): """Test the `__init__` method of the `MeasurementChain`. Parameters ---------- kwargs: A dictionary with keyword arguments that are passed to the `__init__` method. Missing arguments are added. source_kwargs : A dictionary with keyword arguments that are used to construct the `SignalSource` that is passed to the `__init__` method. Missing arguments are added. """ kwargs = TestMeasurementChain._default_init_kwargs(kwargs, source_kwargs) MeasurementChain(**kwargs) # test_init_exceptions ------------------------------------------------------------- @staticmethod @pytest.mark.parametrize( "kwargs, source_kwargs, exception_type, test_name", [({}, {"output_signal": Signal("analog", "V", [1])}, KeyError, "# 2x data")], ids=get_test_name, ) def test_init_exceptions( kwargs: dict, source_kwargs: dict, exception_type, test_name: str ): """Test the exceptions of the `__init__` method. Parameters ---------- kwargs : A dictionary with keyword arguments that are passed to the `__init__` method. Missing arguments are added. source_kwargs : A dictionary with keyword arguments that are used to construct the `SignalSource` that is passed to the `__init__` method. Missing arguments are added. exception_type : The expected exception type test_name : Name of the test """ kwargs = TestMeasurementChain._default_init_kwargs(kwargs, source_kwargs) with pytest.raises(exception_type): MeasurementChain(**kwargs) # test_from_equipment -------------------------------------------------------------- @pytest.mark.parametrize( "num_sources, source_name, exception", [ (0, None, ValueError), (1, None, None), (2, "source_1", None), (2, "wrong name", KeyError), (2, None, ValueError), ], ) def test_from_equipment( self, num_sources: int, source_name: str, exception: Exception ): """Test the `from_equipment` factory and its exceptions. Parameters ---------- num_sources : Number of sources of the generated equipment source_name : Corresponding parameter of `from_equipment` exception : Expected exception """ sources = [ SignalSource(**self._default_source_kwargs({"name": f"source_{i}"})) for i in range(num_sources) ] equipment = MeasurementEquipment("Equipment", sources=sources) if exception is not None: with pytest.raises(exception): MeasurementChain.from_equipment( name="name", equipment=equipment, source_name=source_name ) else: MeasurementChain.from_equipment( name="name", equipment=equipment, source_name=source_name ) # test_add_transformations --------------------------------------------------------- @pytest.mark.parametrize( "tf_kwargs, exp_signal_type, exp_signal_unit", [ ({}, "digital", U_("")), (dict(type_transformation="AA"), "analog", U_("")), (dict(type_transformation=None), "analog", U_("")), (dict(func=None), "digital", U_("V")), ], ) def test_add_transformation(self, tf_kwargs, exp_signal_type, exp_signal_unit): """Test the `add_transformation` method of the `MeasurementChain`. Parameters ---------- tf_kwargs: A dictionary with keyword arguments that are used to construct the `SignalTransformation` that is passed to the `add_transformation` method. Missing arguments are added. exp_signal_type : The expected signal type after the transformation exp_signal_unit : The expected unit after the transformation """ mc = MeasurementChain(**self._default_init_kwargs()) mc.add_transformation(self._default_transformation(tf_kwargs)) signal = mc.output_signal assert signal.signal_type == exp_signal_type assert U_(signal.units) == exp_signal_unit # test_add_transformation_exceptions ----------------------------------------------- @pytest.mark.parametrize( "tf_kwargs, input_signal_source, exception_type, test_name", [ (dict(type_transformation="DA"), None, ValueError, "# inv. signal type #1"), (dict(type_transformation="DD"), None, ValueError, "# inv. signal type #2"), ({}, "not found", KeyError, "# invalid input signal source"), (dict(name="source"), None, KeyError, "# Name does already exist"), ( dict(func=MathematicalExpression("x+a", parameters={"a": Q_(1, "A")})), None, ValueError, "# incompatible function", ), ], ids=get_test_name, ) def test_add_transformation_exceptions( self, tf_kwargs: dict, input_signal_source: str, exception_type, test_name: str ): """Test the exceptions of the `add_transformation` method. Parameters ---------- tf_kwargs: A dictionary with keyword arguments that are used to construct the `SignalTransformation` that is passed to the `add_transformation` method. Missing arguments are added. input_signal_source : The value of the corresponding parameter of 'add_transformation' exception_type : The expected exception type test_name : Name of the test """ mc = MeasurementChain(**self._default_init_kwargs()) tf = self._default_transformation(tf_kwargs) with pytest.raises(exception_type): mc.add_transformation(tf, input_signal_source=input_signal_source) # test_add_transformation_from_equipment ------------------------------------------- @pytest.mark.parametrize( "num_transformations, transformation_name, exception", [ (0, None, ValueError), (1, None, None), (2, "transformation_1", None), (2, "wrong name", KeyError), (2, None, ValueError), ], ) def test_add_transformation_from_equipment( self, num_transformations: int, transformation_name: str, exception ): """Test `add_transformation_from_equipment` and its exceptions. Parameters ---------- num_transformations : Number of transformations of the generated equipment transformation_name : Corresponding parameter of `add_transformation_from_equipment` exception : Expected exception """ mc = MeasurementChain(**self._default_init_kwargs()) transformations = [ self._default_transformation({"name": f"transformation_{i}"}) for i in range(num_transformations) ] equipment = MeasurementEquipment(name="name", transformations=transformations) if exception is not None: with pytest.raises(exception): mc.add_transformation_from_equipment( equipment=equipment, transformation_name=transformation_name ) else: mc.add_transformation_from_equipment( equipment=equipment, transformation_name=transformation_name ) # test_add_signal_data ------------------------------------------------------------- @pytest.mark.parametrize( "kwargs", [ dict(data=xr.DataArray([2, 3])), dict(signal_source="source"), ], ) def test_add_signal_data(self, kwargs): """Test the `add_signal_data` method of the `MeasurementChain`. Parameters ---------- kwargs: A dictionary with keyword arguments that are passed to the `add_signal_data` method. If no name is in the kwargs, a default one is added. """ mc = MeasurementChain(**self._default_init_kwargs({"signal_data": None})) mc.add_transformation(self._default_transformation()) full_kwargs = dict(data=xr.DataArray([1, 2])) full_kwargs.update(kwargs) mc.add_signal_data(**full_kwargs) # test_add_signal_data_exceptions -------------------------------------------------- @pytest.mark.parametrize( "kwargs, exception_type, test_name", [ (dict(signal_source="what"), KeyError, "# invalid signal source"), (dict(signal_source="source"), KeyError, "# already has data #1"), (dict(signal_source="transformation"), KeyError, "# already has data #2"), ], ids=get_test_name, ) def test_add_signal_data_exceptions( self, kwargs: dict, exception_type, test_name: str ): """Test the exceptions of the `add_signal_data` method. Parameters ---------- kwargs : A dictionary with keyword arguments that are passed to the `add_signal_data` method. Missing arguments are added. exception_type : The expected exception type test_name : Name of the test """ mc = MeasurementChain(**self._default_init_kwargs()) mc.add_transformation(self._default_transformation(), data=[1, 2, 3]) mc.add_transformation( self._default_transformation( dict(name="transformation 2", type_transformation="DA") ) ) full_kwargs = dict(data=xr.DataArray([1, 2])) full_kwargs.update(kwargs) with pytest.raises(exception_type): mc.add_signal_data(**full_kwargs) # test_get_equipment --------------------------------------------------------------- @pytest.mark.parametrize( "signal_source, exception", [ ("source", None), ("transformation_1", None), ("transformation_2", None), ("transformation_3", KeyError), ], ) def test_get_equipment(self, signal_source, exception): """Test the `get_equipment` function and their exceptions. Parameters ---------- signal_source : Corresponding function parameter exception : Expected exception """ src_eq = MeasurementEquipment( "Source Eq", sources=[SignalSource(**self._default_source_kwargs())] ) tf_eq = MeasurementEquipment( "Transformation_eq", transformations=[ self._default_transformation({"name": "transformation_1"}) ], ) mc = MeasurementChain.from_equipment("Chain", src_eq) mc.add_transformation_from_equipment(tf_eq) mc.create_transformation("transformation_2", None, output_signal_unit="A") if exception is not None: with pytest.raises(exception): mc.get_equipment(signal_source=signal_source) else: mc.get_equipment(signal_source=signal_source) # test_get_signal_data ------------------------------------------------------------- def test_get_signal_data(self): """Test the `get_signal_data` method. This test assures that the returned data is identical to the one passed to the measurement chain and that a key error is raised if the requested data is not present. """ data = xr.DataArray([1, 2, 3]) mc = MeasurementChain(**self._default_init_kwargs()) mc.add_transformation(self._default_transformation(), data=data) mc.create_transformation("transformation_2", None, output_signal_unit="A") assert np.all(mc.get_signal_data("transformation") == data) # no data with pytest.raises(KeyError): mc.get_signal_data("transformation_2") # source not present with pytest.raises(KeyError): mc.get_signal_data("not found") # test_get_transformation ---------------------------------------------------------- def test_get_transformation(self): """Test the `get_transformation` method.""" mc = MeasurementChain(**self._default_init_kwargs()) mc.add_transformation(self._default_transformation()) transformation = mc.get_transformation("transformation") assert transformation == self._default_transformation() # test_get_transformation_exception ------------------------------------------------ def test_get_transformation_exception(self): """Test that a `KeyError` is raised if the transformation does not exist.""" mc = MeasurementChain(**self._default_init_kwargs()) mc.add_transformation(self._default_transformation()) with pytest.raises(KeyError): mc.get_transformation("not found")
def create_transformation( self, name: str, error: Error, output_signal_type: str = None, output_signal_unit: Union[str, Unit] = None, func: MathematicalExpression = None, data: TimeSeries = None, input_signal_source: str = None, ): """Create and add a transformation to the measurement chain. Parameters ---------- name : Name of the transformation error : The error of the transformation output_signal_type : Type of the output signal (analog or digital) output_signal_unit : Unit of the output signal. If a function is provided, it is not necessary to provide this parameter since it can be derived from the function. In case both, the function and the unit are provided, an exception is raised if a mismatch is dimensionality is detected. This functionality may be used as extra safety layer. If no function is provided, a simple unit conversion function is created. func : A function describing the transformation. The provided value interacts with the 'output_signal_unit' parameter as described in its documentation data : A set of measurement data that is associated with the output signal of the transformation input_signal_source : The source of the signal that should be used as input of the transformation. If `None` is provided, the name of the last added transformation (or the source, if no transformation was added to the chain) is used. Examples -------- >>> from weldx import Q_ >>> from weldx.core import MathematicalExpression >>> from weldx.measurement import Error, MeasurementChain, SignalTransformation >>> mc = MeasurementChain.from_parameters( ... name="Current measurement chain", ... source_error=Error(deviation=Q_(0.5, "percent")), ... source_name="Current sensor", ... output_signal_type="analog", ... output_signal_unit="V" ... ) Create a mathematical expression that accepts a quantity with volts as unit and that returns a dimentsionless quantity. >>> func = MathematicalExpression(expression="a*x + b", ... parameters=dict(a=Q_(5, "1/V"), b=Q_(1, "")) ... ) Use the mathematical expression to create a new transformation which also performs a analog-digital conversion. >>> mc.create_transformation(name="Current AD conversion", ... error=Error(deviation=Q_(1,"percent")), ... func=func, ... output_signal_type="digital" ... ) """ if output_signal_unit is not None: output_signal_unit = U_(output_signal_unit) if output_signal_type is None and output_signal_unit is None and func is None: warn("The created transformation does not perform any transformations.") input_signal_source = self._check_and_get_node_name(input_signal_source) input_signal: Signal = self._graph.nodes[input_signal_source]["signal"] if output_signal_type is None: output_signal_type = input_signal.signal_type type_tf = f"{input_signal.signal_type[0]}{output_signal_type[0]}".upper() if output_signal_unit is not None: if func is not None: if not output_signal_unit.is_compatible_with( self._determine_output_signal_unit(func, input_signal.units), ): raise ValueError( "The unit of the provided functions output has not the same " f"dimensionality as {output_signal_unit}" ) else: unit_conversion = output_signal_unit / input_signal.units func = MathematicalExpression( "a*x", parameters={"a": Q_(1, unit_conversion)}, ) transformation = SignalTransformation(name, error, func, type_tf) self.add_transformation(transformation, data, input_signal_source)
def test_evaluation(expression, parameters, variables, exp_result): """Test the evaluation of the mathematical function.""" expr = MathematicalExpression(expression=expression, parameters=parameters) assert np.all(expr.evaluate(**variables) == exp_result)
twincat_scope = Software(name="Beckhoff TwinCAT ScopeView", version="3.4.3143") src_current = msm.Source( name="Current Sensor", output_signal=msm.Signal(signal_type="analog", unit="V", data=None), error=msm.Error(Q_(0.1, "percent")), ) HKS_sensor.sources = [] HKS_sensor.sources.append(src_current) from weldx.core import MathematicalExpression [a, x, b] = sympy.symbols("a x b") current_AD_func = MathematicalExpression(a * x + b) current_AD_func.set_parameter("a", Q_(32768.0 / 10.0, "1/V")) current_AD_func.set_parameter("b", Q_(0.0, "")) current_AD_transform = msm.DataTransformation( name="AD conversion current measurement", input_signal=src_current.output_signal, output_signal=msm.Signal("digital", "", data=None), error=msm.Error(Q_(0.01, "percent")), func=current_AD_func, ) BH_ELM.data_transformations = [] BH_ELM.data_transformations.append(current_AD_transform) # define current output calibration expression and transformation
def ma_def() -> MathematicalExpression: """Get a default instance for tests.""" return MathematicalExpression( TestMathematicalExpression.expr_def, TestMathematicalExpression.params_def, )
def from_tree(cls, tree, ctx): obj = MathematicalExpression(sympy.sympify(tree["expression"]), parameters=tree["parameters"]) return obj
def test_construction_exceptions(expression, parameters, exception_type, name): """Test the exceptions of the '__init__' method.""" with pytest.raises(exception_type): MathematicalExpression(expression=expression, parameters=parameters)
def test_construction(self, expression, parameters, exp_vars): """Test the construction.""" expr = MathematicalExpression(expression=expression, parameters=parameters) self._check_params_and_vars(expr, parameters, exp_vars)