it.grypho.scala.leonardo.control

Members list

Type members

Classlikes

Continuous-to-discrete conversion.

Continuous-to-discrete conversion.

The method is always a visible argument, never a hidden default. The same plant discretised by zero-order hold and by Tustin has different discrete poles, so a silent choice would make two correct-looking answers disagree with no way to see why.

Attributes

Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
case class _Feedback(g: _Expression, h: _Expression, v: _Variable) extends _Expression

Closed loop with negative feedback: feedback(G, H, v) = G/(1 + G*H).

Closed loop with negative feedback: feedback(G, H, v) = G/(1 + G*H).

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
trait _Expression
class Object
trait Matchable
class Any
Show all

Impulse response: impulse(G, s, t) = L^-1{G(s)}.

Impulse response: impulse(G, s, t) = L^-1{G(s)}.

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
trait _Expression
class Object
trait Matchable
class Any
Show all
case class _Parallel(g: _Expression, h: _Expression, v: _Variable) extends _Expression

Parallel connection: parallel(G, H, v).

Parallel connection: parallel(G, H, v).

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
trait _Expression
class Object
trait Matchable
class Any
Show all
case class _Series(g: _Expression, h: _Expression, v: _Variable) extends _Expression

Series connection: series(G, H, v).

Series connection: series(G, H, v).

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
trait _Expression
class Object
trait Matchable
class Any
Show all
case class _StepResponse(g: _Expression, s: _Variable, t: _Variable) extends _Expression

Unit-step response: step(G, s, t) = L^-1{G(s)/s}.

Unit-step response: step(G, s, t) = L^-1{G(s)/s}.

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
trait _Expression
class Object
trait Matchable
class Any
Show all

Value members

Concrete methods

def bode(g: _Expression, v: _Variable, w: Double): Option[(Double, Double)]

Frequency response of G at angular frequency w: substitute v -> i*w.

Frequency response of G at angular frequency w: substitute v -> i*w.

Rides the existing _Complex closure — no new arithmetic — which is why the whole of Bode and Nyquist is one substitution.

Value parameters

g

the transfer function

v

the frequency variable

w

the angular frequency, in radians per unit time

Attributes

Returns

(magnitude, phase-in-radians), or None where the response is not finite (evaluating an integrator at w = 0, say) — never a fabricated infinity

def c2d(g: _Expression, s: _Variable, z: _Variable, ts: Double, method: DiscretisationMethod): Option[_Expression]

Discretises a continuous transfer function over sample period ts.

Discretises a continuous transfer function over sample period ts.

Tustin is a substitution: replace s by (2/Ts)*(z-1)/(z+1) and renormalise. Zero- order hold is a hold equivalence, G_d(z) = (1 - z^-1) * Z{L^-1[G(s)/s]}, assembled from the inverse Laplace transform and the z-transform — which is the reason the z-transform had to exist first.

Value parameters

g

the continuous transfer function

method

Zoh or Tustin

s

the continuous frequency variable

ts

the sample period; must be positive

z

the discrete variable to express the result in

Attributes

Returns

the discrete transfer function, or None when the conversion does not close

def c2dExact(a: _Expression, b: _Expression, ts: Double): Option[(_MatrixValue, _MatrixValue)]

Exact state-space discretisation over one sample period: A_d = e^(A*Ts).

Exact state-space discretisation over one sample period: A_d = e^(A*Ts).

Uses the block-matrix form, not B_d = A⁻¹(A_d − I)B, and that is a correctness matter rather than tidiness. Exponentiating [[A, B], [0, 0]]*Ts yields [[A_d, B_d], [0, I]] in one step, and it is defined for a singular A — where the inverse form is not, even though B_d exists. A singular A is entirely ordinary: any system with an integrator has one, so the naive form would fail on a common case rather than an exotic one.

This is what the matrix exponential exists to supply, and what a Kalman filter needs.

Value parameters

a

the continuous state matrix

b

the continuous input matrix

ts

the sample period

Attributes

Returns

(A_d, B_d), or None when the operands are not conforming dense matrices

def controllable(a: _Expression, b: _Expression): Option[Boolean]

Is the pair (A, B) controllable — can the input steer every state?

Is the pair (A, B) controllable — can the input steer every state?

Value parameters

a

the state matrix

b

the input matrix

Attributes

Returns

Some(true)/Some(false), or None when either operand is not a dense matrix

def d2c(g: _Expression, z: _Variable, s: _Variable, ts: Double, method: DiscretisationMethod): Option[_Expression]

Inverse of c2d: recovers a continuous transfer function from a discrete one.

Inverse of c2d: recovers a continuous transfer function from a discrete one.

