Skip to content

Commit 4bedfbd

Browse files
committed
System compile streamline & separate simulator
1 parent 499a566 commit 4bedfbd

File tree

7 files changed

+226
-295
lines changed

7 files changed

+226
-295
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ share/python-wheels/
2626
*.egg
2727
MANIFEST
2828

29+
# Examples
30+
examples/pbdm
31+
examples/bugs
32+
2933
# PyInstaller
3034
# Usually these files are written by a python script from a template
3135
# before PyInstaller builds the exe, so as to inject date/other infos into it.

examples/ported_example.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
FunctionalPortedObject,
99
VariablePortedObject,
1010
)
11-
from psymple.system import System
11+
from psymple.system import System, Simulation
1212

1313

1414
##### Temperature model
@@ -103,28 +103,19 @@
103103
children=[temp, pred, prey, pred_prey],
104104
variable_ports=["pred_n", "prey_n"],
105105
variable_wires=[
106-
(["pred.n", "pred_prey.pred"], "pred_n"),
107-
(["prey.n", "pred_prey.prey"], "prey_n"),
106+
(["pred.n", "pred_prey.pred"], None, "pred_n"),
107+
(["prey.n", "pred_prey.prey"], None, "prey_n"),
108108
],
109109
directed_wires=[("temp.temp", "pred.temp"), ("temp.temp", "prey.temp")],
110110
)
111111

112-
compiled = sys.compile()
112+
S = System(sys)
113113

114-
print(compiled.variable_ports, compiled.internal_parameter_assignments)
114+
sim = Simulation(S, solver="discrete_int")
115115

116-
var, par = compiled.get_assignments()
117-
sys = System(variable_assignments=var, parameter_assignments=par)
116+
sim.variables["pred_n"].time_series = [50]
117+
sim.variables["prey_n"].time_series = [100]
118118

119-
print(var)
119+
sim.simulate(10, n_steps=400)
120120

121-
sys.variables["pred_n"].time_series = [50]
122-
sys.variables["prey_n"].time_series = [100]
123-
124-
125-
for var in sys.variables:
126-
print(f"d({var.symbol})/dT = {var.update_rule.equation}")
127-
128-
sys.simulate(t_end=365, n_steps=1000, mode="discrete")
129-
130-
sys.plot_solution({"pred_n", "prey_n"})
121+
sim.plot_solution({"pred_n", "prey_n"})

psymple/ported_objects.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def to_update_rule(self, variables=Variables(), parameters=Parameters()):
7878
TODO: This specific implementation is a relic from the System implementation from
7979
before and should probably be streamlined.
8080
"""
81-
return UpdateRule(self.symbol_wrapper, self.expression, variables, parameters)
81+
return UpdateRule(self.expression, variables, parameters)
8282

8383
def __repr__(self):
8484
return f"{type(self).__name__} {self.name} = {self.expression}"
@@ -110,7 +110,6 @@ def __repr__(self):
110110

111111
def combine(self, other):
112112
# TODO: check description and initial value for consistency
113-
# print(self.variable.symbol, other.variable.symbol)
114113
# assert self.variable.symbol == other.variable.symbol
115114
# TODO: Check if we want to mutate this assignment, or rather produce a new one
116115
self.expression += other.expression
@@ -346,7 +345,6 @@ def add_variable_ports(self, *ports: VariablePort | dict | str):
346345
"""
347346
for port_info in ports:
348347
port = self.parse_port_entry(port_info, VariablePort)
349-
print(f"adding port {port.name}")
350348
self.variable_ports[port.name] = port
351349

