def read_inputs(self): """ Reads inputs from the configured input file. """ if self.input_file_path: # Reads input file reader = VariableIO(self.input_file_path) variables = reader.read() ivc = variables.to_ivc() # ivc will be added through add_subsystem, but we must use set_order() to # put it first. # So we need order of existing subsystem to provide the full order list to # set_order() to get order of systems, we use system_iter() that can be used # only after setup(). # But we will not be allowed to use add_subsystem() after setup(). # So we use setup() on a copy of current instance, and get order from this copy tmp_prob = deepcopy(self) tmp_prob.setup() previous_order = [ system.name for system in tmp_prob.model.system_iter(recurse=False) if system.name != "_auto_ivc" # OpenMDAO 3.2+ specific : _auto_ivc is an output of system_iter() but is not # accepted as input of set_order() ] self.model.add_subsystem(INPUT_SYSTEM_NAME, ivc, promotes=["*"]) self.model.set_order([INPUT_SYSTEM_NAME] + previous_order)
def test_v_n_diagram_openvsp(): # load all inputs reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() input_vars.add_output("data:aerodynamics:aircraft:landing:CL_max", 1.9) input_vars.add_output("data:aerodynamics:wing:low_speed:CL_max_clean", 1.5) input_vars.add_output("data:aerodynamics:wing:low_speed:CL_min_clean", -1.5) input_vars.add_output("data:aerodynamics:aircraft:mach_interpolation:CL_alpha_vector", [5.51, 5.51, 5.56, 5.63, 5.71, 5.80]) input_vars.add_output("data:aerodynamics:aircraft:mach_interpolation:mach_vector", [0., 0.15, 0.21, 0.27, 0.33, 0.39]) input_vars.add_output("data:weight:aircraft:MTOW", 1633.0, units="kg") input_vars.add_output("data:aerodynamics:cruise:mach", 0.2488) input_vars.add_output("data:aerodynamics:wing:cruise:induced_drag_coefficient", 0.048) input_vars.add_output("data:aerodynamics:aircraft:cruise:CD0", 0.02733) register_wrappers() # Run problem with VLM and check obtained value(s) is/(are) correct # noinspection PyTypeChecker problem = run_system(ComputeVNopenvspNoVH(propulsion_id=ENGINE_WRAPPER, compute_cl_alpha=True), input_vars) velocity_vect = np.array( [35.986, 35.986, 70.15, 44.367, 0., 0., 83.942, 83.942, 83.942, 117.265, 117.265, 117.265, 117.265, 105.538, 83.942, 0., 31.974, 45.219, 57.554] ) load_factor_vect = np.array( [1., -1., 3.8, -1.52, 0., 0., -1.52, 3.874, -1.874, 3.8, 0., 3.05, -1.05, 0., 0., 0., 1., 2., 2.] ) velocity_array = problem.get_val("data:flight_domain:velocity", units="m/s") load_factor_array = problem["data:flight_domain:load_factor"] assert np.max(np.abs(velocity_vect - velocity_array)) <= 1e-3 assert np.max(np.abs(load_factor_vect - load_factor_array)) <= 1e-3
def get_indep_var_comp(var_names: List[str], test_file: str, xml_file_name: str) -> om.IndepVarComp: """ Reads required input data from xml file and returns an IndepVarcomp() instance""" reader = VariableIO(pth.join(pth.dirname(test_file), "data", xml_file_name)) reader.path_separator = ":" ivc = reader.read(only=var_names).to_ivc() return ivc
def test_takeoff_phase_connections(): """ Tests complete take-off phase connection with speeds """ # load all inputs reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" ivc = reader.read().to_ivc() register_wrappers() # noinspection PyTypeChecker problem = run_system(TakeOffPhase(propulsion_id=ENGINE_WRAPPER), ivc) vr = problem.get_val("data:mission:sizing:takeoff:VR", units='m/s') assert vr == pytest.approx(36.28, abs=1e-2) vloff = problem.get_val("data:mission:sizing:takeoff:VLOF", units='m/s') assert vloff == pytest.approx(42.52, abs=1e-2) v2 = problem.get_val("data:mission:sizing:takeoff:V2", units='m/s') assert v2 == pytest.approx(47.84, abs=1e-2) tofl = problem.get_val("data:mission:sizing:takeoff:TOFL", units='m') assert tofl == pytest.approx(341.49, abs=1) duration = problem.get_val("data:mission:sizing:takeoff:duration", units='s') assert duration == pytest.approx(20.5, abs=1e-1) fuel1 = problem.get_val("data:mission:sizing:takeoff:fuel", units='kg') assert fuel1 == pytest.approx(0.246, abs=1e-2) fuel2 = problem.get_val("data:mission:sizing:initial_climb:fuel", units='kg') assert fuel2 == pytest.approx(0.075, abs=1e-2)
def test_complete_cg(): """ Run computation of all models """ # with data from file reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() input_vars.add_output("data:weight:propulsion:unusable_fuel:mass", 20.0, units="kg") # Run problem and check obtained value(s) is/(are) correct # noinspection PyTypeChecker problem = run_system(CG(propulsion_id=ENGINE_WRAPPER), input_vars, check=True) cg_global = problem.get_val("data:weight:aircraft:CG:aft:x", units="m") assert cg_global == pytest.approx(2.5800, abs=1e-3) cg_ratio = problem.get_val("data:weight:aircraft:CG:aft:MAC_position") assert cg_ratio == pytest.approx(0.1917, abs=1e-3) z_cg_empty_ac = problem.get_val("data:weight:aircraft_empty:CG:z", units="m") assert z_cg_empty_ac == pytest.approx(1.266, abs=1e-3) z_cg_b1 = problem.get_val("data:weight:propulsion:engine:CG:z", units="m") assert z_cg_b1 == pytest.approx(1.255, abs=1e-2)
def write_needed_inputs( self, source_file_path: str = None, source_formatter: IVariableIOFormatter = None, ): """ Writes the input file of the problem with unconnected inputs of the configured problem. Written value of each variable will be taken: 1. from input_data if it contains the variable 2. from defined default values in component definitions :param source_file_path: if provided, variable values will be read from it :param source_formatter: the class that defines format of input file. if not provided, expected format will be the default one. """ problem = self.get_problem(read_inputs=False) problem.setup() variables = VariableList.from_unconnected_inputs( problem, with_optional_inputs=True) if source_file_path: ref_vars = VariableIO(source_file_path, source_formatter).read() variables.update(ref_vars) for var in variables: var.is_input = True writer = VariableIO(problem.input_file_path) writer.write(variables)
def write_needed_inputs( self, source_file_path: str = None, source_formatter: IVariableIOFormatter = None, ): """ Writes the input file of the problem with unconnected inputs of the problem. .. warning:: :meth:`setup` must have been run on this Problem instance. Written value of each variable will be taken: 1. from input_data if it contains the variable 2. from defined default values in component definitions WARNING: if inputs have already been read, they won't be needed any more and won't be in written file. :param source_file_path: if provided, variable values will be read from it :param source_formatter: the class that defines format of input file. if not provided, expected format will be the default one. """ variables = VariableList.from_unconnected_inputs( self, with_optional_inputs=True) if source_file_path: ref_vars = VariableIO(source_file_path, source_formatter).read() variables.update(ref_vars) for var in variables: var.is_input = True writer = VariableIO(self.input_file_path) writer.write(variables)
def test_mission_group_breguet_with_loop(cleanup): input_file_path = pth.join(DATA_FOLDER_PATH, "test_mission.xml") vars = VariableIO(input_file_path).read() del vars["data:mission:operational:TOW"] ivc = vars.to_ivc() problem = run_system( Mission( propulsion_id="test.wrapper.propulsion.dummy_engine", out_file=pth.join(RESULTS_FOLDER_PATH, "test_looped_mission_group.csv"), use_initializer_iteration=True, mission_file_path=pth.join(DATA_FOLDER_PATH, "test_breguet.yml"), add_solver=True, ), ivc, ) # check loop assert_allclose( problem["data:mission:operational:TOW"], problem["data:mission:operational:OWE"] + problem["data:mission:operational:payload"] + problem["data:mission:operational:block_fuel"], atol=1.0, ) assert_allclose( problem["data:mission:operational:needed_block_fuel"], problem["data:mission:operational:block_fuel"], atol=1.0, ) assert_allclose( problem["data:mission:operational:needed_block_fuel"], 5640.0, atol=1.0, )
def test_mission_group_without_loop(cleanup): input_file_path = pth.join(DATA_FOLDER_PATH, "test_mission.xml") vars = VariableIO(input_file_path).read() ivc = vars.to_ivc() with pytest.raises(FastMissionFileMissingMissionNameError): run_system( Mission( propulsion_id="test.wrapper.propulsion.dummy_engine", out_file=pth.join(RESULTS_FOLDER_PATH, "test_unlooped_mission_group.csv"), use_initializer_iteration=False, mission_file_path=pth.join(DATA_FOLDER_PATH, "test_mission.yml"), adjust_fuel=False, ), ivc, ) problem = run_system( Mission( propulsion_id="test.wrapper.propulsion.dummy_engine", out_file=pth.join(RESULTS_FOLDER_PATH, "test_unlooped_mission_group.csv"), use_initializer_iteration=False, mission_file_path=pth.join(DATA_FOLDER_PATH, "test_mission.yml"), mission_name="operational", adjust_fuel=False, ), ivc, ) assert_allclose(problem["data:mission:operational:needed_block_fuel"], 6589.0, atol=1.0) assert_allclose(problem["data:mission:operational:block_fuel"], 15000.0, atol=1.0)
def get_indep_var_comp(var_names): """ Reads required input data and returns an IndepVarcomp() instance""" reader = VariableIO( pth.join(pth.dirname(__file__), "data", "mass_breakdown_inputs.xml")) reader.path_separator = ":" ivc = reader.read(only=var_names).to_ivc() return ivc
def test_high_speed_connection(): """ Tests high speed components connection """ # load all inputs reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() register_wrappers() # Run problem with VLM and check obtained value(s) is/(are) correct # noinspection PyTypeChecker problem = run_system(AerodynamicsHighSpeed(propulsion_id=ENGINE_WRAPPER), input_vars, check=True) cd0 = problem["data:aerodynamics:aircraft:cruise:CD0"] assert cd0 == pytest.approx(0.0198, abs=1e-4) coef_k = problem["data:aerodynamics:aircraft:cruise:induced_drag_coefficient"] assert coef_k == pytest.approx(0.0528, abs=1e-4) cl_alpha_wing = problem.get_val("data:aerodynamics:aircraft:cruise:CL_alpha", units="rad**-1") assert cl_alpha_wing == pytest.approx(4.820, abs=1e-3) cl_alpha_htp = problem.get_val("data:aerodynamics:horizontal_tail:cruise:CL_alpha", units="rad**-1") assert cl_alpha_htp == pytest.approx(0.6260, abs=1e-4) cl_alpha_vtp = problem.get_val("data:aerodynamics:vertical_tail:cruise:CL_alpha", units="rad**-1") assert cl_alpha_vtp == pytest.approx(2.8553, abs=1e-4) # Run problem with OPENVSP and check change(s) is/(are) correct # noinspection PyTypeChecker problem = run_system(AerodynamicsHighSpeed(propulsion_id=ENGINE_WRAPPER, use_openvsp=True), input_vars, check=True) coef_k = problem["data:aerodynamics:aircraft:cruise:induced_drag_coefficient"] assert coef_k == pytest.approx(0.0487, abs=1e-4) cl_alpha_wing = problem.get_val("data:aerodynamics:aircraft:cruise:CL_alpha", units="rad**-1") assert cl_alpha_wing == pytest.approx(4.536, abs=1e-3) cl_alpha_htp = problem.get_val("data:aerodynamics:horizontal_tail:cruise:CL_alpha", units="rad**-1") assert cl_alpha_htp == pytest.approx(0.7030, abs=1e-4) cl_alpha_vtp = problem.get_val("data:aerodynamics:vertical_tail:cruise:CL_alpha", units="rad**-1") assert cl_alpha_vtp == pytest.approx(2.8553, abs=1e-4)
def write_outputs(self): """ Writes all outputs in the configured output file. """ if self.output_file_path: writer = VariableIO(self.output_file_path) variables = VariableList.from_problem(self) writer.write(variables)
def convert_xml(file_path: str, translator: VarXpathTranslator): """ Modifies given XML file by translating XPaths according to provided translator :param file_path: :param translator: """ reader = VariableIO(file_path, formatter=VariableXmlBaseFormatter(translator)) vars = reader.read() VariableIO(file_path).write(vars)
def test_compute_static_margin(): """ Tests computation of static margin """ reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() input_vars.add_output("data:weight:aircraft:CG:aft:MAC_position", 0.20) problem = run_system(ComputeStaticMargin(), input_vars) static_margin = problem["data:handling_qualities:static_margin"] assert static_margin == pytest.approx(0.55, rel=1e-2)
def test_evaluate_oew(): """ Tests a simple evaluation of Operating Empty Weight from sample XML data. """ reader = VariableIO( pth.join(pth.dirname(__file__), "data", "mass_breakdown_inputs.xml")) reader.path_separator = ":" input_vars = reader.read().to_ivc() mass_computation = run_system(OperatingWeightEmpty(), input_vars) oew = mass_computation["data:weight:aircraft:OWE"] assert oew == pytest.approx(41591, abs=1)
def test_evaluate_owe(): """ Tests a simple evaluation of Operating Weight Empty from sample XML data. """ reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() # noinspection PyTypeChecker mass_computation = run_system( ComputeOperatingWeightEmpty(propulsion_id=ENGINE_WRAPPER), input_vars) oew = mass_computation.get_val("data:weight:aircraft:OWE", units="kg") assert oew == pytest.approx(1031.500, abs=1)
def test_balked_landing_limit(): """ Tests the computation of the forward most possible CG location for a balked landing in case HTP area is fixed""" reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() input_vars.add_output("data:geometry:horizontal_tail:area", 3.78) problem = run_system(ComputeBalkedLandingLimit(propulsion_id=ENGINE_WRAPPER), input_vars) balked_landing_limit = problem["data:handling_qualities:balked_landing_limit:x"] assert balked_landing_limit == pytest.approx(3.43, rel=1e-2) balked_landing_limit_ratio = problem["data:handling_qualities:balked_landing_limit:MAC_position"] assert balked_landing_limit_ratio == pytest.approx(0.24, rel=1e-2)
def test_compute_to_rotation_limit(): """ Tests the computation of the forward most possible CG location for the TO rotation in case HTP area is fixed""" reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() input_vars.add_output("data:geometry:horizontal_tail:area", 3.78) problem = run_system(ComputeTORotationLimitGroup(propulsion_id=ENGINE_WRAPPER), input_vars) to_rotation_limit = problem["data:handling_qualities:to_rotation_limit:x"] assert to_rotation_limit == pytest.approx(2.99, rel=1e-2) to_rotation_limit_ratio = problem["data:handling_qualities:to_rotation_limit:MAC_position"] assert to_rotation_limit_ratio == pytest.approx(-0.0419, rel=1e-2)
def test_complete_cg(): """ Run computation of all models """ # with data from file reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() # Run problem and check obtained value(s) is/(are) correct problem = run_system(CG(), input_vars, check=True) cg_global = problem.get_val("data:weight:aircraft:CG:aft:x", units="m") assert cg_global == pytest.approx(3.46, abs=1e-1) cg_ratio = problem.get_val("data:weight:aircraft:CG:aft:MAC_position") assert cg_ratio == pytest.approx(0.20, abs=1e-2)
def test_loop_compute_oew(): """ Tests a weight computation loop matching the max payload criterion. """ # With payload from npax reader = VariableIO( pth.join(pth.dirname(__file__), "data", "mass_breakdown_inputs.xml")) reader.path_separator = ":" input_vars = reader.read(ignore=[ "data:weight:aircraft:MLW", "data:weight:aircraft:MZFW", "data:weight:aircraft:max_payload", ]).to_ivc() mass_computation = run_system(MassBreakdown(), input_vars) oew = mass_computation["data:weight:aircraft:OWE"] assert oew == pytest.approx(41591, abs=1) # with payload as input reader = VariableIO( pth.join(pth.dirname(__file__), "data", "mass_breakdown_inputs.xml")) reader.path_separator = ":" input_vars = reader.read(ignore=[ "data:weight:aircraft:MLW", "data:weight:aircraft:MZFW", ]).to_ivc() mass_computation = run_system(MassBreakdown(payload_from_npax=False), input_vars) oew = mass_computation["data:weight:aircraft:OWE"] assert oew == pytest.approx(42060, abs=1)
def _data_weight_decomposition(variables: VariableIO, owe=None): """ Returns the two level weight decomposition of MTOW and optionally the decomposition of owe subcategories. :param variables: instance containing variables information :param owe: value of OWE, if provided names of owe subcategories will be provided :return: variable values, names and optionally owe subcategories names """ category_values = [] category_names = [] owe_subcategory_names = [] for variable in variables.names(): name_split = variable.split(":") if isinstance(name_split, list) and len(name_split) == 4: if name_split[0] + name_split[1] + name_split[ 3] == "dataweightmass" and not ("aircraft" in name_split[2]): category_values.append( convert_units(variables[variable].value[0], variables[variable].units, "kg")) category_names.append(name_split[2]) if owe: owe_subcategory_names.append( name_split[2] + "<br>" + str(int(variables[variable].value[0])) + " [kg] (" + str(round(variables[variable].value[0] / owe * 100, 1)) + "%)") if owe: result = category_values, category_names, owe_subcategory_names else: result = category_values, category_names, None return result
def write_outputs(self): """ Writes all outputs in the configured output file. """ if self.output_file_path: writer = VariableIO(self.output_file_path) if self.additional_variables is None: self.additional_variables = [] variables = VariableList(self.additional_variables) for var in variables: var.is_input = None variables.update( VariableList.from_problem(self, promoted_only=True), add_variables=True ) writer.write(variables)
def input_xml() -> VariableIO: """ :return: access to the sample xml data """ # TODO: have more consistency in input data (no need for the whole geometry_inputs_full.xml) return VariableIO( pth.join(pth.dirname(__file__), "data", "geometry_inputs_full.xml"))
def test_update_vt_area(): """ Tests computation of the vertical tail area """ # Research independent input value in .xml file reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() input_vars.add_output("data:aerodynamics:fuselage:cruise:CnBeta", -0.0599) # Run problem and check obtained value(s) is/(are) correct register_wrappers() problem = run_system(UpdateVTArea(propulsion_id=ENGINE_WRAPPER), input_vars) vt_area = problem.get_val("data:geometry:vertical_tail:area", units="m**2") assert vt_area == pytest.approx( 1.751, abs=1e-2) # old-version obtained value 2.4m²
def test_compute_to_rotation_limit(): """ Tests computation of static margin """ reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() problem = run_system( ComputeTORotationLimitGroup(propulsion_id=ENGINE_WRAPPER), input_vars) x_cg_rotation_limit = problem[ "data:handling_qualities:to_rotation_limit:x"] assert x_cg_rotation_limit == pytest.approx(1.9355, rel=1e-2) x_cg_ratio_rotation_limit = problem[ "data:handling_qualities:to_rotation_limit:MAC_position"] assert x_cg_ratio_rotation_limit == pytest.approx(-0.3451, rel=1e-2)
def test_compute_balked_landing(): """ Tests computation of static margin """ reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() problem = run_system( ComputeBalkedLandingLimit(propulsion_id=ENGINE_WRAPPER), input_vars) x_cg_balked_landing_limit = problem[ "data:handling_qualities:balked_landing_limit:x"] assert x_cg_balked_landing_limit == pytest.approx(2.0738, rel=1e-2) x_cg_ratio_balked_landing_limit = problem[ "data:handling_qualities:balked_landing_limit:MAC_position"] assert x_cg_ratio_balked_landing_limit == pytest.approx(-0.23, rel=1e-2)
def mass_breakdown_bar_plot( aircraft_file_path: str, name=None, fig=None, file_formatter=None ) -> go.FigureWidget: """ Returns a figure plot of the aircraft mass breakdown using bar plots. Different designs can be superposed by providing an existing fig. Each design can be provided a name. :param aircraft_file_path: path of data file :param name: name to give to the trace added to the figure :param fig: existing figure to which add the plot :param file_formatter: the formatter that defines the format of data file. If not provided, default format will be assumed. :return: bar plot figure """ variables = VariableIO(aircraft_file_path, file_formatter).read() var_names_and_new_units = { "data:weight:aircraft:MTOW": "kg", "data:weight:aircraft:OWE": "kg", "data:weight:aircraft:payload": "kg", "data:mission:sizing:fuel": "kg", } # pylint: disable=unbalanced-tuple-unpacking # It is balanced for the parameters provided mtow, owe, payload, fuel_mission = _get_variable_values_with_new_units( variables, var_names_and_new_units ) if fig is None: fig = make_subplots( rows=1, cols=2, subplot_titles=("Maximum Take-Off Weight Breakdown", "Overall Weight Empty Breakdown"), ) # Same color for each aircraft configuration i = len(fig.data) weight_labels = ["MTOW", "OWE", "Fuel - Mission", "Payload"] weight_values = [mtow, owe, fuel_mission, payload] fig.add_trace( go.Bar(name="", x=weight_labels, y=weight_values, marker_color=COLS[i], showlegend=False), row=1, col=1, ) # Get data:weight decomposition main_weight_values, main_weight_names, _ = _data_weight_decomposition(variables, owe=None) fig.add_trace( go.Bar(name=name, x=main_weight_names, y=main_weight_values, marker_color=COLS[i]), row=1, col=2, ) fig.update_layout(yaxis_title="[kg]") return fig
def load(self, file_path: str, file_formatter: IVariableIOFormatter = None): """ Loads the file and stores its data. :param file_path: the path of file to interact with :param file_formatter: the formatter that defines file format. If not provided, default format will be assumed. """ self.file = file_path self.load_variables(VariableIO(file_path, file_formatter).read())
def test_low_speed_connection(): """ Tests low speed components connection """ # Clear saved polar results (for wing and htp airfoils) clear_polar_results() # load all inputs reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() register_wrappers() # Run problem with VLM # noinspection PyTypeChecker run_system(AerodynamicsLowSpeed(propulsion_id=ENGINE_WRAPPER, use_openvsp=False), input_vars) # Run problem with OPENVSP # noinspection PyTypeChecker run_system(AerodynamicsLowSpeed(propulsion_id=ENGINE_WRAPPER, use_openvsp=True), input_vars)
def test_compute_static_margin(): """ Tests computation of static margin """ reader = VariableIO(pth.join(pth.dirname(__file__), "data", XML_FILE)) reader.path_separator = ":" input_vars = reader.read().to_ivc() problem = run_system(ComputeStaticMargin(), input_vars) stick_fixed_static_margin = problem[ "data:handling_qualities:stick_fixed_static_margin"] assert stick_fixed_static_margin == pytest.approx(0.0479, rel=1e-2) free_elevator_factor = problem[ "data:aerodynamics:cruise:neutral_point:free_elevator_factor"] assert free_elevator_factor == pytest.approx(0.7217, rel=1e-2) stick_free_static_margin = problem[ "data:handling_qualities:stick_free_static_margin"] assert stick_free_static_margin == pytest.approx(-0.0253, rel=1e-2)