# Standard library imports
import datetime
from typing import Optional
# Third party imports
import numpy as np
import torch
import torch.nn as nn
from scipy.optimize import least_squares
# Local application imports
import twin4build.core as core
import twin4build.utils.types as tps
from twin4build.translator.translator import (
Exact,
MultiPath,
Node,
Optional_,
SignaturePattern,
SinglePath,
)
[docs]
def get_signature_pattern():
node0 = Node(cls=core.namespace.S4BLDG.SetpointController)
node1 = Node(cls=core.namespace.SAREF.Sensor)
node2 = Node(cls=core.namespace.SAREF.Property)
node3 = Node(cls=core.namespace.S4BLDG.Schedule)
node4 = Node(cls=core.namespace.XSD.boolean)
sp = SignaturePattern(
semantic_model_=core.ontologies, id="pid_controller_signature_pattern"
)
sp.add_triple(
Exact(subject=node0, object=node2, predicate=core.namespace.SAREF.observes)
)
sp.add_triple(
Exact(subject=node1, object=node2, predicate=core.namespace.SAREF.observes)
)
sp.add_triple(
Exact(subject=node0, object=node3, predicate=core.namespace.SAREF.hasProfile)
)
sp.add_triple(
Exact(subject=node0, object=node4, predicate=core.namespace.S4BLDG.isReverse)
)
sp.add_input("actualValue", node1, "measuredValue")
sp.add_input("setpointValue", node3, "scheduleValue")
sp.add_parameter("isReverse", node4)
sp.add_modeled_node(node0)
return sp
[docs]
class PIDControllerSystem(core.System, nn.Module):
r"""
PID Controller System.
This class implements a PID controller with a differentiable saturation function.
Args:
kp: Proportional gain
Ti: Integral time constant
Td: Derivative time constant
isReverse: Boolean flag to indicate if the controller is reverse
"""
sp = [get_signature_pattern()]
def __init__(
self,
kp=0.001,
Ti=10,
Td=0.0,
isReverse=False,
**kwargs,
):
super().__init__(**kwargs)
nn.Module.__init__(self)
self.isReverse = isReverse
kp = abs(kp)
if isReverse == False:
kp = -kp
Ti = abs(Ti)
Td = abs(Td)
self.kp = tps.Parameter(
torch.tensor(kp, dtype=torch.float64), requires_grad=False
)
self.Ti = tps.Parameter(
torch.tensor(Ti, dtype=torch.float64), requires_grad=False
)
self.Td = tps.Parameter(
torch.tensor(Td, dtype=torch.float64), requires_grad=False
)
self.input = {"actualValue": tps.Scalar(), "setpointValue": tps.Scalar()}
self.output = {"inputSignal": tps.Scalar(0)}
self._config = {"parameters": ["kp", "Ti", "Td", "isReverse"]}
@property
def config(self):
return self._config
[docs]
def initialize(
self,
start_time: datetime.datetime,
end_time: datetime.datetime,
step_size: int,
simulator: core.Simulator,
) -> None:
self.input["actualValue"].initialize(
start_time=start_time,
end_time=end_time,
step_size=step_size,
simulator=simulator,
)
self.input["setpointValue"].initialize(
start_time=start_time,
end_time=end_time,
step_size=step_size,
simulator=simulator,
)
self.output["inputSignal"].initialize(
start_time=start_time,
end_time=end_time,
step_size=step_size,
simulator=simulator,
)
# self.acc_err = torch.tensor([0], dtype=torch.float64, requires_grad=False)
self.err_prev = torch.tensor([0], dtype=torch.float64, requires_grad=False)
self.err_prev_m1 = torch.tensor([0], dtype=torch.float64, requires_grad=False)
self.u_prev = torch.tensor([0], dtype=torch.float64, requires_grad=False)
[docs]
def asymptotic_smooth_saturation(
self, u, lower=0.0, upper=1.0, eps=0, curve_start=0.01, steepness=1
):
effective_min = lower + eps
effective_max = upper - eps
lower_curve_point = effective_min + curve_start
upper_curve_point = effective_max - curve_start
# Three explicit regions
result = torch.where(
u < lower_curve_point,
# Lower region: curve toward effective_min
effective_min
+ (lower_curve_point - effective_min)
* torch.exp(-steepness * (lower_curve_point - u) / curve_start),
torch.where(
u > upper_curve_point,
# Upper region: curve toward effective_max
effective_max
- (effective_max - upper_curve_point)
* torch.exp(-steepness * (u - upper_curve_point) / curve_start),
# Linear region: perfect passthrough
u,
),
)
return result
[docs]
def do_step(
self,
secondTime: float,
dateTime: datetime.datetime,
step_size: int,
stepIndex: int,
) -> None:
err = self.input["setpointValue"].get() - self.input["actualValue"].get()
du = self.kp.get() * (
(1 + step_size / self.Ti.get() + self.Td.get() / step_size) * err
+ (-1 - 2 * self.Td.get() / step_size) * self.err_prev
+ self.Td.get() / step_size * self.err_prev_m1
)
u = self.u_prev + du
u = self.asymptotic_smooth_saturation(
u, lower=0.0, upper=1.0, curve_start=0.05, steepness=1
)
self.u_prev = u
self.err_prev_m1 = self.err_prev
self.err_prev = err
self.output["inputSignal"].set(u, stepIndex)