<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Advanced
Step1:  In this case, the two positional arguments to b.add_constraint must be the left-hand side of the expression (which will become the constrained parameter) and the right-hand side of the expression (either another parameter or a ConstraintParameter).
Simple Case
The easiest way to create a constraint parameter is via math operations on existing parameters.  For example, let's say we wanted the secondary temperature to always be half the primary temperature
Step2:  Now, as with any other constraint, if we change the value of a parameter in the constraint, the constrained value will automatically adjust.
Step3:  Complex Case with New Parameter
Now let's say that instead of hardcoding the ratio between the temperatures, we wanted to parameterize the system in terms of the temperature ratio (a parameter that does not exist in the default bundle).  Of course, this case is already a built-in constraint, so in practice you would use teffratio... but we'll recreate that constraint from scratch here.
Step4:  First we need to create all the parameters that we need that do not already exist, and attach them to the bundle with appropriate tags.  Ultimately the choice of tags is inconsequential, but here it makes some sense to apply our new teffratio to the parent orbit that contains both stars.
Almost always (since we're creating constraints), we'll need to define a new FloatParameter.  Note that the default_unit is quite important here, as the constraint expressions will propagate units.
Step5:  We'll attach the new parameter by calling b.get_or_create.  The first argument here is the qualifier, the second is the parameter object itself, and additional keyword arguments are tags to be applied.  If a parameter already exists that matches the filter (including the qualifier) then that parameter will be returned (and the new parameter will be ignored), otherwise the new parameter will be attached to the bundle and returned.
The first returned argument is the matching parameter, and the second is a boolean which tells whether the new parameter was added or if an existing on was retrieved.
Step6:  Our new parameter is available through filtering as is any other parameter.
Step7:  We can now define our constraint as before, but replace the hardcoded 0.5 with the teffratio parameter.
<ASSISTANT_TASK:>
Python Code:
import phoebe
from phoebe import u
b = phoebe.default_binary()
Explanation: Advanced: Custom Constraints
Built-in Constraints are convenient as they automatically determine the correct expression and include support for multiple parameterizations via b.flip_constraint.  However, for cases where a built-in constraint does not exist, PHOEBE provides a framework for easily creating your own expression to link multiple parameters via b.add_constraint.
End of explanation
print(b.filter(qualifier='teff'))
lhs = b.get_parameter(qualifier='teff', component='secondary')
rhs = 0.5 * b.get_parameter(qualifier='teff', component='primary')
rhs
b.add_constraint(lhs, rhs)
print(b.filter(qualifier='teff'))
Explanation: In this case, the two positional arguments to b.add_constraint must be the left-hand side of the expression (which will become the constrained parameter) and the right-hand side of the expression (either another parameter or a ConstraintParameter).
Simple Case
The easiest way to create a constraint parameter is via math operations on existing parameters.  For example, let's say we wanted the secondary temperature to always be half the primary temperature:
End of explanation
b.set_value(qualifier='teff', component='primary', value=7000)
print(b.filter(qualifier='teff'))
Explanation: Now, as with any other constraint, if we change the value of a parameter in the constraint, the constrained value will automatically adjust.
End of explanation
b = phoebe.default_binary()
b.filter(qualifier='teffratio')
Explanation: Complex Case with New Parameter
Now let's say that instead of hardcoding the ratio between the temperatures, we wanted to parameterize the system in terms of the temperature ratio (a parameter that does not exist in the default bundle).  Of course, this case is already a built-in constraint, so in practice you would use teffratio... but we'll recreate that constraint from scratch here.
End of explanation
teffratio_def = phoebe.parameters.FloatParameter(qualifier='teffratio',
                                                 default_unit=u.dimensionless_unscaled,
                                                 value=1, 
                                                 description='effective temperature ratio')
Explanation: First we need to create all the parameters that we need that do not already exist, and attach them to the bundle with appropriate tags.  Ultimately the choice of tags is inconsequential, but here it makes some sense to apply our new teffratio to the parent orbit that contains both stars.
Almost always (since we're creating constraints), we'll need to define a new FloatParameter.  Note that the default_unit is quite important here, as the constraint expressions will propagate units.
End of explanation
teffratio_param, created = b.get_or_create('teffratio', teffratio_def, context='component', component='binary')
Explanation: We'll attach the new parameter by calling b.get_or_create.  The first argument here is the qualifier, the second is the parameter object itself, and additional keyword arguments are tags to be applied.  If a parameter already exists that matches the filter (including the qualifier) then that parameter will be returned (and the new parameter will be ignored), otherwise the new parameter will be attached to the bundle and returned.
The first returned argument is the matching parameter, and the second is a boolean which tells whether the new parameter was added or if an existing on was retrieved.
End of explanation
print(b.filter(qualifier='teffratio'))
print(b.get_parameter(qualifier='teffratio').tags)
Explanation: Our new parameter is available through filtering as is any other parameter.
End of explanation
lhs = b.get_parameter(qualifier='teff', component='secondary')
rhs = teffratio_param * b.get_parameter(qualifier='teff', component='primary')
rhs
b.add_constraint(lhs, rhs)
print(b.filter(qualifier=['teff', 'teffratio']))
b.set_value('teffratio', 0.5)
print(b.filter(qualifier=['teff', 'teffratio']))
Explanation: We can now define our constraint as before, but replace the hardcoded 0.5 with the teffratio parameter.
End of explanation
<END_TASK>