Product
Introducing License Enforcement in Socket
Ensure open-source compliance with Socket’s License Enforcement Beta. Set up your License Policy and secure your software!
The primary goal of CoSApp is to help technical departments in the design of complex systems. To do so, the framework allows the simulation of various systems representing the different parts of the final product in a common environment. The benefit is the ability for each subsystem team to carry out design study with a direct feedback at product level.
The main features are:
Couple your simulation models with CoSApp to get immediate impact on main product variables and iterate to converge on a better design.
All systems can share design parameters associated with an acceptable range. You can take advantage of those limited degrees of freedom without fear of breaking your collaborators' work.
CoSApp solvers can be combined into versatile, customized workflows that fit specific simulation intents.
Have a look at the introduction, containing many tutorials!
This code is the property of Safran SA. It uses code coming from various open-source projects (see LICENSE file).
If you use CoSApp, please cite us!
Lac et al. (2024), CoSApp: a Python library to create, simulate and design complex systems, Journal of Open Source Software 9(94), 6292.
BibTeX entry:
@article{Lac.etal:joss2024,
author={Étienne Lac and Guy {De Spiegeleer} and Adrien Delsalle and Frédéric Collonval and Duc-Trung Lê and Mathias Malandain},
title={CoSApp: a Python library to create, simulate and design complex systems},
journal={Journal of Open Source Software},
year={2024},
volume={9},
number={94},
pages={6292},
doi={10.21105/joss.06292},
publisher={The Open Journal}
}
Run a Jupyter Lab instance with binder to try out CoSApp features through examples.
A JupyterLite image including CoSApp is now available in the main README file (MR #300).
Fix a bug with primary event initialization (MR #293).
Improved API for partial connections: name mappings can now be given as lists mixing variable names and dictionaries, which is convenient when most variable names are identical, and only a few differ (MRs #294, #295 & #306). Example:
from cosapp.base import System
class SomeSystem(System):
def setup(self):
foo = self.add_child(Foo('foo'), pulling=['a', 'b', {'c': 'c_foo'}])
bar = self.add_child(Bar('bar'))
self.connect(foo, bar, ['x', 'y', {'z': 'v'}])
New utility function cosapp.tools.views.show_tree
displaying the hierarchical tree of a system similar to a folder tree in a filesystem (MRs #296-#298).
Residue.variables
providing the names of the variables involved in the residue (MR #299).numpy
v1, until full migration to v2 (MR #310).self.problem.clear()
during transitions, for instance, no longer affects time-dependent unknowns such as transients.RunSingleCase
(MR #279).pytest
version due to a bug in version 8.1 (MR #280).sphinx
< 7.3 in the documentation building environment, owing to an incompatibility with sphinx-mdinclude
(MR #289). This is a temporary patch until root cause is fixed.NonLinearSolver
for systems with rates (MR #268).Publication of an article on CoSApp in the Journal of Open-Source Software, referenced to version 0.15.4 (MR #271).
NonLinearSolver
(MR #261).System
(MR #259).pythonfmu
< 0.6.3 in dependency list, to prevent a crash during tests (MR #262). Temporary fix until root cause is identified.VisJs
rendering of sub-systems (MR #252).EvalString
(MR #253).TypeError
when passing pulling
argument as a tuple in System.add_child
(MR #256).MathematicalProblem
objects now indicates the number of unknowns and equations, for a better readability (MR #249).execution_index
in System.add_child
and Driver.add_child
can now take a negative value, with a behaviour following that of list.insert
(MR #250).ExplicitTimeRecorder.event_data
, when no recorder is set (MR #245).NonLinearSolver
with NumPy array residues (MR #248).RunSingleCase
subdriver runner
in NonLinearSolver
drivers (MR #239).swap_system
to replace on the fly a subsystem by another System
instance (MR #238).Improved performance, through a revised clean/dirty mechanism (MR #215).
Possibility to add a contextual description to sub-systems and ports of a system, as well as sub-drivers (MR #216). This feature is useful for automatic documentation tools, and has been included in the Markdown representation of systems (also used in function cosapp.tools.display_doc
). Example:
from cosapp.base import System
from my_module import FlowPort
class MySystem(System):
def setup(self):
self.add_input(FlowPort, "fl_in1", desc="Primary inlet flow port")
self.add_input(FlowPort, "fl_in2", desc="Secondary inlet flow port")
self.add_output(FlowPort, "fl_out")
New hook function _parse_module_config
, returning pre-defined settings for cosapp.tools.parse_module
(MR #218). This allows module maintainers to simply call
from cosapp.tools import parse_module
import my_module
parse_module(my_module)
instead of, e.g.,
parse_module(
my_module,
ctor_config={
"ComplexSystem1": [
dict(n=1, foo=0.5),
dict(n=2, foo=0.1),
],
"ComplexSystem2": [
dict(xi=0.0, __alias__="ComplexSystem2_a"),
dict(xi=1.0, __alias__="ComplexSystem2_b"),
],
},
excludes=["Foo*", "*Bar?"],
)
provided my_module._parse_module_config()
returns a dictionary specifying the values of ctor_config
, excludes
, etc.
Make SolverResults
a dataclass
, for easier handling of NonLinearSolver.results
, e.g. (MR #220).
Expose attribute problem
in system setup (MR #221). Previously, problem
was only exposed in method System.transition
.
NonLinearSolver
log message (MR #214).cosapp.tools.parse_module
(MR #219).cosapp.tools.display_doc
for classes with setup arguments (MR #208).Note: extensive use of self-documenting f-strings (introduced in Python 3.8) has made tutorials incompatible with Python 3.7.
parse_module
in cosapp.tools
collecting all system and port classes within a Python module. This parser, generating a JSON file containing the description of all CoSApp symbols, is primarily meant to be used for constructional GUI applications, developed separately (MRs #192 & #209).The code is now tested for Python 3.8, 3.9 and 3.10. Support of Python 3.7 is thus officially dropped, although no version-specific Python code was introduced in this version of CoSApp.
Module connectors
moved from cosapp.core
to cosapp.ports
(MR #189).
New "direct" (with no unit conversion) connector classes PlainConnector
, CopyConnector
and DeepCopyConnector
, in cosapp.ports.connectors
(MR #188).
New method MathematicalProblem.is_empty()
, equivalent to shape == (0, 0)
(MR #186).
Improved VisJs graph rendering, by limiting node size for long system names (MR #184).
New utility functions get_state
and set_state
in cosapp.utils
, for quick system data recovery (MR #193):
from cosapp.utils import get_state, set_state
s = SomeSystem('s')
# ... many design steps, say
# Save state in local object
designed = get_state(s)
s.drivers.clear()
s.add_driver(SomeDriver('driver'))
try:
s.run_drivers()
except:
# Recover previous state
set_state(s, designed)
Functions radians
, degrees
and arctan2
/atan2
have been added to the scope of EvalString
objects, and can therefore be used in equations, e.g. (MR #178).
Recorders can now record constant properties (MR #181).
Deprecation of System.get_unsolved_problem
in favour of new method assembled_problem
(MR #174).
Inner off-design problem of systems is now exposed as attribute problem
, but only within the transition
method (MR #174). This allows users to add or remove off-design constraints during event-driven transitions, while keeping this property inaccessible the rest of the time.
from cosapp.base import System
from math import sin, cos
class SomeSystem(System):
def setup(self):
self.add_inward('x', 0.0)
self.add_inward('y', 0.0)
self.add_outward('z', 0.0)
a = self.add_event('event_a', trigger='x > y')
b = self.add_event('event_b', trigger='x < y')
def compute(self):
self.z = cos(self.x) * sin(self.y)
def transition(self):
offdesign = self.problem
if self.event_a.present:
offdesign.clear()
offdesign.add_equation('z == 0.5').add_unknown('x')
if self.event_b.present:
offdesign.clear()
None
variables (MR #172).System.add_child
when a pulling
error is raised (MR #197).System.new_problem
to facilitate the creation of dynamic design methods, e.g. (MR #162).NonLinearSolver
(MRs #148 and #152).Optimizer.set_objective
is deprecated, in favour of set_minimum
and set_maximum
(MR #150).System.add_child
, add_driver
, etc. (MR #145).jupyterlab
3.4 (MR #149).pytest
7.1 (MR #142).Simplification of driver Optimizer
:
runner
(MR #136). This change introduces new methods set_objective
, add_unknowns
and add_constraints
in driver Optimizer
.Optimizer.add_constraints
(MR #138).Before:
from cosapp.drivers import Optimizer
s = SomeSystem('s')
optim = s.add_driver(Optimizer('optim'))
optim.runner.set_objective('cost')
optim.runner.add_unknown(['a', 'b', 'p_in.x'])
# Enter constraints as non-negative expressions:
optim.runner.add_constraints([
"b - a", # b >= a
"a", # a >= 0
"1 - a", # a <= 1
])
optim.runner.add_constraints(
"p_out.y",
inequality = False, # p_out.y == 0
)
s.run_drivers()
After:
optim.set_objective('cost')
optim.add_unknown(['a', 'b', 'p_in.x'])
optim.add_constraints(
"b >= a",
"0 <= a <= 1",
"p_out.y == 0",
)
from cosapp.base import Port, System
class XyzPort(Port):
def setup(self):
self.add_variable('x')
self.add_variable('y')
self.add_variable('z')
class SomeSystem(System):
def setup(self):
self.add_input(XyzPort, 'p_in')
self.add_output(XyzPort, 'p_out')
def compute(self):
self.p_out.set_from(self.p_in) # assign values from `p_in`
self.p_out.z = 0.0
s = SomeSystem('s')
# Multi-variable setter `set_values`
s.p_in.set_values(x=1, y=-0.5, z=0.1)
s.run_once()
# Dict-like (key, value) iterator `items`:
for varname, value in s.p_out.items():
print(f"p_out.{varname} = {value})
RunOnce
and RunSingleCase
recorders with hold=False
(MR #130).Implementation of multimode systems and hybrid continuous/discrete time solver (MRs #100, #103, #105-#108, #110-#121):
System.transition
describing system transition upon the occurrence of events.ExplicitTimeDriver
.Possibility to specify a stop criterion in time simulation scenarios (MR #107).
New module cosapp.base
(MR #96) containing base classes for user-defined classes (in particular, Port
, System
and Driver
). Also contains BaseConnector
, base class for custom connectors (see "User-defined and peer-to-peer connectors" below), as well as CoSApp-specific exceptions ScopeError
, UnitError
and ConnectorError
.
Note: Port
, System
and Driver
can still be imported from cosapp.ports
, cosapp.systems
and cosapp.drivers
, respectively.
Public API cosapp.base.SurrogateModel
to define custom surrogate models used in System.make_surrogate
(MR #97).
Pre-defined models have been moved to module cosapp.utils.surrogate_models
.
System-to-system connections (MR #94).
class LegacyPortToPort(System):
def setup(self):
a = self.add_child(ModelA('a'))
b = self.add_child(ModelB('b'))
# Explicit port-to-port connections
self.connect(a.p_in, b.p_out)
self.connect(a.outwards, b.inwards, {'y': 'x'})
class Alternative(System):
"""Same as `LegacyPortToPort`, with alternative connection syntax"""
def setup(self):
a = self.add_child(ModelA('a'))
b = self.add_child(ModelB('b'))
# Alternative syntax: connect systems, with port or variable mapping
self.connect(a, b, {'p_in': 'p_out', 'y': 'x'})
import numpy
from copy import deepcopy
from cosapp.base import Port, System, BaseConnector
class DeepCopyConnector(BaseConnector):
"""User-defined deep-copy connector"""
def transfer(self) -> None:
source, sink = self.source, self.sink
for target, origin in self.mapping.items():
value = getattr(source, origin)
setattr(sink, target, deepcopy(value))
class CustomPort(Port):
def setup(self):
self.add_variable('x', 0.0)
self.add_variable('y', 1.0)
class Connector(BaseConnector):
"""Connector for peer-to-peer connections"""
def transfer(self) -> None:
source, sink = self.source, self.sink
sink.x = source.y
sink.y = -source.x
class MyModel(System):
def setup(self):
self.add_input(CustomPort, 'p_in')
self.add_output(CustomPort, 'p_out')
self.add_inward('entry', numpy.identity(3))
self.add_outward('exit', numpy.zeros_like(self.entry))
class Assembly(System):
def setup(self):
a = self.add_child(MyModel('a'))
b = self.add_child(MyModel('b'))
self.connect(a, b, {'exit', 'entry'}, cls=DeepCopyConnector)
self.connect(a.p_in, b.p_out) # will use CustomPort.Connector
engine = Turbofan('engine')
solver = engine.add_driver(NonLinearSolver('solver'))
# Add design points:
takeoff = solver.add_child(RunSingleCase('takeoff'))
cruise = solver.add_child(RunSingleCase('cruise'))
# Unknowns defined at solver level regarded as *design* unknowns
solver.add_unknown(['fan.diameter', 'core.turbine.inlet.area'])
# Local off-design equations can be directly defined at case level
takeoff.add_equation('thrust == 1.2e5')
cruise.add_equation('Mach == 0.8')
tree()
for systems and drivers, yielding all elements in a composite tree (MR #68).head = CompositeSystem('head')
bottom_to_top = [s.name for s in head.tree()]
top_to_bottom = [s.name for s in head.tree(downwards=True)]
from cosapp.patterns.visitor import Visitor, send as send_visitor
class DataCollector(Visitor):
def __init__(self):
self.data = {}
def visit_system(self, system):
key = system.full_name()
self.data.setdefault(key, {})
self.data[key]['children'] = [
child.name for child in system.children.values()
]
send_visitor(self, system.inputs.values())
def visit_port(self, port):
# specify what to do with a port
def visit_driver(self, driver):
# specify what to do with a driver
head = CompositeSystem('head')
collector = DataCollector()
send_visitor(collector, head.tree())
print(collector.data)
System.exec_order
a view on System.children
dictionary keys, rather than an independent attribute (MR #70). Execution order can still be specified, via a dedicated setter for exec_order
.add_target
(MR #61).jac is None
after converting it as a numpy
array (MR #56).PortMarkdownFormatter
(MR #59).New binder container, allowing anyone to run interactively the tutorials used in the online documentation (MR #30 and #36).
Deferred equations to set targets:
New method add_target
, defining a deferred equation on on a target variable (MR #48).
In effect, add_target
creates an equation whose right-hand side is evaluated dynamically prior to each execution of the nonlienar solver.
In the example below, the feature is illustrated in design mode. Outward z
is a function of two independent variables x
and y
.
When design method 'target_z'
is activated, the actual value of z
, set interactively, is used as a target value, with unknown y
:
class SystemWithTarget(System):
def setup(self):
self.add_inward('x', 1.0)
self.add_inward('y', 1.0)
self.add_outward('z', 1.0)
# Define design problem with a target on `z`
design = self.add_design_method('target_z')
design.add_unknown('y').add_target('z')
def compute(self):
self.z = self.x * self.y**2
s = SystemWithTarget('s')
solver = s.add_driver(NonLinearSolver('solver', tol=1e-9))
# Activate design method 'target_z': outward `z` becomes a target
solver.runner.design.extend(s.design_methods['target_z'])
s.x = 0.5
s.y = 0.5
s.z = 2.0 # set target
s.run_drivers()
assert s.y == pytest.approx(2) # solution of x * y**2 == 2
assert s.z == pytest.approx(2)
s.z = 4.0 # dynamically set new target
s.run_drivers()
assert s.y == pytest.approx(np.sqrt(8)) # solution of x * y**2 == 4
assert s.z == pytest.approx(4)
Targets can be also be declared in off-design mode, by calling self.add_target(...)
in System.setup
.
System
, Driver
, and design methods (MR #41).time
in DataFrame
recorders attached to a time driver (MR #34).MonteCarlo
driver (MR #37).conda
by mamba
in CI scripts (MR #39).rate
type inference (MR #44).point = PointMass('point')
driver = point.add_driver(RungeKutta(order=3, time_interval=(0, 2), dt=0.01))
recorder = driver.add_recorder(recorders.DataFrameRecorder(
includes=['x', 'a', 'norm(v)']), # norm(v) will be recorded in DataFrame
period=0.1,
)
SystemSurrogate
(MR #15).NumericalSolver
(MR #22).ArithmeticError
when an unknown is declared several time (MR #18).numpy
(MR #20 and #24).numpy
in NonLinearSolver
(MR #19).pandas
and xlrd
(MR #21).System.make_surrogate
(MR #3 and #12):plane = Aeroplane('plane') # system with subsystems engine1 and engine2
# Say engine systems have one input parameter `fuel_rate`
# and possibly several outputs, and many sub-systems
# Create training schedule for input data
doe = pandas.DataFrame(
# loads of input data
columns=['fuel_rate', 'fan.diameter', ..] # input names
)
plane.engine1.make_surrogate(doe) # generates output data and train model
plane.run_once() # executes the surrogate model of `engine1` instead of original compute()
# dump model to file
plane.engine1.dump_surrogate('engine.bin')
# load model into `engine2`:
plane.engine2.load_surrogate('engine.bin')
# deactivate surrogate model on demand
plane.engine1.active_surrogate = plane.engine2.active_surrogate = False
Add several US-common unit conversions (MR #2).
New method to export cosapp system structure into a dictionary (MR #5)
Make recorders capture port and system properties (MR #8).
Fix Module/System naming bug: 'inwards' and 'outwards' are allowed as Module/System names (MR #9).
Broad code quality improvement (MR #11).
typing.NoReturn
by None
when appropriate.DeprecationWarning
raised by numpy
in class Variable
.str.join()
for just two elements.Global rewording of tutorial notebooks, including a few error fixes.
First open-source version. No major code change; mostly updates of license files, URLs in docs, and CI scripts.
RunOnce
driver, preventing undue call to run_once
method.System.add_property
allowing users to create read-only properties.AssignString
of the kind 'x = [0, 1, 2]'
won't change variable x
into an array of integers, if x
is declared as an array of floats.TimeStackUnknown
not able to stack transient variables defined on a children System or with partially pulled transient variable.rate
attributes in systems.setup_run
are called.IterativeConnector
(it equals 1. now)system = MySystem('something') # system with transient variables x and v
driver = system.add_driver(RungeKutta(time_interval=(0, 2), dt=0.01, order=3))
driver.set_scenario(
init = {'x': 0.5, 'v': 0}, # initial conditions
values =
{
'omega': 0.7,
'F_ext': '0.6 * cos(omega * t)' # explicit time-dependency
}
)
DEBUG
level will now display the call stack through the systems and driversTimeDriver
notebook in tutorials).Ports:
add_variable("x", units="m", types=Number)
=> add_variable("x", unit="m", dtype=Number)
freeze
=> removedunfreeze
=> replaced by add_unknown
in Systems and Driversconnect_to
=> replaced by connect
at system levelSystems:
time_ref
is no longer an argument of method compute
:
def compute(self, time_ref):
=> def compute(self):
Create a new connection between a.in1
and b.out
:
self.a.in1.connect_to(self.b.out)
=> self.connect(self.a.in1, self.b.out)
add_residues
=> add_equation
set_numerical_default
=> Pass keyword to add_unknown
add_inward("x", units="m", types=Number)
=> add_inward("x", unit="m", dtype=Number)
add_outward("x", units="m", types=Number)
=> add_outward("x", unit="m", dtype=Number)
Drivers:
add_unknowns(maximal_absolute_step, maximal_relative_step, low_bound, high_bound)
=> add_unknown(max_abs_step, max_rel_step, lower_bound, upper_bound)
add_equations
=> add_equation
Equations are now represented by a unique string, instead of two strings (left-hand-side, right-hand-side):
add_equations("a", "b")
=> add_equation("a == b")
add_equations([("x", "2 * y + 1"), ("a", "b")])
=> add_equation(["x == 2 * y + 1", "a == b"])
For NonLinearSolver
:
fatol
and xtol
=> tol
maxiter
=> max_iter
For Optimizer
:
ftol
=> tol
maxiter
=> max_iter
COSAPP_CONFIG_DIR
MonteCarlo:
Montecarlo
=> MonteCarlo
Montecarlo.add_input_vars
=> MonteCarlo.add_random_variable
Montecarlo.add_response_vars
=> MonteCarlo.add_response
MonteCarlo has been improved by using Sobol random generator
Viewers code on System
is moved in a subpackage of cosapp.tools
Residue reference is now calculated only once
Various bug fix
Variable
class to manage variable attributeswatchdog
is now optional$HOME/.cosapp.d
get_latest_solution
=> save_solution
load_solver_solution
=> load_solution
This release introduces lots of API changes:
cosapp.ports
cosapp.systems
cosapp.drivers
cosapp.recorders
cosapp.tools
cosapp.notebook
(! this is now a separated package)data
have been renamed in inwards
and add_data
in add_inward
locals
have been renamed in outwards
and add_locals
in add_outward
BaseRecorder.record_iteration
renamed in BaseRecorder.record_state
cosapp.notebook
has been moved to an independent package cosapp_notebook
. But it is still accessible from cosapp.notebook
.cosapp.core.signal
)
Module.setup_ran
: Signal emitted after the call_setup_run
executionModule.computed
: Signal emitted after the full compute
stack (i.e.: _postcompute
)Module.clean_ran
: Signal emitted after the call_clean_run
executionBaseRecorder.state_recorded
: Signale emitted after the record_state
executionRunSingleCase
MonteCarlo
driverSystem
and Driver
have now a common ancestor Module
=> Driver
variables are now stored as data or localsSystem
connections based on N2 graph (syntax: cosapp.viewmodel(mySystem)
)API changes: System.add_driver
and Driver.add_child
take now an instance of Driver
System
FAQs
CoSApp, the Collaborative System Approach.
We found that cosapp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Product
Ensure open-source compliance with Socket’s License Enforcement Beta. Set up your License Policy and secure your software!
Product
We're launching a new set of license analysis and compliance features for analyzing, managing, and complying with licenses across a range of supported languages and ecosystems.
Product
We're excited to introduce Socket Optimize, a powerful CLI command to secure open source dependencies with tested, optimized package overrides.