Only Tustin is invertible in closed form — the substitution z = (2 + Ts*s)/(2 - Ts*s) is its exact inverse, which is what makes d2c(c2d(G)) return to G. A zero-order-hold inverse would need a matrix logarithm and is declined rather than approximated.

Value parameters

g

the discrete transfer function

method

must be Tustin

s

the continuous variable to express the result in

ts

the sample period

z

the discrete variable

Attributes

Returns

the continuous transfer function, or None

def dcgain(g: _Expression, v: _Variable): Option[Double]

Steady-state gain: G evaluated at v = 0.

Steady-state gain: G evaluated at v = 0.

Value parameters

g

the transfer function

v

the frequency variable

Attributes

Returns

G(0), or None when it is not finite (a pole at the origin) or not rational

Closed loop with negative feedback: G / (1 + G*H).

Closed loop with negative feedback: G / (1 + G*H).

The sign convention is stated because it cannot be inferred. Negative feedback is the control convention and what a unity-feedback loop means by default; a substantial part of the literature writes the positive form G/(1 - G*H), and choosing silently would make every closed-loop result wrong for half its readers — the reasoning that also fixes the spherical polar-angle convention in the vector package. For positive feedback, negate h.

Value parameters

g

the forward path

h

the feedback path (use _Number(1) for unity feedback)

v

the frequency variable

Attributes

Returns

the closed-loop transfer function, normalised to a single rational

def frequencyResponse(g: _Expression, v: _Variable, wMin: Double, wMax: Double, points: Int, env: Environment): Vector[(Double, Double, Double)]

The Bode sweep: magnitude in decibels and unwrapped phase in degrees, over a logarithmically spaced grid (issue F_0004).

The Bode sweep: magnitude in decibels and unwrapped phase in degrees, over a logarithmically spaced grid (issue F_0004).

Unwrapping is the substance here, not the grid. bode's phase comes from atan2, whose principal value is (-pi, pi], so a swept curve jumps by a full turn wherever it crosses the branch cut — 1/(s+1)^3 tends to -270 degrees but a raw sweep reports +90. That jump is an artefact of the arctangent and not of the plant, and every reader who plots a raw sweep inherits it. This accumulates a turn whenever consecutive samples differ by more than half a turn, which restores the continuous curve.

dB and degrees rather than the raw pair, because that is what a Bode plot is; the raw magnitude and radian phase stay available from bode itself, so nothing is lost.

The one assumption worth stating: unwrapping cannot distinguish a genuine half-turn step from a grid too coarse to resolve a fast one, so a sparse sweep across a lightly damped resonance can unwrap the wrong way. That is inherent to unwrapping rather than particular to this implementation, and the remedy is points, not cleverness.

A frequency where the response is not finite — a pole on the imaginary axis — is dropped, mirroring scalar.sample; the unwrapping state carries across the gap, so the curve resumes rather than restarting.

Value parameters

env

bindings for any parameter the plant carries

g

the transfer function

points

how many samples, at least 2

v

the frequency variable

wMax

the highest angular frequency

wMin

the lowest angular frequency, strictly positive

Attributes

Returns

(omega, magnitude-in-dB, unwrapped-phase-in-degrees), ascending in omega

Impulse response L^-1{G(s)}. Declines for an improper G, as stepResponse does.

Impulse response L^-1{G(s)}. Declines for an improper G, as stepResponse does.

Value parameters

g

the transfer function

s

the frequency variable

t

the time variable, free in the result

Attributes

Returns

the response, or _ImpulseResponse unchanged when no rule applies

def isStable(g: _Expression, v: _Variable): Option[Boolean]

Continuous-time stability: every pole strictly in the left half-plane.

Continuous-time stability: every pole strictly in the left half-plane.

Decided from the poles rather than from a Routh table, a deliberate departure from the original plan. Routh's advantage is symbolic coefficients, but it carries well-known degenerate cases — a zero in the first column, or an identically zero row — each needing its own repair, and a mis-handled one yields a confident wrong verdict rather than a refusal. Root location has neither failure mode. Symbolic coefficients are refused here instead, which is the same answer Routh would have to give once scalar.sign could not decide, and routhTable remains available for the table itself.

Strict, so marginal stability is not stability: poles on the imaginary axis give false. An oscillator that never decays is not a stable system, and reporting it as one would be the confident wrong answer.

Value parameters

g

the transfer function

v

the frequency variable

Attributes

Returns

Some(true)/Some(false), or None when the poles cannot be determined

def isStableDiscrete(g: _Expression, v: _Variable): Option[Boolean]

Discrete-time stability: every pole strictly inside the unit circle.

Discrete-time stability: every pole strictly inside the unit circle.

