def __init__( self, parent, gravity=-9.81, gamma=0.7, initial_velocity=None, initial_position=None, ): Block.__init__(self, parent) self.position = State( self, shape=2, derivative_function=self.position_derivative, initial_condition=initial_position, ) self.velocity = State( self, shape=2, derivative_function=self.velocity_derivative, initial_condition=initial_velocity, ) self.ground = ZeroCrossEventSource(self, event_function=self.ground_event, direction=-1) self.ground.register_listener(self.on_ground_event) self.gravity = gravity self.gamma = gamma
def __init__( self, parent, motor_constant, resistance, inductance, moment_of_inertia, initial_omega=0, initial_current=0, ): Block.__init__(self, parent) self.motor_constant = motor_constant self.resistance = resistance self.inductance = inductance self.moment_of_inertia = moment_of_inertia self.voltage = Port() self.external_torque = Port() self.omega = State( self, derivative_function=self.omega_dot, initial_condition=initial_omega, ) self.current = State( self, derivative_function=self.current_dot, initial_condition=initial_current, )
def test_multi_time_system_state(): """Test the system state with multiple time instances""" system = System() state_1 = State(owner=system) state_2 = State(owner=system, shape=2) input_1 = InputSignal(owner=system) input_2 = InputSignal(owner=system, shape=2) times = np.r_[0.0, 1.0, 2.0] states = np.empty(shape=(system.num_states, times.shape[0])) inputs = np.empty(shape=(system.num_inputs, times.shape[0])) states[state_1.state_slice] = (1.0, 2.0, 3.0) states[state_2.state_slice] = ((1.0, 2.0, 3.0), (4.0, 5.0, 6.0)) inputs[input_1.input_slice] = (1.0, 2.0, 3.0) inputs[input_2.input_slice] = ((1.0, 2.0, 3.0), (4.0, 5.0, 6.0)) system_state = SystemState(system=system, time=times, state=states, inputs=inputs) npt.assert_equal(system_state[state_1], (1.0, 2.0, 3.0)) npt.assert_equal(system_state[state_2], ((1.0, 2.0, 3.0), (4.0, 5.0, 6.0))) npt.assert_equal(system_state[input_1], (1.0, 2.0, 3.0)) npt.assert_equal(system_state[input_2], ((1.0, 2.0, 3.0), (4.0, 5.0, 6.0)))
def pendulum(length=1): """Create a steady-state model for a pendulum""" system = System() g = 9.81 # Gravity def omega_dt(data): """Calculate the acceleration of the pendulum""" return np.sin(phi(data)) * g / length def phi_dt(data): """Calculate the velocity of the pendulum""" return omega(data) omega = State(system, derivative_function=omega_dt) phi = State(system, derivative_function=phi_dt) # Set up a steady-state configuration config = SteadyStateConfiguration(system) # Minimize the total energy config.objective = lambda data: (g * length * (1 - np.cos(phi(data))) + (length * omega(data))**2) # Constrain the angular velocity (no steady state) config.states[omega].steady_state = False # Constrain the angle config.states[phi].steady_state = False config.states[phi].lower_bounds = -np.pi config.states[phi].upper_bounds = np.pi return config
def __init__( self, parent, motor_constant, resistance, inductance, moment_of_inertia, initial_speed=None, initial_current=None, ): Block.__init__(self, parent) self.motor_constant = motor_constant self.resistance = resistance self.inductance = inductance self.moment_of_inertia = moment_of_inertia # Create the velocity and current state # These can also be used as signals which export the exact value of # the respective state. self.omega = State( self, derivative_function=self.omega_dt, initial_condition=initial_speed, ) self.current = State( self, derivative_function=self.current_dt, initial_condition=initial_current, ) # Create (input) ports for voltage and external torque load self.voltage = Port() self.external_torque = Port()
def test_system(): """Test the ``System`` class""" system = System() state_a = State( system, shape=(3, 3), initial_condition=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], derivative_function=None, ) state_b = State( system, shape=3, initial_condition=[10, 11, 12], derivative_function=None, ) input_c = InputSignal(system, value=1) input_d = InputSignal(system, shape=2, value=[2, 3]) event_a = ZeroCrossEventSource(system, event_function=None) event_b = ZeroCrossEventSource(system, event_function=None) # Check the counts assert system.num_inputs == input_c.size + input_d.size assert system.num_states == state_a.size + state_b.size assert system.num_events == 2 # Check the initial condition # This also checks that state indices are disjoint. initial_condition = system.initial_condition npt.assert_almost_equal( initial_condition[state_a.state_slice], state_a.initial_condition.ravel(), ) npt.assert_almost_equal( initial_condition[state_b.state_slice], state_b.initial_condition.ravel(), ) # Check the initial inputs # This also checks that input indices are disjoint. initial_input = system.initial_input npt.assert_almost_equal(initial_input[input_c.input_slice], np.atleast_1d(input_c.value).ravel()) npt.assert_almost_equal(initial_input[input_d.input_slice], np.atleast_1d(input_d.value).ravel()) # Check that event indices are disjoint assert event_a.event_index != event_b.event_index
def test_state_access(): """Test whether calling a ``State`` object calls the ``get_state_value`` and ``set_state_value`` methods of the provider object""" system = System() state = State(system) provider = Mock() # Check read access state(provider) provider.get_state_value.assert_called_with(state) # Check write access provider.reset_mock() state.set_value(provider, 10) provider.set_state_value.assert_called_with(state, 10)
def test_system_state_dictionary_access(): """Test the deprecated dictionary access for system states""" system = System() state_a = State(system, initial_condition=10) state_b = State(system, shape=2, initial_condition=[11, 12]) state_c = State(system, shape=(2, 2), initial_condition=[[13, 14], [15, 16]]) system_state = SystemState(time=0, system=system) npt.assert_equal(system_state[state_a], state_a(system_state)) npt.assert_equal(system_state[state_b], state_b(system_state)) npt.assert_equal(system_state[state_b, 0], state_b(system_state)[0]) npt.assert_equal(system_state[state_b, 1], state_b(system_state)[1]) npt.assert_equal(system_state[state_c, 0, 0], state_c(system_state)[0, 0])
def __init__( self, owner, mass, moment_of_inertia, initial_velocity_earth=None, initial_position_earth=None, initial_transformation=None, initial_angular_rates_earth=None, ): Block.__init__(self, owner) self.mass = mass self.moment_of_inertia = moment_of_inertia self.forces_body = Port(shape=3) self.moments_body = Port(shape=3) if initial_transformation is None: initial_transformation = np.eye(3) self.velocity_earth = State( self, shape=3, derivative_function=self.velocity_earth_dot, initial_condition=initial_velocity_earth, ) self.position_earth = State( self, shape=3, derivative_function=self.position_earth_dot, initial_condition=initial_position_earth, ) self.omega_earth = State( self, shape=3, derivative_function=self.omega_earth_dot, initial_condition=initial_angular_rates_earth, ) self.dcm = State( self, shape=(3, 3), derivative_function=self.dcm_dot, initial_condition=initial_transformation, )
def test_state_constraint_access(): """Test access to state constraint properties""" system = System() state_1 = State(system) state_2 = State(system) config = SteadyStateConfiguration(system) config.states[state_1].lower_bounds = 10 config.states[state_2].lower_bounds = 20 config.states[state_1].upper_bounds = 15 config.states[state_2].upper_bounds = 25 config.states[state_2].steady_state = False config.states[state_1].initial_condition = 100 npt.assert_equal(config.states[state_1].lower_bounds, 10) npt.assert_equal(config.states[state_2].lower_bounds, 20) npt.assert_equal(config.states[state_1].upper_bounds, 15) npt.assert_equal(config.states[state_2].upper_bounds, 25) assert not config.states[state_2].steady_state npt.assert_equal(config.states[state_1].initial_condition, 100)
def test_simulation_result_dictionary_access(): """Test the deprecated dictionary access for simulation results""" system = System() state_a = State(system, initial_condition=10) state_b = State(system, shape=2, initial_condition=[11, 12]) state_c = State(system, shape=(2, 2), initial_condition=[[13, 14], [15, 16]]) system_state = SystemState(time=0, system=system) result = SimulationResult(system) result.append(system_state) npt.assert_equal(result[state_a], state_a(result)) npt.assert_equal(result[state_b], state_b(result)) npt.assert_equal(result[state_b, 0], state_b(result)[0]) npt.assert_equal(result[state_b, 1], state_b(result)[1]) npt.assert_equal(result[state_c, 0, 0], state_c(result)[0, 0])
def __init__(self, owner, shape=1, initial_condition=None): """ Constructor for ``ZeroOrderHold`` Args: owner: The owner of the block (system or block) shape: The shape of the input and output signal initial_condition: The initial state of the sampling output (before the first tick of the block) """ Block.__init__(self, owner) self.event_input = EventPort(self) self.event_input.register_listener(self.update_state) self.input = Port(shape=shape) self.output = State( self, shape=shape, initial_condition=initial_condition, derivative_function=None, )
class ZeroOrderHold(Block): """A zero-order-hold block which samples an input signal when the connected event occurs. The block provides an event port ``event_input`` that should be connected to the event source that shall trigger the sampling. """ def __init__(self, owner, shape=1, initial_condition=None): """ Constructor for ``ZeroOrderHold`` Args: owner: The owner of the block (system or block) shape: The shape of the input and output signal initial_condition: The initial state of the sampling output (before the first tick of the block) """ Block.__init__(self, owner) self.event_input = EventPort(self) self.event_input.register_listener(self.update_state) self.input = Port(shape=shape) self.output = State( self, shape=shape, initial_condition=initial_condition, derivative_function=None, ) def update_state(self, data): """Update the state on a clock event Args: data: The time, states and signals of the system """ self.output.set_value(data, self.input(data))
def test_state(): """Test the ``State`` class""" system = System() state_a = State(system, derivative_function=(lambda data: 0)) state_b = State(system, shape=3, derivative_function=(lambda data: np.zeros(3))) state_c = State(system, shape=(3, 3), derivative_function=(lambda data: np.zeros(3, 3))) state_d = State(system, derivative_function=(lambda data: 0), initial_condition=1) # Check the sizes assert state_a.size == 1 assert state_b.size == 3 assert state_c.size == 9 assert state_d.size == 1 # Test the slice property assert state_c.state_slice == slice(state_c.state_index, state_c.state_index + state_c.size)
def water_tank_model( inflow_area=0.01, outflow_area=0.02, tank_area=0.2, target_height=5, initial_condition=None, initial_guess=None, max_inflow_velocity=None, ): """Create a steady-state configuration for a water tank""" g = 9.81 # Gravity # Create a new system system = System() # Model the inflow inflow_velocity = InputSignal(system) # Model the height state def height_derivative(data): """Calculate the time derivative of the height""" return (inflow_area * inflow_velocity(data) - outflow_area * np.sqrt(2 * g * height_state(data))) / tank_area height_state = State(system, derivative_function=height_derivative) # Set up the steady-state configuration config = SteadyStateConfiguration(system) # Enforce the inflow to be non-negative config.inputs[inflow_velocity].lower_bounds = 0 if max_inflow_velocity is not None: config.inputs[inflow_velocity].upper_bounds = max_inflow_velocity # Define the target height config.states[height_state].lower_bounds = target_height config.states[height_state].upper_bounds = target_height if initial_condition is not None: config.states[height_state].initial_condition = initial_condition if initial_guess is not None: config.inputs[inflow_velocity].initial_guess = initial_guess return config
def test_system_state_updater_dictionary_access(): """Test the deprecated dictionary access for simulation results""" system = System() counter = State(system, shape=(2, 2)) clock = Clock(system, 1.0) def _update_counter(system_state): old_val = counter(system_state).copy() system_state[counter] += 1 system_state[counter, 0] += 1 system_state[counter, 0, 0] += 1 new_val = counter(system_state) npt.assert_almost_equal(old_val + [[3, 2], [1, 1]], new_val) clock.register_listener(_update_counter) simulator = Simulator(system, start_time=0) for _ in simulator.run_until(time_boundary=10): pass
def test_excessive_events_second_level(): """Test the detection of excessive events when it is introduced by toggling the same event over and over.""" system = System() state = State(system, derivative_function=(lambda data: -1), initial_condition=5) event = ZeroCrossEventSource(system, event_function=state) def event_handler(data): """Event handler for the zero-crossing event""" state.set_value(data, -state(data)) event.register_listener(event_handler) simulator = Simulator(system, start_time=0) with pytest.raises(ExcessiveEventError): for _ in simulator.run_until(6): pass
def test_discrete_only(): """Test a system with only discrete-time states.""" system = System() clock = Clock(system, period=1.0) counter = State(system) def increase_counter(data): """Increase the counter whenever a clock event occurs""" counter.set_value(data, counter(data) + 1) clock.register_listener(increase_counter) simulator = Simulator(system, start_time=0.0) result = SimulationResult(system) result.collect_from(simulator.run_until(5.5, include_last=False)) result.collect_from(simulator.run_until(10)) npt.assert_almost_equal(result.time, [0, 1, 2, 3, 4, 5, 5.5, 6, 7, 8, 9, 10]) npt.assert_almost_equal(counter(result), [1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11])
def integrator(owner, input_signal, initial_condition=None): """ Create a state that provides the integrated value of the input callable. The resulting signal will have the same shape as the input callable. Args: owner: The owner of the integrator input_signal: A callable accepting an object implementing the system object access protocol and providing the value of the derivative initial_condition: The initial condition of the integrator (default: ``None``) Returns: A state that provides the integrated value of the input signal """ return State( owner, shape=input_signal.shape, derivative_function=input_signal, initial_condition=initial_condition, )
# Create the system system = System() # Define the derivatives def velocity_dt(system_state): """Calculate the derivative of the velocity""" pos = position(system_state) distance = linalg.norm(pos, axis=0) return -G * SUN_MASS / (distance**3) * pos # Create the states velocity = State( owner=system, shape=2, derivative_function=velocity_dt, initial_condition=V_0, ) position = State(owner=system, shape=2, derivative_function=velocity, initial_condition=X_0) # Run a simulation simulator = Simulator(system, start_time=0.0, max_step=24 * 60 * 60) result = SimulationResult(system, simulator.run_until(time_boundary=ROTATION_TIME)) # Plot the result trajectory = position(result) plt.plot(trajectory[0], trajectory[1])
def test_system_state(): """Test the ``SystemState`` class""" system = System() input_a = InputSignal(system, shape=(3, 3), value=np.eye(3)) input_c = InputSignal(system, value=1) input_d = InputSignal(system, shape=2, value=[2, 3]) state_a = State( system, shape=(3, 3), initial_condition=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], derivative_function=input_a, ) state_a_dep = State( system, shape=(3, 3), initial_condition=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], derivative_function=input_a, ) state_b = State( system, shape=3, initial_condition=[10, 11, 12], derivative_function=(lambda data: np.r_[13, 14, 15]), ) state_b1 = State(system, shape=3, derivative_function=state_b) state_b2 = State(system, shape=3, derivative_function=None) signal_c = constant(value=16) event_a = ZeroCrossEventSource(system, event_function=(lambda data: 23)) system_state = SystemState(time=0, system=system) # Check the initial state npt.assert_almost_equal( system_state.state[state_a.state_slice], state_a.initial_condition.ravel(), ) npt.assert_almost_equal( system_state.state[state_a_dep.state_slice], state_a_dep.initial_condition.ravel(), ) npt.assert_almost_equal( system_state.state[state_b.state_slice], state_b.initial_condition.ravel(), ) npt.assert_almost_equal( system_state.state[state_b1.state_slice], state_b1.initial_condition.ravel(), ) npt.assert_almost_equal(system_state.state[state_b2.state_slice], np.zeros(state_b2.size)) # Check the inputs property npt.assert_almost_equal(system_state.inputs[input_a.input_slice], np.ravel(input_a.value)) npt.assert_almost_equal(system_state.inputs[input_c.input_slice], np.ravel(input_c.value)) npt.assert_almost_equal(system_state.inputs[input_d.input_slice], np.ravel(input_d.value)) # Check the get_state_value function npt.assert_almost_equal(system_state.get_state_value(state_a), state_a.initial_condition) npt.assert_almost_equal(system_state.get_state_value(state_a_dep), state_a_dep.initial_condition) npt.assert_almost_equal(system_state.get_state_value(state_b), state_b.initial_condition) # Check the function access npt.assert_equal(event_a(system_state), event_a.event_function(system_state)) npt.assert_equal(state_a(system_state), state_a.initial_condition) npt.assert_equal(input_a(system_state), input_a.value) npt.assert_equal(signal_c(system_state), signal_c.value)
moment_of_inertia=MOMENT_OF_INERTIA, thrust_coefficient=THRUST_COEFFICIENT, power_coefficient=POWER_COEFFICIENT, diameter=DIAMETER, ) # Provide constant signals for the voltage and the air density voltage = constant(value=3.5) density = constant(value=1.29) # Connect them to the corresponding inputs of the engine engine.voltage.connect(voltage) engine.density.connect(density) # Set up the state for keeping the sampled value. sample_state = State(system) # Create a clock for sampling at 100Hz sample_clock = Clock(system, period=0.01) # Define the function for updating the state def update_sample(system_state): """Update the state of the sampler""" sample_state.set_value(system_state, engine.thrust(system_state)) # Register it as event handler on the clock sample_clock.register_listener(update_sample) # Create the simulator and run it
from matplotlib import pyplot as plt from modypy.model import State, System, signal_function from modypy.simulation import SimulationResult, Simulator # Create a new system system = System() # Define the cosine signal @signal_function def cosine_input(system_state): """Calculate the value of the input signal""" return np.cos(system_state.time) integrator_state = State(system, derivative_function=cosine_input) # Set up a simulation simulator = Simulator(system, start_time=0.0, max_step=0.1) # Run the simulation for 10s and capture the result result = SimulationResult(system, simulator.run_until(time_boundary=10.0)) # Plot the result input_line, integrator_line = plt.plot( result.time, cosine_input(result), "r", result.time, integrator_state(result), "g",
# Create a new system system = System() # Model the inflow inflow_velocity = InputSignal(system) # Model the height state def height_derivative(data): """Calculate the time derivative of the height""" return (A1 * inflow_velocity(data) - A2 * np.sqrt(2 * G * height_state(data))) / At height_state = State(system, derivative_function=height_derivative) # Configure for steady-state determination steady_state_config = SteadyStateConfiguration(system) # Enforce the height to equal the target height steady_state_config.states[height_state].lower_bounds = TARGET_HEIGHT steady_state_config.states[height_state].upper_bounds = TARGET_HEIGHT # Enforce the inflow to be non-negative steady_state_config.inputs[inflow_velocity].lower_bounds = 0 # Find the steady state result = find_steady_state(steady_state_config) print("Target height: %f" % TARGET_HEIGHT) print("Steady state height: %f" % height_state(result.system_state)) print("Steady state inflow: %f" % inflow_velocity(result.system_state)) print("Steady state height derivative: %f" %
def __init__( self, parent, system_matrix, input_matrix, output_matrix, feed_through_matrix, initial_condition=None, ): Block.__init__(self, parent) # Determine the number of states and the shape of the state if np.isscalar(system_matrix): self.state_shape = () num_states = 1 else: system_matrix = np.asarray(system_matrix) if (system_matrix.ndim == 2 and system_matrix.shape[0] > 0 and system_matrix.shape[0] == system_matrix.shape[1]): self.state_shape = system_matrix.shape[0] num_states = self.state_shape else: raise InvalidLTIException("The system matrix must be a scalar " "or a non-empty square matrix") # Determine the number of inputs and the shape of the input signal if np.isscalar(input_matrix): if num_states > 1: raise InvalidLTIException("There is more than one state, but " "the input matrix is neither empty, " "nor a vector or a matrix") num_inputs = 1 self.input_shape = () else: input_matrix = np.asarray(input_matrix) if input_matrix.ndim == 1: # The input matrix is a column vector => one input if num_states != input_matrix.shape[0]: raise InvalidLTIException( "The height of the input matrix " "must match the number of states") num_inputs = 1 elif input_matrix.ndim == 2: # The input matrix is a matrix if num_states != input_matrix.shape[0]: raise InvalidLTIException("The height of the input matrix " "does not match the number of " "states") num_inputs = input_matrix.shape[1] else: raise InvalidLTIException("The input matrix must be empty," "a scalar, a vector or a matrix") self.input_shape = num_inputs # Determine the number of outputs and the shape of the output array if np.isscalar(output_matrix): if num_states > 1: raise InvalidLTIException("There is more than one state, but " "the output matrix is neither an " "empty, a vector nor a matrix") num_outputs = 1 self.output_shape = () else: output_matrix = np.asarray(output_matrix) if output_matrix.ndim == 1: # The output matrix is a row vector => one output if num_states != output_matrix.shape[0]: raise InvalidLTIException("The width of the output matrix " "does not match the number of " "states") num_outputs = 1 elif output_matrix.ndim == 2: # The output matrix is a matrix if num_states != output_matrix.shape[1]: raise InvalidLTIException("The width of the output matrix " "does not match the number of " "states") num_outputs = output_matrix.shape[0] else: raise InvalidLTIException("The output matrix must be empty, a" "scalar, a vector or a matrix") self.output_shape = num_outputs if np.isscalar(feed_through_matrix): if not (num_inputs == 1 and num_outputs == 1): raise InvalidLTIException("A scalar feed-through matrix is " "only allowed for systems with " "exactly one input and one output") else: feed_through_matrix = np.asarray(feed_through_matrix) if feed_through_matrix.ndim == 1: # A vector feed_through_matrix is interpreted as row vector, # so there must be exactly one output. if num_outputs == 0: raise InvalidLTIException("The feed-through matrix for a " "system without outputs must be" "empty") elif num_outputs > 1: raise InvalidLTIException("The feed-through matrix for a " "system with more than one " "output must be a matrix") if feed_through_matrix.shape[0] != num_inputs: raise InvalidLTIException( "The width of the feed-through " "matrix must match the number of " "inputs") elif feed_through_matrix.ndim == 2: if feed_through_matrix.shape[0] != num_outputs: raise InvalidLTIException( "The height of the feed-through " "matrix must match the number of " "outputs") if feed_through_matrix.shape[1] != num_inputs: raise InvalidLTIException( "The width of the feed-through " "matrix must match the number of " "inputs") else: raise InvalidLTIException("The feed-through matrix must be " "empty, a scalar, a vector or a " "matrix") self.system_matrix = system_matrix self.input_matrix = input_matrix self.output_matrix = output_matrix self.feed_through_matrix = feed_through_matrix self.input = Port(shape=self.input_shape) self.state = State( self, shape=self.state_shape, derivative_function=self.state_derivative, initial_condition=initial_condition, ) self.output = Signal(shape=self.output_shape, value=self.output_function)
# The initial conditions INITIAL_HEIGHT = 10.0 INITIAL_VELOCITY = 0.0 # The system system = System() # The system states def velocity_dt(_system_state): """Calculate the derivative of the vertical speed""" return -G velocity = State(system, derivative_function=velocity_dt, initial_condition=INITIAL_VELOCITY) height = integrator(system, input_signal=velocity, initial_condition=INITIAL_HEIGHT) # Run a simulation without event handler simulator = Simulator(system=system, start_time=0, max_step=0.1) result = SimulationResult(system=system) result.collect_from(simulator.run_until(time_boundary=2)) # Plot the simulation result plt.plot(result.time, height(result)) plt.xlabel("Time (s)") plt.ylabel("Height (m)") plt.title("Falling Ball")
def propeller_model( thrust_coefficient=0.09, power_coefficient=0.04, density=1.29, diameter=8 * 25.4e-3, moment_of_inertia=5.284e-6, target_thrust=1.5 * 9.81 / 4, maximum_torque=None, ): """Create a steady-state configuration for two propellers""" system = System() torque_1 = InputSignal(system) torque_2 = InputSignal(system) density_signal = constant(density) propeller_1 = Propeller( system, thrust_coefficient=thrust_coefficient, power_coefficient=power_coefficient, diameter=diameter, ) propeller_2 = Propeller( system, thrust_coefficient=thrust_coefficient, power_coefficient=power_coefficient, diameter=diameter, ) def speed_1_dt(data): """Derivative of the speed of the first propeller""" tot_torque = torque_1(data) - propeller_1.torque(data) return tot_torque / (2 * np.pi * moment_of_inertia) def speed_2_dt(data): """Derivative of the speed of the second propeller""" tot_torque = torque_2(data) - propeller_2.torque(data) return tot_torque / (2 * np.pi * moment_of_inertia) speed_1 = State(system, derivative_function=speed_1_dt) speed_2 = State(system, derivative_function=speed_2_dt) propeller_1.density.connect(density_signal) propeller_2.density.connect(density_signal) propeller_1.speed_rps.connect(speed_1) propeller_2.speed_rps.connect(speed_2) total_thrust = sum_signal((propeller_1.thrust, propeller_2.thrust)) total_power = sum_signal((propeller_1.power, propeller_2.power)) # Set up steady-state configuration config = SteadyStateConfiguration(system) # Minimize the total power config.objective = total_power # Constrain the speed to be positive config.states[speed_1].lower_bounds = 0 config.states[speed_2].lower_bounds = 0 # Constrain the torques to be positive config.inputs[torque_1].lower_bounds = 0 config.inputs[torque_2].lower_bounds = 0 npt.assert_equal(config.inputs[torque_1].lower_bounds, 0) npt.assert_equal(config.inputs[torque_2].lower_bounds, 0) if maximum_torque is not None: config.inputs[torque_1].upper_bounds = maximum_torque config.inputs[torque_2].upper_bounds = maximum_torque # Constrain the total thrust config.ports[total_thrust].lower_bounds = target_thrust config.ports[total_thrust].upper_bounds = target_thrust return config