Validation¶
Model users and model developers are usually distinct persons. Therefore, users may not be aware of model limitations. Moreover, only a few model parameters may be meaningful to them.
To address these issues, CoSApp allows model developers to specify the validity range and visibility scope of all variables. This section focuses on the validation feature.
The concept¶
A validity range and a limit range can be defined on all variables in CoSApp.
The validity range defines an interval within which the model can be used with confidence. The yellow areas between validity range and limits define values for which validation by an expert is required. Finally, values beyond the limits should be avoided as the reliability of the model is unknown.
These ranges are only provided as information, and are not enforced during the execution of a model. Therefore, validation should be performed a posteriori. This behavior has been chosen to limit the constraints on mathematical systems within a model.
Defining validation criteria¶
CoSApp variables are defined in Port or in System instances. Validity ranges can be specified in the setup of these classes.
For example in a Port:
[2]:
from cosapp.base import System, Port
from cosapp.drivers import ValidityCheck
class MyPort(Port):
def setup(self):
self.add_variable('v', 22.,
valid_range = (-2, 5),
invalid_comment = 'design rule abc forbids "a" outside [-2, 5]',
limits = (-10, None),
out_of_limits_comment = 'The model has not been tested outside [-10, [',
)
And in a System:
[3]:
class MySystem(System):
def setup(self):
# Definition of validation criteria on a inward variable 'd'
self.add_inward('d', 7.,
valid_range = (-2, 5),
invalid_comment = "design rule abc forbid 'd' outside [-2, 5]",
limits = (None, 10),
out_of_limits_comment = "The model has not been tested outside ]-inf, 10]",
)
# Overwrite the default validation criteria on the variable 'v' of port 'port_in'
port_in = self.add_input(MyPort, 'port_in', {
'v': dict( # Variable name in MyPort
valid_range = (0, 3),
invalid_comment = "design rule blah-blah recommends 'v' be within [0, 3]",
limits = (None, 10),
out_of_limits_comment = "The model has not been tested outside ]-inf, 10]"
)
})
If no range are provided, the variable is always considered as valid. In case the value is not valid but falls within the limits, the invalid_comment will be shown to inform the user on the reason behind the validity range. If the value is out of limits, the out_of_limits_comment will be displayed.
If one end of the range is unbounded, users may specify a None value. For example, a non-negative variable will correspond to limits = (0, None).
Displaying validation criteria¶
You can get relevant information by displaying the documentation of the Port or System object.
[4]:
from cosapp.tools import display_doc
display_doc(MyPort)
[5]:
display_doc(MySystem)
Testing validation criteria¶
As mentioned earlier, validity ranges are not enforced during simulations. To check the validity of the results after execution, you may add special driver ValidityCheck to the master System, which will produce a post-run data validity report.
Values falling outside the prescribed limits will be gathered in the ERROR section; those between the valid range and the limits will appear in the WARNING section. Valid values will simply be omitted.
[6]:
s = MySystem('master_system')
s.add_driver(ValidityCheck('validation'))
s.run_drivers()
inwards.d = 7 not in [-2, 5] - design rule abc forbid 'd' outside [-2, 5]
port_in.v = 22 not in [-inf, 10] - The model has not been tested outside ]-inf, 10]
Example¶
We will demonstrate this concept on the circuit example.
The physical problem consists in determining the two node voltages which satisfy electric current conservation.
[7]:
from cosapp.base import System, Port, Scope, ScopeError
from cosapp.drivers import NonLinearSolver, NonLinearMethods, ValidityCheck
import numpy as np
Set validity criteria¶
By default, the current intensity will be defined as positive.
In Resistor components, we add a validity range on the current intensity pretending an non-constant behavior of the resistance value for high current.
We can look at the documentation to check that the validation criteria are taken into account
[10]:
from cosapp.tools import display_doc
display_doc(Resistor)
Building the circuit¶
[11]:
class Diode(System):
"""Diode model
The current intensity flowing through the diode is calculated based on
$ I = I_s \\exp \\left( \\dfrac{V_{in} - V_{out}}{V_t} - 1 \\right) $
"""
tags = ['cosapp', 'developer']
def setup(self):
self.add_input(Voltage, 'V_in')
self.add_input(Voltage, 'V_out')
self.add_output(Intensity, 'I')
self.add_inward('Is', 1e-15, desc='Saturation current in Amps')
self.add_inward('Vt', .025875, scope=Scope.PROTECTED, desc='Thermal voltage in Volts')
self.add_outward('deltaV')
def compute(self):
self.deltaV = self.V_in.V - self.V_out.V
self.I.I = self.Is * np.exp(self.deltaV / self.Vt - 1.)
class Node(System):
def setup(self, n_in=1, n_out=1):
self.add_property('n_in', int(n_in))
self.add_property('n_out', int(n_out))
if min(self.n_in, self.n_out) < 1:
raise ValueError("Node needs at least one incoming and one outgoing current")
for i in range(self.n_in):
self.add_input(Intensity, f"I_in{i}")
for i in range(self.n_out):
self.add_input(Intensity, f"I_out{i}")
self.add_inward('V')
self.add_unknown('V') # Iterative variable
self.add_outward('sum_I_in', 0., desc='Sum of all incoming currents')
self.add_outward('sum_I_out', 0., desc='Sum of all outgoing currents')
self.add_equation('sum_I_in == sum_I_out', name='V')
def compute(self):
self.sum_I_in = sum(self[f"I_in{i}.I"] for i in range(self.n_in))
self.sum_I_out = sum(self[f"I_out{i}.I"] for i in range(self.n_out))
class Source(System):
def setup(self, I = 0.1):
self.add_inward('I', I)
self.add_output(Intensity, 'I_out', {'I': I})
def compute(self):
self.I_out.I = self.I
class Ground(System):
def setup(self, V = 0.):
self.add_inward('V', V)
self.add_output(Voltage, 'V_out', {'V': V})
def compute(self):
self.V_out.V = self.V
class Circuit(System):
def setup(self):
n1 = self.add_child(Node('n1', n_in=1, n_out=2), pulling={'I_in0': 'I_in'})
n2 = self.add_child(Node('n2'))
R1 = self.add_child(Resistor('R1', R=100.), pulling={'V_out': 'Vg'})
R2 = self.add_child(Resistor('R2', R=10000.))
D1 = self.add_child(Diode('D1'), pulling={'V_out': 'Vg'})
self.connect(R1.V_in, n1.inwards, 'V')
self.connect(R2.V_in, n1.inwards, 'V')
self.connect(R1.I, n1.I_out0)
self.connect(R2.I, n1.I_out1)
self.connect(R2.V_out, n2.inwards, 'V')
self.connect(D1.V_in, n2.inwards, 'V')
self.connect(R2.I, n2.I_in0)
self.connect(D1.I, n2.I_out0)
Solving the valid problem¶
First we will solve the initial problem with a intensity source of 0.1 A and check that the results are valid.
[12]:
p = System('model')
# Plug the source, the ground and the circuit
p.add_child(Source('source', I=0.1))
p.add_child(Ground('ground', V=0.))
p.add_child(Circuit('circuit'))
p.connect(p.source.I_out, p.circuit.I_in)
p.connect(p.ground.V_out, p.circuit.Vg)
# Add numerical solver and validation
p.add_driver(NonLinearSolver('solver', method=NonLinearMethods.POWELL))
p.add_driver(ValidityCheck('validation'))
# Execute the problem
p.run_drivers()
# sanity check: should sum to -.1 Amps
print('Sanity check : 0.1 = ? ', p['circuit.R1.I.I'] + p['circuit.D1.I.I'])
Sanity check : 0.1 = ? 0.10000000000012288
Solving invalid problems¶
Then we will solve the problem with a high intensity will be set to trigger validation criteria on the resistors.
[13]:
p = System('model')
# Plug the source, the ground and the circuit
p.add_child(Source('source', I=50.))
p.add_child(Ground('ground'))
p.add_child(Circuit('circuit'))
p.connect(p.source.I_out, p.circuit.I_in)
p.connect(p.ground.V_out, p.circuit.Vg)
# Add numerical solver and validation
p.add_driver(NonLinearSolver('solver', method=NonLinearMethods.POWELL))
p.add_driver(ValidityCheck('validation'))
# Init to help the numerical solver
p.circuit.n1.V = 1000
# Execute the problem
p.run_drivers()
# sanity check
print('Sanity check : 50 = ? ', p['circuit.R1.I.I'] + p['circuit.D1.I.I'])
circuit.R1.I.I = 49.505 not in [-25, 25] - The resistance may not be constant for currents exceeding 25 A
Sanity check : 50 = ? 50.0
Finally we will solve the problem with a intensity source of -0.1 A. That value is not consider has valid.
[14]:
p = System('model')
# Plug the source, the ground and the circuit
p.add_child(Source('source', I=-0.1))
p.add_child(Ground('ground'))
p.add_child(Circuit('circuit'))
p.connect(p.source.I_out, p.circuit.I_in)
p.connect(p.ground.V_out, p.circuit.Vg)
# Add numerical solver and validation
p.add_driver(NonLinearSolver('solver', method=NonLinearMethods.POWELL))
p.add_driver(ValidityCheck('validation'))
# Execute the problem
p.run_drivers()
# sanity check: should sum to -0.1 A
print('Sanity check: -0.1 = ? ', p['circuit.R1.I.I'] + p['circuit.D1.I.I'])
source.I_out.I = -0.1 not in [0.0, inf] - Current can only flow in one direction.
circuit.n1.I_in0.I = -0.1 not in [0.0, inf] - Current can only flow in one direction.
circuit.n1.I_out0.I = -0.1 not in [0.0, inf] - Current can only flow in one direction.
circuit.n1.I_out1.I = -2.076e-21 not in [0.0, inf] - Current can only flow in one direction.
circuit.I_in.I = -0.1 not in [0.0, inf] - Current can only flow in one direction.
circuit.n2.I_in0.I = -2.076e-21 not in [0.0, inf] - Current can only flow in one direction.
circuit.n2.I_out0.I = -2.076e-21 not in [0.0, inf] - Current can only flow in one direction.
Sanity check: -0.1 = ? -0.1