352350
def parse_assignment_entry(
@@ -500,7 +498,6 @@ def add_variable_assignments(
500498
internal_variables = set(self.internals.keys())
501499
variable_ports = set(self.variable_ports.keys())
502500
input_ports = set(self.input_ports.keys())
503-
print(free_symbols, internal_variables, input_ports)
504501
undefined_ports = free_symbols - internal_variables - variable_ports - input_ports
505502
self.add_input_ports(*undefined_ports)
506503

@@ -1129,7 +1126,6 @@ def set_input_parameters(self, parameter_assignments=[]):
11291126
# without full recompilation
11301127
assg_dict = {}
11311128
for assg in parameter_assignments:
1132-
# print(str(assg.parameter.symbol))
11331129
assg_dict[str(assg.symbol)] = assg
11341130
for name, port in self.input_ports.items():
11351131
if name in assg_dict:

psymple/system.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from psymple.ported_objects import (
2121
ParameterAssignment,
2222
DifferentialAssignment,
23+
PortedObject,
2324
)
2425

2526

@@ -28,6 +29,182 @@ class PopulationSystemError(Exception):
2829

2930

3031
class System:
32+
def __init__(self, ported_object):
33+
self.variables = {}
34+
self.parameters = {}
35+
36+
assert isinstance(ported_object, PortedObject)
37+
compiled = ported_object.compile()
38+
39+
self.create_time_variable()
40+
41+
variable_assignments, parameter_assignments = compiled.get_assignments()
42+
43+
variables, parameters = self.get_symbol_containers(variable_assignments, parameter_assignments)
44+
self.create_simulation_variables(variable_assignments, variables + self.time, parameters)
45+
self.create_simulation_parameters(parameter_assignments, variables + self.time, parameters)
46+
self.update_update_rules()
47+
48+
def create_time_variable(self):
49+
# At the moment the only global variable is time
50+
self.time = SimVariable(Variable(T, 0.0, "system time"))
51+
self.time.set_update_rule(
52+
SimUpdateRule(
53+
#self.time,
54+
equation="1",
55+
variables=Variables(),
56+
parameters=Parameters(),
57+
description="system time",
58+
)
59+
)
60+
61+
def get_symbol_containers(self, variable_assignments, parameter_assignments):
62+
variables = [SimVariable(assg.variable) for assg in variable_assignments]
63+
parameters = [SimParameter(assg.parameter) for assg in parameter_assignments]
64+
return Variables(variables), Parameters(parameters)
65+
66+
def create_simulation_variables(self, variable_assignments, variables, parameters):
67+
for assg in variable_assignments:
68+
update_rule = assg.to_update_rule(variables, parameters)
69+
sim_variable = SimVariable(assg.variable)
70+
sim_variable.set_update_rule(update_rule)
71+
self.variables[str(assg.variable.symbol)] = sim_variable
72+
73+
def create_simulation_parameters(self, parameter_assignments, variables, parameters):
74+
for assg in parameter_assignments:
75+
sim_parameter = SimParameter(assg.parameter)
76+
sim_parameter.initialize_update_rule(variables, parameters)
77+
self.parameters[str(assg.parameter.symbol)] = sim_parameter
78+
79+
def update_update_rules(self):
80+
variables = Variables(list(self.variables.values()))
81+
parameters = Parameters(list(self.parameters.values()))
82+
for var in self.variables.values():
83+
new_update_rule = SimUpdateRule.from_update_rule(var.update_rule, variables + self.time, parameters)
84+
var.set_update_rule(new_update_rule)
85+
for par in self.parameters.values():
86+
par.initialize_update_rule(variables + self.time, parameters)
87+
88+
def _compute_parameter_update_order(self):
89+
variable_symbols = {v.symbol for v in self.variables.values()} | {T}
90+
# print("params")
91+
# for par in self.parameters:
92+
# print(type(par), par)
93+
parameter_symbols = {p.symbol: p for p in self.parameters.values()}
94+
# print("param symbol")
95+
# for symbol in parameter_symbols:
96+
# print(type(symbol), symbol)
97+
G = nx.DiGraph()
98+
G.add_nodes_from(parameter_symbols)
99+
for parameter in self.parameters.values():
100+
parsym = parameter.symbol
101+
for dependency in parameter.dependent_parameters():
102+
if dependency.symbol in parameter_symbols:
103+
G.add_edge(dependency.symbol, parsym)
104+
elif dependency.symbol not in variable_symbols:
105+
raise PopulationSystemError(
106+
f"Parameter {parsym} references undefined symbol {dependency}"
107+
)
108+
try:
109+
nodes = nx.topological_sort(G)
110+
except nx.exception.NetworkXUnfeasible:
111+
raise PopulationSystemError(
112+
f"System parameters contain cyclic dependencies"
113+
)
114+
return list(nodes)
115+
116+
117+
118+
class Simulation:
119+
def __init__(self, system, solver = "discrete_int"):
120+
self.system = system
121+
self.variables = system.variables
122+
self.parameters = system.parameters
123+
self.time = system.time
124+
self.solver = solver
125+
126+
def _compute_substitutions(self):
127+
update_order = [str(par) for par in self.system._compute_parameter_update_order()]
128+
print(update_order)
129+
variables = Variables(list(self.variables.values())) + self.time
130+
for parameter in update_order:
131+
self.parameters[parameter].substitute_parameters(variables)
132+
for variable in self.variables.values():
133+
variable.substitute_parameters(variables)
134+
135+
#TODO: Remove variable dependency from update_rule
136+
137+
def simulate(self, t_end, **options):
138+
self._compute_substitutions()
139+
if self.solver == "discrete_int":
140+
assert "n_steps" in options.keys()
141+
n_steps = options["n_steps"]
142+
solver = DiscreteIntegrator(self, t_end, n_steps)
143+
solver.run()
144+
145+
def plot_solution(self, variables, t_range=None):
146+
t_series = self.time.time_series
147+
if t_range is None:
148+
sl = slice(None, None)
149+
else:
150+
lower = bisect(t_series, t_range[0])
151+
upper = bisect(t_series, t_range[1])
152+
sl = slice(lower, upper)
153+
if isinstance(variables, set):
154+
variables = {v: {} for v in variables}
155+
legend = []
156+
for var_name, options in variables.items():
157+
variable = self.variables[var_name]
158+
if isinstance(options, str):
159+
plt.plot(t_series[sl], variable.time_series[sl], options)
160+
else:
161+
plt.plot(t_series[sl], variable.time_series[sl], **options)
162+
legend.append(variable.symbol.name)
163+
plt.legend(legend, loc="best")
164+
plt.xlabel("time")
165+
plt.grid()
166+
plt.show()
167+
168+
169+
class Solver:
170+
def __init__(self, simulation, t_end):
171+
if t_end <= 0 or not isinstance(t_end, int):
172+
raise ValueError(
173+
"Simulation time must terminate at a positive integer, "
174+
f"not '{t_end}'."
175+
)
176+
self.t_end = t_end
177+
self.simulation = simulation
178+
179+
class DiscreteIntegrator(Solver):
180+
def __init__(self, simulation, t_end, n_steps):
181+
super().__init__(simulation, t_end)
182+
self.n_steps = n_steps
183+
184+
def run(self):
185+
for i in range(self.t_end):
186+
self._advance_time_unit(self.n_steps)
187+
188+
def _advance_time(self, time_step):
189+
self.simulation.time.update_buffer()
190+
for variable in self.simulation.variables.values():
191+
variable.update_buffer()
192+
for variable in self.simulation.variables.values():
193+
variable.update_time_series(time_step)
194+
self.simulation.time.update_time_series(time_step)
195+
196+
def _advance_time_unit(self, n_steps):
197+
if n_steps <= 0 or not isinstance(n_steps, int):
198+
raise ValueError(
199+
"Number of time steps in a day must be a positive integer, "
200+
f"not '{n_steps}'."
201+
)
202+
for i in range(n_steps):
203+
self._advance_time(1 / n_steps)
204+
205+
206+
'''
207+
class System_old:
31208
def __init__(
32209
self, population=None, variable_assignments=[], parameter_assignments=[]
33210
):
@@ -230,3 +407,4 @@ def plot_solution(self, variables, t_range=None):
230407
plt.xlabel("time")
231408
plt.grid()
232409
plt.show()
410+
'''

psymple/variables.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __init__(self, parameter, computed_value=None):
102102

103103
def initialize_update_rule(self, variables, parameters):
104104
self.update_rule = SimUpdateRule(
105-
parameters[self.symbol],
105+
#parameters[self.symbol],
106106
self.value,
107107
variables,
108108
parameters,
@@ -154,7 +154,7 @@ class UpdateRule:
154154

155155
def __init__(
156156
self,
157-
variable: Variable,
157+
#variable: Variable,
158158
equation="0",
159159
variables: Variables = Variables(),
160160
parameters: Parameters = Parameters(),
@@ -176,7 +176,7 @@ def __init__(
176176
description: short description of the rule
177177
"""
178178

179-
self.variable = variable
179+
#self.variable = variable
180180
self.equation = sym.sympify(equation, locals=sym_custom_ns)
181181
self._initialize_dependencies(variables, parameters)
182182
self.description = description
@@ -203,7 +203,7 @@ def _initialize_dependencies(
203203
"""
204204
variable_symbols = set(variables.get_symbols())
205205
parameter_symbols = set(parameters.get_symbols())
206-
all_symbols = variable_symbols | parameter_symbols | {T}
206+
all_symbols = variable_symbols | parameter_symbols
207207
equation_symbols = sym.sympify(self.equation, locals=sym_custom_ns).free_symbols
208208
if warn and not equation_symbols.issubset(all_symbols):
209209
undefined_symbols = equation_symbols - all_symbols
@@ -270,17 +270,17 @@ class SimUpdateRule(UpdateRule):
270270

271271
@classmethod
272272
def from_update_rule(cls, rule, variables, parameters):
273-
if rule.variable.symbol in variables:
274-
variable = variables[rule.variable.symbol]
275-
elif rule.variable.symbol in parameters:
276-
variable = parameters[rule.variable.symbol]
277-
else:
278-
raise ValueError(
279-
f"Symbol {rule.variable.symbol} neither in "
280-
f"provided variables {variables} nor parameters {parameters}"
281-
)
273+
#if rule.variable.symbol in variables:
274+
# variable = variables[rule.variable.symbol]
275+
#elif rule.variable.symbol in parameters:
276+
# variable = parameters[rule.variable.symbol]
277+
#else:
278+
# raise ValueError(
279+
# f"Symbol {rule.variable.symbol} neither in "
280+
# f"provided variables {variables} nor parameters {parameters}"
281+
# )
282282
return SimUpdateRule(
283-
variable,
283+
#variable,
284284
rule.equation,
285285
variables,
286286
parameters,

0 commit comments

Comments
 (0)