diff --git a/src/App.jsx b/src/App.jsx index 2d667857..e0fd06b9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -50,6 +50,9 @@ const nodeTypes = { splitter2: Splitter2Node, splitter3: Splitter3Node, bubbler: BubblerNode, + white_noise: SourceNode, + pink_noise: SourceNode, + }; // Defining initial nodes and edges. In the data section, we have label, but also parameters specific to the node. @@ -608,6 +611,13 @@ export default function App() { break; case 'bubbler': nodeData = { ...nodeData, conversion_efficiency: '0.95', vial_efficiency: '0.9', replacement_times: '' }; + break; + case 'white_noise': + nodeData = { ...nodeData, spectral_density: '1', sampling_rate: '' }; + break; + case 'pink_noise': + nodeData = { ...nodeData, spectral_density: '1', num_octaves: '16', sampling_rate: '' }; + break; default: // For any other types, just use basic data break; diff --git a/src/custom_pathsim_blocks.py b/src/custom_pathsim_blocks.py index f6031fa0..1165c40e 100644 --- a/src/custom_pathsim_blocks.py +++ b/src/custom_pathsim_blocks.py @@ -5,14 +5,15 @@ class Process(ODE): - def __init__(self, residence_time=0, ic=0, gen=0): + def __init__(self, residence_time=0, initial_value=0, source_term=0): alpha = -1 / residence_time if residence_time != 0 else 0 super().__init__( - func=lambda x, u, t: x * alpha + sum(u) + gen, initial_value=ic + func=lambda x, u, t: x * alpha + sum(u) + source_term, + initial_value=initial_value, ) self.residence_time = residence_time - self.ic = ic - self.gen = gen + self.initial_value = initial_value + self.source_term = source_term def update(self, t): x = self.engine.get() @@ -39,6 +40,22 @@ def update(self, t): self.outputs.update_from_array(self.fractions * u) +class Splitter2(Splitter): + def __init__(self, f1, f2): + """ + Splitter with two outputs, fractions are f1 and f2. + """ + super().__init__(n=2, fractions=[f1, f2]) + + +class Splitter3(Splitter): + def __init__(self, f1, f2, f3): + """ + Splitter with three outputs, fractions are f1, f2 and f3. + """ + super().__init__(n=3, fractions=[f1, f2, f3]) + + # BUBBLER SYSTEM diff --git a/src/pathsim_utils.py b/src/pathsim_utils.py index f18378c7..a7aaf476 100644 --- a/src/pathsim_utils.py +++ b/src/pathsim_utils.py @@ -19,8 +19,10 @@ PID, Schedule, ) -from .custom_pathsim_blocks import Process, Splitter, Bubbler +from pathsim.blocks.noise import WhiteNoise, PinkNoise +from .custom_pathsim_blocks import Process, Splitter, Splitter2, Splitter3, Bubbler from flask import jsonify +import inspect NAME_TO_SOLVER = { "SSPRK22": pathsim.solvers.SSPRK22, @@ -34,8 +36,8 @@ "amplifier": Amplifier, "amplifier_reverse": Amplifier, "scope": Scope, - "splitter2": Splitter, - "splitter3": Splitter, + "splitter2": Splitter2, + "splitter3": Splitter3, "adder": Adder, "adder_reverse": Adder, "multiplier": Multiplier, @@ -47,6 +49,8 @@ "function": Function, "delay": Delay, "bubbler": Bubbler, + "white_noise": WhiteNoise, + "pink_noise": PinkNoise, } @@ -296,14 +300,25 @@ def auto_block_construction(node: dict, eval_namespace: dict = None) -> Block: block_class = map_str_to_object[block_type] - # skip 'self' - parameters_for_class = block_class.__init__.__code__.co_varnames[1:] + parameters_for_class = inspect.signature(block_class.__init__).parameters + parameters = {} + for k, value in parameters_for_class.items(): + if k == "self": + continue + # Skip 'operations' for Adder, as it is handled separately + # https://github.com/festim-dev/fuel-cycle-sim/issues/73 + if k in ["operations"]: + continue + user_input = node["data"][k] + if user_input == "": + if value.default is inspect._empty: + raise ValueError( + f"expected parameter for {k} in {block_type} ({node['label']})" + ) + parameters[k] = value.default + else: + parameters[k] = eval(user_input, eval_namespace) - parameters = { - k: eval(v, eval_namespace) - for k, v in node["data"].items() - if k in parameters_for_class - } return block_class(**parameters) @@ -329,21 +344,15 @@ def make_blocks( tau=eval(node["data"]["delay"], eval_namespace), ) elif block_type == "splitter2": - block = Splitter( - n=2, - fractions=[ - eval(node["data"]["f1"], eval_namespace), - eval(node["data"]["f2"], eval_namespace), - ], + block = Splitter2( + f1=eval(node["data"]["f1"], eval_namespace), + f2=eval(node["data"]["f2"], eval_namespace), ) elif block_type == "splitter3": - block = Splitter( - n=3, - fractions=[ - eval(node["data"]["f1"], eval_namespace), - eval(node["data"]["f2"], eval_namespace), - eval(node["data"]["f3"], eval_namespace), - ], + block = Splitter3( + f1=eval(node["data"]["f1"], eval_namespace), + f2=eval(node["data"]["f2"], eval_namespace), + f3=eval(node["data"]["f3"], eval_namespace), ) elif block_type == "bubbler": block, events_bubbler = create_bubbler(node) diff --git a/src/templates/template_with_macros.py b/src/templates/template_with_macros.py index f98480d4..9fb28e53 100644 --- a/src/templates/template_with_macros.py +++ b/src/templates/template_with_macros.py @@ -48,7 +48,9 @@ iterations_max={{ solverParams["iterations_max"] }}, log={{ solverParams["log"].capitalize() }}, tolerance_fpi={{ solverParams["tolerance_fpi"] }}, + {%- if solverParams["extra_params"] != '' -%} **{{ solverParams["extra_params"] }}, + {%- endif -%} ) if __name__ == "__main__": diff --git a/test/test_backend.py b/test/test_backend.py index ce689df7..2c46174a 100644 --- a/test/test_backend.py +++ b/test/test_backend.py @@ -3,7 +3,7 @@ auto_block_construction, create_function, ) -from src.custom_pathsim_blocks import Process, Splitter +from src.custom_pathsim_blocks import Process, Splitter2, Splitter3 import pathsim.blocks @@ -33,17 +33,17 @@ "data": {"expression": "3*x**2", "label": "Function"}, }, "delay": {"type": "delay", "data": {"tau": "1.0", "label": "Delay"}}, - "rng": {"type": "rng", "data": {"seed": "42", "label": "RNG"}}, + "rng": {"type": "rng", "data": {"sampling_rate": "2", "label": "RNG"}}, "pid": { "type": "pid", - "data": {"kp": "1.0", "ki": "0.0", "kd": "0.0", "label": "PID"}, + "data": {"Kp": "1.0", "Ki": "0.0", "Kd": "0.0", "f_max": "100", "label": "PID"}, }, "process": { "type": "process", "data": { "residence_time": "1.0", - "ic": "0.0", - "gen": "0.0", + "initial_value": "0.0", + "source_term": "0.0", "label": "Process", }, }, @@ -56,6 +56,23 @@ "data": {"f1": "1/3", "f2": "1/3", "f3": "1/3", "label": "Splitter 3"}, }, "scope": {"type": "scope", "data": {"label": "Scope"}}, + "white_noise": { + "type": "white_noise", + "data": { + "spectral_density": "1", + "sampling_rate": "2", + "label": "White Noise Source", + }, + }, + "pink_noise": { + "type": "pink_noise", + "data": { + "spectral_density": "1", + "num_octaves": "16", + "sampling_rate": "5", + "label": "Pink Noise Source", + }, + }, } @@ -117,8 +134,10 @@ def test_create_integrator(): ("rng", pathsim.blocks.RNG), ("pid", pathsim.blocks.PID), ("process", Process), - ("splitter2", Splitter), - ("splitter3", Splitter), + ("splitter2", Splitter2), + ("splitter3", Splitter3), + ("white_noise", pathsim.blocks.noise.WhiteNoise), + ("pink_noise", pathsim.blocks.noise.PinkNoise), ], ) def test_auto_block_construction(node_factory, block_type, expected_class): @@ -140,8 +159,8 @@ def test_auto_block_construction(node_factory, block_type, expected_class): ("rng", pathsim.blocks.RNG), ("pid", pathsim.blocks.PID), ("process", Process), - ("splitter2", Splitter), - ("splitter3", Splitter), + ("white_noise", pathsim.blocks.noise.WhiteNoise), + ("pink_noise", pathsim.blocks.noise.PinkNoise), ], ) def test_auto_block_construction_with_var(node_factory, block_type, expected_class):