The continuous test does not transfer. z = 0.5 is a stable discrete pole while the same value read as a continuous one is unstable, so applying the left-half-plane rule to a z-domain transfer function inverts the answer rather than degrading it.

Value parameters

g

the discrete transfer function

v

the z-domain variable

Attributes

Returns

Some(true)/Some(false), or None when the poles cannot be determined

def nyquist(g: _Expression, v: _Variable, w: Double): Option[(Double, Double)]

Nyquist point: the real and imaginary parts of G(i*w).

Nyquist point: the real and imaginary parts of G(i*w).

The same substitution as bode, reported in rectangular rather than polar form — the two are one computation presented two ways, not two computations.

Value parameters

g

the transfer function

v

the frequency variable

w

the angular frequency

Attributes

Returns

(real, imaginary), or None where the response is not finite

def nyquistSweep(g: _Expression, v: _Variable, wMin: Double, wMax: Double, points: Int, env: Environment): Vector[(Double, Double)]

The Nyquist sweep: the same grid as frequencyResponse, in rectangular coordinates.

The Nyquist sweep: the same grid as frequencyResponse, in rectangular coordinates.

One sweep presented two ways — the relationship bode and nyquist already have, and the reason both read the same logGrid rather than each building one. Unwrapping does not arise here: a point in the plane is unchanged by adding a turn to its argument.

Value parameters

env

bindings for any parameter the plant carries

g

the transfer function

points

how many samples, at least 2

v

the frequency variable

wMax

the highest angular frequency

wMin

the lowest angular frequency, strictly positive

Attributes

Returns

(real, imaginary) for each grid frequency, non-finite points dropped

def observable(a: _Expression, c: _Expression): Option[Boolean]

Is the pair (A, C) observable — can the output distinguish every state?

Is the pair (A, C) observable — can the output distinguish every state?

Observability of (A, C) is controllability of (Aᵀ, Cᵀ) — the duality, used here rather than restated, so the two can never disagree.

Value parameters

a

the state matrix

c

the output matrix

Attributes

Returns

Some(true)/Some(false), or None when either operand is not a dense matrix

Parallel connection: G + H.

Parallel connection: G + H.

Value parameters

g

first block

h

second block

v

the frequency variable

Attributes

Returns

the combined transfer function, normalised to a single rational

def poles(g: _Expression, v: _Variable): Option[Vector[_Value]]

Poles of G: the roots of its denominator.

Poles of G: the roots of its denominator.

Returns _Values rather than Doubles because a complex pole is the interesting case, not an error — an oscillatory mode is a conjugate pair, and reducing the result to reals would silently drop exactly the systems a control engineer cares about.

Value parameters

g

the transfer function

v

the frequency variable

Attributes

Returns

the poles, or None when G is not rational in v or the roots do not resolve

def routhTable(g: _Expression, v: _Variable): Option[Vector[Vector[Double]]]

The Routh array of G's denominator, for inspecting a stability margin by hand.

The Routh array of G's denominator, for inspecting a stability margin by hand.

Numeric coefficients only. isStable does not consult it — see the note there.

Value parameters

g

the transfer function

v

the frequency variable

Attributes

Returns

the array, row by row from the highest power down, or None

Series (cascade) connection: G * H.

Series (cascade) connection: G * H.

Value parameters

g

first block

h

second block

v

the frequency variable

Attributes

Returns

the combined transfer function, normalised to a single rational

Bundles (A, B, C, D) into the 1x4 row that represents a state-space model.

Bundles (A, B, C, D) into the 1x4 row that represents a state-space model.

Value parameters

a

state matrix

b

input matrix

c

output matrix

d

feedthrough matrix

Attributes

Returns

the model as a _Matrix node

Unit-step response L^-1{G(s)/s} — almost entirely a re-spelling of the inverse Laplace transform's repeated-pole machinery.

Unit-step response L^-1{G(s)/s} — almost entirely a re-spelling of the inverse Laplace transform's repeated-pole machinery.

Declines for an improper G (numerator degree above denominator degree). Such a response contains an impulsive term at t = 0 that the inverse-transform tier does not represent, so returning only the smooth part would be a confidently incomplete answer rather than a refusal.

Value parameters

g

the transfer function

s

the frequency variable

t

the time variable, free in the result

Attributes

Returns

the response, or _StepResponse unchanged when no rule applies

def zeros(g: _Expression, v: _Variable): Option[Vector[_Value]]

Zeros of G: the roots of its numerator. See poles for the _Value result type.

Zeros of G: the roots of its numerator. See poles for the _Value result type.

Value parameters

g

the transfer function

v

the frequency variable

Attributes

Returns

the zeros, or None when G is not rational in v