Saturday, December 20, 2025

Proportional (P) Controller: Step Response and the Effect of Gain (Kp)


The Proportional (P) controller is the simplest form of feedback control. It calculates the control output as:u = Kp × error
(where error = setpoint − actual value)
When we apply a step input (sudden change in setpoint from 0 to 1), the system's response depends heavily on the gain Kp.Key Effects of Increasing Kp
  • Faster response: The output rises toward the setpoint more quickly.
  • Reduced steady-state error: The final offset gets smaller, but never zero.
For a typical first-order system and unit step, the steady-state error is:ess = 1 / (1 + Kp)
Final output = Kp / (1 + Kp)
Step Response Comparison![Step Response Graph]
(Embed the graph here: setpoint jumps to 1.0; outputs shown for Kp = 0.5, 1.0, 2.0, 5.0, 10.0)
  • Kp = 0.5 → final ≈ 0.33 (error 0.67) – slow, large offset
  • Kp = 1.0 → final ≈ 0.50 (error 0.50)
  • Kp = 2.0 → final ≈ 0.67 (error 0.33)
  • Kp = 5.0 → final ≈ 0.83 (error 0.17)
  • Kp = 10.0 → final ≈ 0.91 (error 0.09) – fast, small offset
In this first-order example, there's no overshoot. Higher-order systems can oscillate at high Kp.LimitationPure P control always leaves some steady-state error for step changes. To eliminate it, add an integral term (PI/PID controller).When to Use P-OnlyGreat for simple applications where a small offset is acceptable and fast response matters (e.g., basic fan speed or level control).Higher Kp gives you speed and accuracy—but push it too far and stability suffers.

Results



Theoretical steady-state values for unit step input:
(Formula: final output = Kp / (1 + Kp), error = 1 / (1 + Kp))
Kp =  0.5 → final output = 0.3333, steady-state error = 0.6667
Kp =  1.0 → final output = 0.5000, steady-state error = 0.5000
Kp =  2.0 → final output = 0.6667, steady-state error = 0.3333
Kp =  5.0 → final output = 0.8333, steady-state error = 0.1667
Kp = 10.0 → final output = 0.9091, steady-state error = 0.0909

Friday, December 19, 2025

Employment Act and the Control, Integration and Economic Reality Test

Employees (contract of service) enjoy protections such as paid holiday, sick pay, maternity/paternity leave, minimum wage, protection against unfair dismissal, and redundancy pay.

Independent contractors (contract for services) generally have none of these, but they gain flexibility, the ability to work for multiple clients, and potential tax advantages (though often with higher administrative burdens).

Misclassification can be expensive: backdated employment rights, tax penalties, and fines. That's why courts and tribunals don't simply accept the label in the written contract – they look at the reality of the working relationship using established legal tests.        

The three classic tests most commonly referenced are:1. The Control Test2. The Integration (or Organisation) Test3. The Economic Reality (or "Business on Own Account") Test
How Courts Actually Decide

In practice, no single test is decisive. Modern courts (especially in the UK post cases like Uber BV v Aslam and Pimlico Plumbers) use a multifactor approach, weighing all relevant factors to reflect the true "economic reality" of the relationship. Written contracts are considered, but they are not conclusive – courts look behind the label.  

Visualising the Tests in ActionTo make this clearer, here's a practical example using three hypothetical workers in a tech company:
  • Alex – Full-time developer: high control, fully integrated, no financial risk → clearly an employee.
  • Sam – Freelance designer: low control, not integrated, multiple clients and own equipment → clearly a contractor.
  • Jordan – Consultant: mixed indicators → ambiguous (may fall into the intermediate "worker" category with some rights).
The dashboard below applies the three tests and visualises the results:




Final Thought
Getting this classification right from the start saves headaches later. If you're an employer engaging freelancers or contractors, consider running your arrangements through these three lenses – or seek specialist advice when the picture is unclear.


Python code by Grok
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# Data for three example workers
workers = {
    "Alex (Full-time Developer)": {"control": 4, "integration": 3, "independence": 0},
    "Sam (Freelance Designer)":   {"control": 1, "integration": 1, "independence": 5},
    "Jordan (Consultant)":        {"control": 2, "integration": 2, "independence": 3}
}

typical_employee = {"control": 4, "integration": 3, "independence": 0}
typical_contractor = {"control": 1, "integration": 1, "independence": 5}

def get_radar_values(data):
    return [
        data["control"] * 2.5,
        data["integration"] * 3.33,
        data["independence"] * 2,
        data["independence"] * 2,
        data["independence"] * 2
    ]

categories = ['Control', 'Integration', 'Financial Independence', 'Own Equipment & Risk', 'Multiple Clients']
theta_closed = categories + [categories[0]]

fig = make_subplots(
    rows=1, cols=2,
    subplot_titles=("Radar Chart: Employment Tests Comparison", "Likelihood of Employee Status"),
    specs=[[{"type": "polar"}, {"type": "bar"}]],
    horizontal_spacing=0.15
)

# Typical profiles
emp_vals = get_radar_values(typical_employee) + [get_radar_values(typical_employee)[0]]
cont_vals = get_radar_values(typical_contractor) + [get_radar_values(typical_contractor)[0]]

fig.add_trace(go.Scatterpolar(r=emp_vals, theta=theta_closed, fill='toself',
                              fillcolor='lightgreen', opacity=0.3, line_color='green',
                              name='Typical Employee'), row=1, col=1)
fig.add_trace(go.Scatterpolar(r=cont_vals, theta=theta_closed, fill='toself',
                              fillcolor='lightcoral', opacity=0.3, line_color='red',
                              name='Typical Contractor'), row=1, col=1)

# Workers
colors = ['#4CAF50', '#2196F3', '#FF9800']
for i, (name, data) in enumerate(workers.items()):
    vals = get_radar_values(data) + [get_radar_values(data)[0]]
    fig.add_trace(go.Scatterpolar(r=vals, theta=theta_closed, fill='toself',
                                  name=name, fillcolor=colors[i], opacity=0.6,
                                  line_color=colors[i]), row=1, col=1)

# Bar chart
classifications = []
probs = []
for name, data in workers.items():
    emp_score = (data["control"] + data["integration"]) / 7 * 100
    cont_score = data["independence"] / 5 * 100
    total = emp_score + cont_score
    emp_prob = emp_score / total * 100 if total > 0 else 50
    probs.append(emp_prob)
    classifications.append(name)

fig.add_trace(go.Bar(x=classifications, y=probs, marker_color=colors,
                     text=[f"{p:.0f}%" for p in probs], textposition='outside'), row=1, col=2)

fig.update_layout(title="<b>Contract of Service vs Contract for Services</b><br>Visualising the Three Tests",
                  height=700, showlegend=True, margin=dict(t=100))
fig.update_polars(radialaxis=dict(range=[0,10]))
fig.update_yaxes(title_text="Likelihood of Employee Classification (%)", row=1, col=2, range=[0,100])

fig.show()