To solve the given optimization problem, we first need to convert the natural language description into a symbolic representation. This involves defining variables and formulating the objective function and constraints in terms of these variables.

Let's define:
- \(x_1\) as the number of bottles of perfume to buy and sell.
- \(x_2\) as the number of bottles of mascara to buy and sell.

The objective is to maximize profit. The profit from selling one bottle of perfume is $20, and the profit from selling one bottle of mascara is $15. Therefore, the objective function can be written as:
\[ \text{Maximize:} \quad 20x_1 + 15x_2 \]

The constraints are as follows:
1. The budget constraint: The total cost of buying perfume and mascara must not exceed $20,000. Given that each bottle of perfume costs $50 and each bottle of mascara costs $40, we have:
\[ 50x_1 + 40x_2 \leq 20000 \]
2. The sales estimate for perfume: At least 20 but at most 40 bottles of perfume will be sold, which gives us:
\[ 20 \leq x_1 \leq 40 \]
3. The relationship between the sales of mascara and perfume: The number of mascara bottles sold is at most a third of the number of perfume bottles sold, so we have:
\[ x_2 \leq \frac{1}{3}x_1 \]

In symbolic representation with natural language objects, this can be summarized as:
```json
{
  'sym_variables': [('x1', 'number of bottles of perfume'), ('x2', 'number of bottles of mascara')],
  'objective_function': '20*x1 + 15*x2',
  'constraints': [
    '50*x1 + 40*x2 <= 20000',
    '20 <= x1 <= 40',
    'x2 <= (1/3)*x1'
  ]
}
```

Now, let's implement this problem using Gurobi in Python:

```python
from gurobipy import *

# Create a new model
m = Model("Makeup_Store_Profit")

# Define the variables
x1 = m.addVar(vtype=GRB.INTEGER, name="perfume_bottles")
x2 = m.addVar(vtype=GRB.INTEGER, name="mascara_bottles")

# Set the objective function
m.setObjective(20*x1 + 15*x2, GRB.MAXIMIZE)

# Add constraints
m.addConstr(50*x1 + 40*x2 <= 20000, "budget_constraint")
m.addConstr(x1 >= 20, "min_perfume_sales")
m.addConstr(x1 <= 40, "max_perfume_sales")
m.addConstr(x2 <= (1/3)*x1, "mascara_to_perfume_ratio")

# Optimize the model
m.optimize()

# Print the solution
if m.status == GRB.OPTIMAL:
    print("Optimal solution found:")
    print(f"Number of perfume bottles: {x1.x}")
    print(f"Number of mascara bottles: {x2.x}")
    print(f"Maximum profit: {m.objVal}")
else:
    print("No optimal solution found")
```